UNPKG

playable.js

Version:

A lightweight HTML5 game engine.

693 lines (613 loc) 17.3 kB
import {Stage} from './Stage'; import {Ticker} from '../system/Ticker'; import {Matrix} from '../geom/Matrix'; import {Vector} from '../geom/Vector'; import {Rectangle} from '../geom/Rectangle'; import {Event} from '../event/Event'; import {TouchEvent} from '../event/TouchEvent'; import {EventEmitter} from '../event/EventEmitter'; export class Layer extends EventEmitter { public static pixelRatio: number = typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1; public name: string = ''; public tag: string = ''; public touchable: boolean = true; protected $x: number = 0; protected $y: number = 0; protected $width: number = 0; protected $height: number = 0; protected $anchorX: number = 0; protected $anchorY: number = 0; protected $skewX: number = 0; protected $skewY: number = 0; protected $scaleX: number = 1; protected $scaleY: number = 1; protected $rotation: number = 0; protected $alpha: number = 1; protected $visible: boolean = true; protected $smoothing: boolean = true; protected $background: string = null; protected $stage: Stage = null; protected $parent: Layer = null; protected $children: Array<Layer> = []; protected $dirty: boolean = true; protected $shouldEmitTap: boolean = true; protected $touches: Array<boolean> = []; protected readonly $canvas: HTMLCanvasElement; protected readonly $context: CanvasRenderingContext2D; public constructor() { super(); this.$canvas = document.createElement('canvas'); this.$context = this.$canvas.getContext('2d'); } public get x(): number { return this.$x; } public set x(x: number) { if (this.$x !== x) { this.$x = x; this.$markParentDirty(); } } public get y(): number { return this.$y; } public set y(y: number) { if (this.$y !== y) { this.$y = y; this.$markParentDirty(); } } public get width(): number { return this.$width ? this.$width : this.$canvas.width / Layer.pixelRatio; } public set width(width: number) { if (this.$width !== width) { this.$width = width; this.$resizeCanvas(); } } public get height(): number { return this.$height ? this.$height : this.$canvas.height / Layer.pixelRatio; } public set height(height: number) { if (this.$height !== height) { this.$height = height; this.$resizeCanvas(); } } public get anchorX(): number { return this.$anchorX; } public set anchorX(anchorX: number) { if (this.$anchorX !== anchorX) { this.$anchorX = anchorX; this.$resizeCanvas(); } } public get anchorY(): number { return this.$anchorY; } public set anchorY(anchorY: number) { if (this.$anchorY !== anchorY) { this.$anchorY = anchorY; this.$resizeCanvas(); } } public get skewX(): number { return this.$skewX; } public set skewX(skewX: number) { if (this.$skewX !== skewX) { this.$skewX = skewX; this.$markParentDirty(); } } public get skewY(): number { return this.$skewY; } public set skewY(skewY: number) { if (this.$skewY !== skewY) { this.$skewY = skewY; this.$markParentDirty(); } } public get scaleX(): number { return this.$scaleX; } public set scaleX(scaleX: number) { if (this.$scaleX !== scaleX) { this.$scaleX = scaleX; this.$markParentDirty(); } } public get scaleY(): number { return this.$scaleY; } public set scaleY(scaleY: number) { if (this.$scaleY !== scaleY) { this.$scaleY = scaleY; this.$markParentDirty(); } } public get rotation(): number { return this.$rotation; } public set rotation(rotation: number) { if (this.$rotation !== rotation) { this.$rotation = rotation; this.$markParentDirty(); } } public get alpha(): number { return this.$alpha; } public set alpha(alpha: number) { if (this.$alpha !== alpha) { this.$alpha = alpha; this.$markParentDirty(); } } public get visible(): boolean { return this.$visible; } public set visible(visible: boolean) { if (this.$visible !== visible) { this.$visible = visible; this.$markParentDirty(); } } public get smoothing(): boolean { return this.$smoothing; } public set smoothing(smoothing: boolean) { this.$smoothing = smoothing; this.$resizeCanvas(); } public get background(): string { return this.$background; } public set background(background: string) { if (this.$background !== background) { this.$background = background; this.$markDirty(); } } public get stage(): Stage { return this.$stage; } public get parent(): Layer { return this.$parent; } public get numChildren(): number { return this.$children.length; } public get ticker(): Ticker { return this.$stage ? this.$stage.ticker : null; } public get canvas(): HTMLCanvasElement { return this.$canvas; } public addChild(child: Layer): this { return this.addChildAt(child, this.$children.length); } public addChildAt(child: Layer, index: number): this { let children = this.$children; if (child.$parent) { child.$parent.removeChild(child); } if (index < 0 || index > children.length) { index = children.length; } child.$emitAdded(this); children.splice(index, 0, child); this.$resizeCanvas(); return this; } public replaceChild(oldChild: Layer, newChild: Layer): this { let index = this.getChildIndex(oldChild); this.removeChildAt(index); this.addChildAt(newChild, index); return this; } public getChildByName(name: string): Layer { let children = this.$children; for (let child of children) { if (child.name === name) { return child; } } return null; } public getChildrenByTag(tag: string): Array<Layer> { let result = []; let children = this.$children; for (let child of children) { if (child.tag === tag) { result.push(child); } } return result; } public getChildAt(index: number): Layer { return this.$children[index] || null; } public getChildIndex(child: Layer): number { return this.$children.indexOf(child); } public hasChild(child: Layer): boolean { return this.getChildIndex(child) >= 0; } public swapChildren(child1: Layer, child2: Layer): this { let index1 = this.getChildIndex(child1); let index2 = this.getChildIndex(child2); if (index1 >= 0 && index2 >= 0) { this.swapChildrenAt(index1, index2); } return this; } public swapChildrenAt(index1: number, index2: number): this { let child1 = this.$children[index1]; let child2 = this.$children[index2]; if (index1 !== index2 && child1 && child2) { this.$children[index1] = child2; this.$children[index2] = child1; this.$markDirty(); } return this; } public setChildIndex(child: Layer, index: number): this { let children = this.$children; let oldIndex = this.getChildIndex(child); if (index < 0) { index = 0; } else if (index > children.length) { index = children.length; } if (oldIndex >= 0 && index > oldIndex) { for (let i = oldIndex + 1; i <= index; ++i) { children[i - 1] = children[i]; } children[index] = child; this.$markDirty(); } else if (oldIndex >= 0 && index < oldIndex) { for (let i = oldIndex - 1; i >= index; --i) { children[i + 1] = children[i]; } children[index] = child; this.$markDirty(); } return this; } public removeChild(child: Layer): this { let index = this.getChildIndex(child); return this.removeChildAt(index); } public removeChildAt(index: number): this { let children = this.$children; let child = children[index]; if (child) { children.splice(index, 1); child.$emitRemoved(); this.$resizeCanvas(); } return this; } public removeChildByName(name: string): this { let children = this.$children; for (let i = 0, l = children.length; i < l; ++i) { let child = children[i]; if (child.name === name) { this.removeChildAt(i); break; } } return this; } public removeChildrenByTag(tag: string): this { let children = this.$children; for (let i = children.length - 1; i >= 0; --i) { let child = children[i]; if (child.tag === tag) { this.removeChildAt(i); } } return this; } public removeAllChildren(): this { let children = this.$children; for (let child of children) { child.$emitRemoved(); } this.$children.length = 0; this.$resizeCanvas(); return this; } public removeSelf(): this { if (this.$parent) { this.$parent.removeChild(this); } return this; } protected $markDirty(sizeDirty?: boolean): void { if (sizeDirty) { this.$resizeParentCanvas(); } else if (!this.$dirty) { this.$markParentDirty(); } this.$dirty = true; } protected $markParentDirty(): void { if (this.$parent) { this.$parent.$markDirty(); } } protected $resizeCanvas(): void { let width = this.$width; let height = this.$height; let canvas = this.$canvas; let anchorX = this.$anchorX; let anchorY = this.$anchorY; let context = this.$context; let smoothing = this.$smoothing; let pixelRatio = Layer.pixelRatio; if (width && height) { canvas.width = width * pixelRatio; canvas.height = height * pixelRatio; } else { let bounds = this.$getContentBounds(); canvas.width = (width || bounds.right + anchorX) * pixelRatio; canvas.height = (height || bounds.bottom + anchorY) * pixelRatio; bounds.release(); } if (context.imageSmoothingEnabled !== smoothing) { context.imageSmoothingEnabled = smoothing; } this.$markDirty(true); } protected $resizeParentCanvas(): void { if (this.$parent) { this.$parent.$resizeCanvas(); } } protected $getTransform(): Matrix { let degToRad = Math.PI / 180; let matrix = Matrix.create(); matrix.translate(-this.$anchorX, -this.$anchorY); matrix.skew(this.skewX * degToRad, this.skewY * degToRad); matrix.rotate(this.rotation * degToRad); matrix.scale(this.scaleX, this.scaleY); matrix.translate(this.x, this.y); return matrix; } protected $getChildTransform(child: Layer): Matrix { return child.$getTransform(); } protected $getChildBounds(child: Layer): Rectangle { let width = child.width; let height = child.height; let bounds = Rectangle.create(); let matrix = this.$getChildTransform(child); let topLeft = Vector.create(0, 0).transform(matrix); let topRight = Vector.create(width, 0).transform(matrix); let bottomLeft = Vector.create(0, height).transform(matrix); let bottomRight = Vector.create(width, height).transform(matrix); let minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); let maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); let minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); let maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); bounds.top = minY; bounds.bottom = maxY; bounds.left = minX; bounds.right = maxX; matrix.release(); topLeft.release(); topRight.release(); bottomLeft.release(); bottomRight.release(); return bounds; } protected $getContentBounds(): Rectangle { let bounds; let children = this.$children; for (let child of children) { if (child.$visible) { let childBounds = this.$getChildBounds(child); if (bounds) { bounds.top = Math.min(bounds.top, childBounds.top); bounds.bottom = Math.max(bounds.bottom, childBounds.bottom); bounds.left = Math.min(bounds.left, childBounds.left); bounds.right = Math.max(bounds.right, childBounds.right); childBounds.release(); } else { bounds = childBounds; } } } bounds = bounds || Rectangle.create(); return bounds; } protected $emitTouchEvent(event: TouchEvent, inside: boolean): boolean { let type = event.type; let localX = event.localX; let localY = event.localY; let touches = this.$touches; let identifier = event.identifier; if (type === TouchEvent.TOUCH_START) { this.$shouldEmitTap = true; touches[identifier] = true; } else if (!touches[identifier]) { return false; } else if (type === TouchEvent.TOUCH_TAP || type === TouchEvent.TOUCH_CANCEL) { touches[identifier] = false; } if (type === TouchEvent.TOUCH_MOVE) { this.$shouldEmitTap = false; } let children = this.$children; for (let i = children.length - 1; i >= 0; --i) { let child = children[i]; if (!child.$visible || !child.touchable) { continue; } let matrix = this.$getChildTransform(child); let localPos = Vector.create(localX, localY).transform(matrix.invert()).subtract(child.$anchorX, child.$anchorY); let inside = child.$localHitTest(localPos); localPos.release(); matrix.release(); if (inside || type !== TouchEvent.TOUCH_START) { event.target = child; event.localX = event.targetX = localPos.x; event.localY = event.targetY = localPos.y; if (child.$emitTouchEvent(event, inside)) { break; } } } if (type === TouchEvent.TOUCH_TAP && (!inside || !this.$shouldEmitTap)) { return true; } if (!event.cancelBubble) { event.localX = localX; event.localY = localY; this.emit(event); } return true; } protected $emitAdded(parent: Layer): void { let stage = parent.$stage; this.$parent = parent; this.emit(Event.ADDED); if (stage) { this.$emitAddedToStage(stage); } } protected $emitRemoved(): void { let stage = this.$stage; this.$parent = null; this.emit(Event.REMOVED); if (stage) { this.$emitRemovedFromStage(); } } protected $emitAddedToStage(stage: Stage): void { let children = this.$children; this.$stage = stage; this.emit(Event.ADDED_TO_STAGE); if (this.hasEventListener(Event.ENTER_FRAME)) { stage.ticker.registerEnterFrameCallback(this); } for (let child of children) { child.$emitAddedToStage(stage); } } protected $emitRemovedFromStage(): void { let stage = this.$stage; let children = this.$children; this.$stage = null; this.emit(Event.REMOVED_FROM_STAGE); if (this.hasEventListener(Event.ENTER_FRAME)) { stage.ticker.unregisterEnterFrameCallback(this); } for (let child of children) { child.$emitRemovedFromStage(); } } protected $localHitTest(vector: Vector): boolean { return vector.x >= -this.anchorX && vector.x <= this.width - this.anchorX && vector.y >= -this.anchorY && vector.y <= this.height - this.anchorY; } protected $isChildVisible(child: Layer): boolean { if (!child.visible || !child.alpha || !child.width || !child.height) { return false; } let minX = -this.$anchorX; let maxX = this.width + minX; let minY = -this.$anchorY; let maxY = this.height + minY; let bounds = this.$getChildBounds(child); let inside = bounds.left <= maxX && bounds.right >= minX && bounds.top <= maxY && bounds.bottom >= minY; bounds.release(); return inside; } protected $drawChild(child: Layer): number { let ctx = this.$context; let canvas = child.$canvas; let width = child.width; let height = child.height; let pixelRatio = Layer.pixelRatio; let matrix = this.$getChildTransform(child).scale(pixelRatio); let drawCalls = child.$render(); let globalAlpha = ctx.globalAlpha; if (globalAlpha !== child.alpha) { ctx.globalAlpha = child.alpha; } if (matrix.b === 0 && matrix.c === 0) { let tx = (matrix.tx + 0.5) | 0; let ty = (matrix.ty + 0.5) | 0; width = (width * matrix.a) + 0.5 | 0; height = (height * matrix.d) + 0.5 | 0; ctx.drawImage(canvas, tx, ty, width, height); } else { ctx.save(); ctx.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); ctx.drawImage(canvas, 0, 0, width, height); ctx.restore(); } if (globalAlpha !== child.alpha) { ctx.globalAlpha = globalAlpha; } matrix.release(); return drawCalls + 1; } protected $render(): number { if (!this.$dirty) { return 0; } let drawCalls = 0; let ctx = this.$context; let canvas = this.$canvas; let children = this.$children; let canvasWidth = canvas.width; let canvasHeight = canvas.height; let anchorX = (this.$anchorX + 0.5) | 0; let anchorY = (this.$anchorY + 0.5) | 0; let background = this.$background; let pixelRatio = Layer.pixelRatio; ctx.globalAlpha = 1; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvasWidth, canvasHeight); if (background) { ctx.fillStyle = background; ctx.fillRect(0, 0, canvasWidth, canvasHeight); } ctx.translate(anchorX * pixelRatio, anchorY * pixelRatio); for (let child of children) { if (this.$isChildVisible(child)) { drawCalls += this.$drawChild(child); } } this.$dirty = false; return drawCalls; } public on(type: string, listener: (...args: any[]) => void): this { super.on(type, listener); if (type === Event.ENTER_FRAME && this.ticker) { this.ticker.registerEnterFrameCallback(this); } else if (type === Event.ADDED && this.$parent) { let event = Event.create(type); listener.call(this, event); event.release(); } else if (type === Event.ADDED_TO_STAGE && this.$stage) { let event = Event.create(type); listener.call(this, event); event.release(); } return this; } public off(type: string, listener?: (...args: any[]) => void): this { super.off(type, listener); if (type === Event.ENTER_FRAME && !this.hasEventListener(Event.ENTER_FRAME) && this.ticker) { this.ticker.unregisterEnterFrameCallback(this); } return this; } }