type DrawArea = {width:number,height:number}; type DrawFunction = (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 { this.context.clear(); 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) { x += current.parentNode.padding.left; y += current.parentNode.padding.top; 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 = 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.drawScope.set("draw", e); } public handleDrawBefore(e:DrawFunction) { this.drawScope.set("draw:before", e); } public handleDrawAfter(e:DrawFunction) { 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); } }