413 lines
10 KiB
TypeScript
413 lines
10 KiB
TypeScript
|
|
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.buffered = true;
|
|
|
|
let offscreen = new CanvasBuffer(width, height);
|
|
for (const node of this.ChildNodes)
|
|
{
|
|
this.DrawChild(
|
|
offscreen,
|
|
node,
|
|
width, height
|
|
);
|
|
};
|
|
offscreen.writeTo(
|
|
this.context,
|
|
0, 0,
|
|
offscreen.width, offscreen.height,
|
|
this.padding.left, this.padding.top,
|
|
offscreen.width, offscreen.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;
|
|
|
|
|
|
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,
|
|
canvasWidth * node.scale,
|
|
canvasHeight * 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);
|
|
}
|
|
} |