This commit is contained in:
Abdussamed 2023-06-11 16:01:50 +03:00
commit a187e04b80
9 changed files with 2069 additions and 0 deletions

148
Compositor/Canvas.ts Normal file
View File

@ -0,0 +1,148 @@
import Node from "./Node";
import NodeEvent from "./NodeEvent";
export default class Canvas
{
public context : CanvasRenderingContext2D;
public canvas : HTMLCanvasElement;
public width : number;
public height : number;
public rootNode = new Node("Root");
public nodes: [Node,number][] = [];
public pool : DrawPool = new DrawPool();
public constructor()
{
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d") as CanvasRenderingContext2D;
this.installMouseManager();
this.installKeyboardManager();
}
public init()
{
this.canvas.style.imageRendering = "pixelated";
window.document.body.appendChild(this.canvas);
this.recalculateSize();
window.addEventListener("resize",() => this.recalculateSize());
this.pool.trigger = () => this.drawContext();
}
public installMouseManager()
{
this.canvas.addEventListener("mousedown",(event) => {
let mouseEvent = new NodeEvent();
mouseEvent.x = event.offsetX;
mouseEvent.y = event.offsetY;
mouseEvent.type = "mouse:down";
this.rootNode.emit("mouse", mouseEvent);
});
this.canvas.addEventListener("mousemove",(event) => {
let mouseEvent = new NodeEvent();
mouseEvent.x = event.offsetX;
mouseEvent.y = event.offsetY;
mouseEvent.data = {
deltaX: event.movementX,
deltaY: event.movementY
};
mouseEvent.type = "mouse:move";
this.rootNode.emit("mouse", mouseEvent);
});
this.canvas.addEventListener("mouseup",(event) => {
let mouseEvent = new NodeEvent();
mouseEvent.x = event.offsetX;
mouseEvent.y = event.offsetY;
mouseEvent.type = "mouse:up";
this.rootNode.emit("mouse", mouseEvent);
});
this.canvas.addEventListener("click",(event) => {
let mouseEvent = new NodeEvent();
mouseEvent.x = event.offsetX;
mouseEvent.y = event.offsetY;
mouseEvent.type = "mouse:click";
this.rootNode.emit("mouse", mouseEvent);
});
this.canvas.addEventListener("wheel",(event) => {
let mouseEvent = new NodeEvent();
mouseEvent.x = event.offsetX;
mouseEvent.y = event.offsetY;
mouseEvent.data = {
xModulus: event.deltaX == 0 ? 0 : event.deltaX < 0 ? -1 : +1,
yModulus: event.deltaY == 0 ? 0 : event.deltaY < 0 ? -1 : +1,
xScroll: event.deltaX,
yScroll: event.deltaY
};
mouseEvent.type = "mouse:wheel";
this.rootNode.emit("mouse", mouseEvent);
});
}
public installKeyboardManager()
{
}
public recalculateSize()
{
let {
width,
height
} = document.body.getClientRects()[0];
this.canvas.setAttribute("width", width + "px");
this.canvas.setAttribute("height", height + "px");
this.canvas.style.width = width + "px";
this.canvas.style.height = height + "px";
this.width = width;
this.height = height
this.pool.notify();
}
public addNode(node:Node, priority : number= 0)
{
this.nodes.push([node,priority]);
node.on('draw',() => {
this.pool.notify();
});
this.pool.notify();
}
public drawContext()
{
this.rootNode.width = this.width;
this.rootNode.height = this.height;
this.rootNode.ChildNodes = this.nodes.sort(([,n], [,k]) => n - k).map(e => e[0]);
this.context.clearRect(0, 0, this.width, this.height);
this.rootNode.context.resize(this.rootNode.width, this.rootNode.height);
this.rootNode.draw().writeTo(
this.context,
0,
0,
this.rootNode.width,
this.rootNode.height,
0,
0,
this.rootNode.width,
this.rootNode.height
);
}
};
class DrawPool
{
public busy : boolean = true;
public trigger : Function;
public inf : number = -1;
public addPool(e:Function)
{
if(!this.busy)
{
this.notify()
}else this.trigger = e;
}
public notify()
{
this.busy = true;
this.inf = requestAnimationFrame(() => {
if(this.trigger)
{
this.trigger();
}
this.busy = false;
});
}
}

168
Compositor/CanvasBuffer.ts Normal file
View File

@ -0,0 +1,168 @@
export default class CanvasBuffer
{
public offscreen! : OffscreenCanvas;
public context! : OffscreenCanvasRenderingContext2D;
public width: number = 0;
public height: number = 0;
constructor(width?:number, height?:number){
this.offscreen = new OffscreenCanvas(width || 0, height || 0);
this.context = this.offscreen.getContext("2d") as OffscreenCanvasRenderingContext2D;
this.width = width || 0;
this.height = height || 0;
}
public resize(width: number, height: number)
{
if(this.width != width)
{
this.offscreen.width = width;
this.width = width || 0;
}
if(this.height != height)
{
this.offscreen.height = height;
this.height = height || 0;
}
}
public clear()
{
this.context.clearRect(0, 0, this.offscreen.width, this.offscreen.height);
}
public writeFrom(
canvas: OffscreenCanvas | OffscreenCanvasRenderingContext2D | HTMLCanvasElement | CanvasRenderingContext2D | CanvasBuffer,
x?:number,
y?:number,
width?:number,
height?:number,
dx?:number,
dy?:number,
dwidth?:number,
dheight?:number
)
{
let args = [
x,
y,
width,
height,
dx,
dy,
dwidth,
dheight
];
if(
canvas instanceof OffscreenCanvas ||
canvas instanceof HTMLCanvasElement
)
{
_draw_content_to_canvas(
this.context,
canvas,
...args
);
}
if(
canvas instanceof OffscreenCanvasRenderingContext2D ||
canvas instanceof CanvasRenderingContext2D
)
{
_draw_content_to_canvas(
this.context,
canvas.canvas,
...args
);
}
if(
canvas instanceof CanvasBuffer
)
{
_draw_content_to_canvas(
this.context,
canvas.offscreen,
...args
);
}
}
public writeTo(
canvas: OffscreenCanvas | OffscreenCanvasRenderingContext2D | HTMLCanvasElement | CanvasRenderingContext2D | CanvasBuffer,
x?:number,
y?:number,
width?:number,
height?:number,
dx?:number,
dy?:number,
dwidth?:number,
dheight?:number
)
{
let args = [
x,
y,
width,
height,
dx,
dy,
dwidth,
dheight
];
if(
canvas instanceof OffscreenCanvas ||
canvas instanceof HTMLCanvasElement
)
{
let content = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
_draw_content_to_canvas(
content,
this.offscreen,
...args
);
}
if(
canvas instanceof OffscreenCanvasRenderingContext2D ||
canvas instanceof CanvasRenderingContext2D
)
{
_draw_content_to_canvas(
canvas,
this.offscreen,
...args
);
}
if(
canvas instanceof CanvasBuffer
)
{
_draw_content_to_canvas(
canvas.context,
this.offscreen,
...args
);
}
}
}
function _draw_content_to_canvas(
context : OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,
canvas : OffscreenCanvas | HTMLCanvasElement,
x?:number,
y?:number,
width?:number,
height?:number,
dx?:number,
dy?:number,
dwidth?:number,
dheight?:number
)
{
context.drawImage(
canvas,
x || 0,
y || 0,
width || canvas.width,
height || canvas.height,
dx || 0,
dy || 0,
dwidth || canvas.width,
dheight || canvas.height
)
}

409
Compositor/Node.ts Normal file
View File

@ -0,0 +1,409 @@
type DrawArea = {width:number,height:number};
type DrawFunction<T> = (this: T, ctx: CanvasBuffer,area:DrawArea) => any;
import CanvasBuffer from "./CanvasBuffer";
import NodeEvent from "./NodeEvent";
export default class Node
{
public name = "";
public context : CanvasBuffer = new CanvasBuffer();
protected buffered : boolean = false;
protected requiredRedraw : boolean = true;
public ChildNodes : Node[] = [];
public parentNode? : Node;
public width : number = 0;
public height : number = 0;
public x : number = 0;
public y : number = 0;
public scale : number = 1;
public rotate = 0;
public mouseHover : boolean = false;
public padding = {
left: 0,
right: 0,
top: 0,
bottom: 0
};
constructor(name?:string)
{
this.name = name || "";
}
public update()
{
this.requiredRedraw = true;
if(this.parentNode)
{
this.parentNode.update();
}
else
{
this.emit("draw", new NodeEvent(), false);
}
}
public draw(maxWidth?: number, maxHeight?: number) : CanvasBuffer
{
let drawableAreaWidth = this.width;
let drawableAreaHeight = this.height;
if(maxWidth)
{
drawableAreaWidth = Math.min(drawableAreaWidth, maxWidth);
}
if(maxHeight)
{
drawableAreaHeight = Math.min(drawableAreaHeight, maxHeight);
}
if(this.hasEvent("draw"))
{
let cancelDraw = false;
this.handleEvent("draw:before",(callback) => {
return callback.call(this,{
drawableAreaWidth,
drawableAreaHeight
})
});
let frame = this.handleEvent("draw",(event) => {
let offscreen = new CanvasBuffer(drawableAreaWidth, drawableAreaHeight);
cancelDraw = event.apply(this, [
offscreen, {
width: drawableAreaWidth,
height: drawableAreaHeight
}
]);
return offscreen;
}) as CanvasBuffer;
this.context = frame;
this.buffered = true;
this.handleEvent("draw:after",(callback) => {
return callback.call(this,{
drawableAreaWidth,
drawableAreaHeight
})
});
if(!cancelDraw)
{
return frame;
}
};
let width = drawableAreaWidth - (this.padding.right + this.padding.left);
let height = drawableAreaHeight - (this.padding.bottom + this.padding.top);
this.context.resize(width, height);
this.buffered = true;
for (const node of this.ChildNodes)
{
this.DrawChild(
this.context,
node,
width,
height
);
};
return this.context;
}
public getContextArea()
{
return {
width: Math.min(
this.width,
this.parentNode?.width || Infinity
) - (
this.padding.right + this.padding.left
),
height: Math.min(
this.height,
this.parentNode?.height || Infinity
) - (
this.padding.bottom + this.padding.top
)
};
}
public DrawChild(canvas:CanvasBuffer, node:Node, maxWidth: number, maxHeight: number)
{
let canvasWidth = Math.min(node.width, maxWidth);
let canvasHeight = Math.min(node.height, maxHeight);
let x = 0;
let y = 0;
node.handleEvent("draw:before",(callback) => {
return callback.call(node,{
canvasWidth,
canvasHeight
})
});
if(node.width == 0 || node.height == 0)
{
return;
}
canvasWidth = Math.min(node.width, maxWidth);
canvasHeight = Math.min(node.height, maxHeight);
x += node.x;
y += node.y;
x += this.padding.left;
y += this.padding.top;
let frame : CanvasBuffer;
if(!node.buffered)
{
frame = node.draw(canvasWidth, canvasHeight);
}
else
{
if(node.requiredRedraw)
{
frame = node.draw(canvasWidth, canvasHeight);
}
else
{
frame = node.context as CanvasBuffer;
}
};
let context = canvas.context;
context.save();
if(node.rotate != 0)
{
let translateX = canvasWidth / 2;
let translateY = canvasHeight / 2;
translateX += x;
translateY += y;
context.translate(translateX, translateY);
context.rotate(node.rotate);
context.translate(-translateX, -translateY);
}
canvas.writeFrom(
frame.offscreen,
0,
0,
frame.offscreen.width,
frame.offscreen.height,
x,
y,
frame.offscreen.width * node.scale,
frame.offscreen.height * node.scale
)
node.requiredRedraw = false;
context.restore();
node.handleEvent("draw:after",(callback) => {
return callback.call(this,{
canvasWidth,
canvasHeight
})
});
}
public isMatchPoint(x:number, y: number)
{
let {
x:realX,
y:realY,
width:realWidth,
height:realheight
} = this.realOffset();
let top = realY;
let left = realX;
let right = realX + realWidth;
let bottom = realY + realheight;
if(
top < y &&
bottom > y &&
left < x &&
right > x
)
{
return true;
}
return false;
}
protected _events : Map<
string,
(
(
this:Node,
e:NodeEvent
) => any
)[]
> = new Map();
public on(eventName:string, callback: ((this:Node,e:NodeEvent) => any))
{
if(this._events.has(eventName))
{
this._events.get(eventName)?.push(callback);
}else{
this._events.set(eventName,[callback]);
}
}
public realOffset() : {x:number,y:number,width: number, height: number}
{
let {
width,
height
} = this.getContextArea();
if(this.parentNode)
{
let current : Node = this;
let x = 0;
let y = 0;
while(1)
{
x += current.x;
y += current.y;
if(current.parentNode)
{
current = current.parentNode;
}
else
{
break;
}
};
return {
x,
y,
width,
height
}
}else{
let x = this.x;
let y = this.y;
return {
x,
y,
width,
height
}
}
}
public emit(eventName:string, args: NodeEvent, notifyChild:boolean = true)
{
let eventList = this._events.get(eventName);
if(notifyChild)
{
this.ChildNodes.filter(node => {
if(node.isMatchPoint(args.x, args.y))
{
if(!node.mouseHover)
{
node.mouseHover = true;
let event = args.clone();
event.type = "mouse:enter";
node.emit("mouse", event, false);
};
return true
}else{
if(node.mouseHover)
{
node.mouseHover = false;
let event = args.clone();
event.type = "mouse:leave";
node.emit("mouse", event);
};
return false
}
}).some(node => {
node.emit(eventName, args);
return args.prevented != false;
});
}
if(args.stoppedBubbling == false)
{
if(eventList)
{
for (const event of eventList)
{
event.apply(this, [args]);
if(args.prevented)
{
break;
}
}
};
}
};
public drawScope : Map<string, Function> = new Map();
public hasEvent(eventName: string) : boolean
{
return this.drawScope.has(eventName);
}
public handleEvent(eventName: string, onCallback?:(callback:Function) => any) : any
{
let func = this.drawScope.get(eventName);
if(func)
{
if(onCallback)
{
return onCallback(func);
}else{
return func.call(this);
}
}
}
public handleDraw(e:DrawFunction<this>)
{
this.drawScope.set("draw", e);
}
public handleDrawBefore(e:DrawFunction<this>)
{
this.drawScope.set("draw:before", e);
}
public handleDrawAfter(e:DrawFunction<this>)
{
this.drawScope.set("draw:after", e);
}
public addNode(node:Node)
{
let {
width,
height
} = this.getContextArea();
node.width = node.width || width;
node.height = node.height || height;
node.parentNode = this;
this.ChildNodes.push(node);
}
}

24
Compositor/NodeEvent.ts Normal file
View File

@ -0,0 +1,24 @@
export default class NodeEvent
{
public type = "";
public prevented = false;
public stoppedBubbling = false;
public data : any = {};
public keyCode : number;
public key : string;
public x : number;
public y : number;
public clone()
{
let u = new NodeEvent();
u.type = this.type;
u.prevented = this.prevented;
u.stoppedBubbling = this.stoppedBubbling;
u.data = this.data;
u.keyCode = this.keyCode;
u.key = this.key;
u.x = this.x;
u.y = this.y;
return u;
}
}

5
index.css Normal file
View File

@ -0,0 +1,5 @@
html,body{
margin: 0;
height: 100%;
overflow: hidden;
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Indexed</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<script src="./index.js"></script>
</body>
</html>

1161
index.js Normal file

File diff suppressed because it is too large Load Diff

1
index.js.map Normal file

File diff suppressed because one or more lines are too long

140
index.ts Normal file
View File

@ -0,0 +1,140 @@
import Node from "./Compositor/Node";
import Canvas from "./Compositor/Canvas"
import CanvasBuffer from "./Compositor/CanvasBuffer";
let container = new Node("Box");
container.handleDrawBefore(function(){
this.width = 400;
this.height = 400;
this.padding = {
left: 2,
right: 2,
top: 2,
bottom: 2
};
});
let scroll = 2;
container.handleDraw(function(canvasBuffer,area){
let ctx = canvasBuffer.context;
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.rect(2,2,area.width - 2,area.height - 2);
ctx.stroke();
ctx.closePath();
let offscreen = new CanvasBuffer(area.width - 8, area.height - 8);
for (const child of this.ChildNodes)
{
this.DrawChild(offscreen, child, offscreen.width - 8, offscreen.height - 8);
};
offscreen.writeTo(
ctx,
0,0, area.width, area.height,
4,4, area.width, area.height
);
});
class Button extends Node
{
public color : string = "#00ff00";
constructor(x: number, y:number, name: string){
super();
this.x = x;
this.y = y;
this.name = name;
this.handleDrawBefore(function(area){
this.width = 50;
this.height = 50;
});
this.handleDraw(function(canvasBuffer,area){
let ctx = canvasBuffer.context;
ctx.beginPath();
ctx.lineWidth = 1;
ctx.fillStyle = this.color;
ctx.fillRect(0,0,area.width - 4,area.height - 4);
ctx.closePath();
return true;
});
this.on("mouse",(event) => {
switch(event.type)
{
case "mouse:down":{
event.stoppedBubbling = true;
this.color = "#007700";
this.update();
break;
}
case "mouse:up":{
event.stoppedBubbling = true;
this.color = "#00dd00";
this.update();
break;
}
case "mouse:enter":{
event.stoppedBubbling = true;
this.color = "#00dd00";
this.update();
break;
}
case "mouse:leave":{
event.stoppedBubbling = true;
this.color = "#00ff00";
this.update();
break;
}
}
});
}
};
class TextNode extends Node
{
public color : string = "#00ff00";
public textSize: TextMetrics;
public text: string;
public fontSize : number = 24;
constructor(text: string){
super();
this.text = text;
this.context.context.font = "system-ui "+this.fontSize+"px";
this.textSize = this.context.context.measureText(text);
this.handleDrawBefore(function(){
this.width = this.textSize.width;
this.height = 50;
});
this.handleDraw(function(canvasBuffer,area){
let ctx = canvasBuffer.context;
ctx.beginPath();
ctx.lineWidth = 1;
ctx.fillStyle = this.color;
ctx.fillRect(2,2,area.width - 2,area.height - 2);
ctx.closePath();
});
}
};
for(let a = 0; a < 500; a += 50)
{
for(let b = 0; b < 500; b += 50)
{
container.addNode(new Button(a,b,"Box A"));
}
}
let t = new Canvas();
t.init();
t.addNode(container);
t.canvas.addEventListener("wheel",function(event){
event.preventDefault();
event.stopPropagation();
let delta = event.deltaY < 0 ? -5 : +5;
scroll = Math.max(scroll + delta, 0);
container.update();
});