UNPKG

playable.js

Version:

A lightweight HTML5 game engine.

392 lines (355 loc) 12 kB
import {Layer} from './Layer'; import {Ticker} from '../system/Ticker'; import {Vector} from '../geom/Vector'; import {Rectangle} from '../geom/Rectangle'; import {Event} from '../event/Event'; import {TouchEvent} from '../event/TouchEvent'; import {ResourceManager, ResourceInfo, ResourceManagerOption} from '../net/ResourceManager'; export class Stage extends Layer { public static readonly SHOW_ALL: string = 'showAll'; public static readonly EXACT_FIT: string = 'exactFit'; public static readonly NO_SCALE: string = 'noScale'; public static readonly NO_BORDER: string = 'noBorder'; public static readonly FIXED_WIDE: string = 'fixedWide'; public static readonly FIXED_NARROW: string = 'fixedNarrow'; public static readonly FIXED_WIDTH: string = 'fixedWidth'; public static readonly FIXED_HEIGHT: string = 'fixedHeight'; protected $drawCalls: number = 0; protected $activated: boolean = false; protected $scaleMode: string = Stage.SHOW_ALL; protected $viewportWidth: number = 0; protected $viewportHeight: number = 0; protected $viewportBackground: string = null; protected readonly $ticker: Ticker; protected readonly $elementEvents: ElementEvent[]; protected readonly $viewportCanvas: HTMLCanvasElement; protected readonly $viewportContext: CanvasRenderingContext2D; protected readonly $renderBounds: Rectangle = Rectangle.create(); protected readonly $boundResizeViewportCanvas: () => void; public constructor(canvas?: HTMLCanvasElement) { super(); this.$ticker = new Ticker(this); this.$elementEvents = []; this.$viewportCanvas = canvas || document.createElement('canvas'); this.$viewportContext = this.$viewportCanvas.getContext('2d'); this.$boundResizeViewportCanvas = this.$resizeViewportCanvas.bind(this); this.$resizeViewportCanvas(); this.$initEvents(); this.width = 320; this.height = 568; if (!canvas) { this.$viewportCanvas.style.top = '0'; this.$viewportCanvas.style.left = '0'; this.$viewportCanvas.style.position = 'fixed'; document.body.appendChild(this.$viewportCanvas); } } public get x(): number { return 0; } public set x(x: number) { this.$x = 0; } public get y(): number { return 0; } public set y(y: number) { this.$y = 0; } public get ticker(): Ticker { return this.$ticker; } public get fps(): number { return this.$ticker.fps; } public get drawCalls(): number { return this.$drawCalls; } public get activated(): boolean { return this.$activated; } public get scaleMode(): string { return this.$scaleMode; } public set scaleMode(scaleMode: string) { if (this.scaleMode !== scaleMode) { this.$scaleMode = scaleMode; this.$resizeCanvas(); } } public get viewportCanvas(): HTMLCanvasElement { return this.$viewportCanvas; } public get viewportWidth(): number { return this.$viewportWidth ? this.$viewportWidth : this.$viewportCanvas.width / Layer.pixelRatio; } public set viewportWidth(width: number) { if (this.$viewportWidth !== width) { this.$viewportWidth = width; this.$resizeViewportCanvas(); } } public get viewportHeight(): number { return this.$viewportHeight ? this.$viewportHeight : this.$viewportCanvas.height / Layer.pixelRatio; } public set viewportHeight(height: number) { if (this.$viewportHeight !== height) { this.$viewportHeight = height; this.$resizeViewportCanvas(); } } public get viewportBackground(): string { return this.$viewportBackground; } public set viewportBackground(viewportBackground: string) { this.$viewportBackground = viewportBackground; this.$viewportCanvas.style.background = viewportBackground; } public createResourceManager(list: Array<ResourceInfo>, options?: ResourceManagerOption): ResourceManager { return new ResourceManager(this, list, options); } public removeSelf(): this { let canvas = this.$viewportCanvas; if (canvas.parentElement) { canvas.parentElement.removeChild(canvas); } if (this.$activated) { this.$activated = false; this.emit(Event.DEACTIVATE); } if (this.$stage) { this.$emitRemovedFromStage(); } this.$removeElementEvents(); return this; } protected $initEvents(): void { let prefix; let hiddenKey; let resizeTimer; let ticker = this.$ticker; let prefixes = ['', 'o', 'ms', 'moz', 'webkit']; for (prefix of prefixes) { hiddenKey = prefix ? prefix + 'Hidden' : 'hidden'; if (document[hiddenKey] !== undefined) { break; } } this.$addTouchEventListeners(); this.$addElementEvent(window, 'orientationchange', () => { ticker.clearTimeout(resizeTimer); resizeTimer = ticker.setTimeout(this.$boundResizeViewportCanvas, 100); }); this.$addElementEvent(window, prefix + 'visibilitychange', () => { let hidden = document[hiddenKey]; this.$activated = !hidden; this.emit(hidden ? Event.DEACTIVATE : Event.ACTIVATE); }); this.on(Event.ENTER_FRAME, this.$render); setTimeout(() => { if (!document[hiddenKey]) { this.$activated = true; this.emit(Event.ACTIVATE); } this.$emitAddedToStage(this); }); } protected $addElementEvent(target: EventTarget, type: string, listener: (event) => void, options?: boolean | AddEventListenerOptions): void { target.addEventListener(type, listener, options); this.$elementEvents.push({target, type, listener}); } protected $removeElementEvents(): void { let listeners = this.$elementEvents; for (let {target, type, listener} of listeners) { target.removeEventListener(type, listener); } listeners.length = 0; } protected $addTouchEventListeners(): void { if (document.ontouchstart !== undefined) { this.$addElementEvent(document, 'touchstart', event => { this.$dispatchTouches(TouchEvent.TOUCH_START, event); }); this.$addElementEvent(document, 'touchmove', event => { this.$dispatchTouches(TouchEvent.TOUCH_MOVE, event); event.preventDefault(); }, {passive: false}); this.$addElementEvent(document, 'touchend', event => { this.$dispatchTouches(TouchEvent.TOUCH_END, event); this.$dispatchTouches(TouchEvent.TOUCH_TAP, event); }); this.$addElementEvent(document, 'touchcancel', event => { this.$dispatchTouches(TouchEvent.TOUCH_CANCEL, event); }); } else { let touching = false; this.$addElementEvent(window, 'mousedown', event => { this.$dispatchTouchEvent(TouchEvent.TOUCH_START, event.pageX, event.pageY, 0); touching = true; }); this.$addElementEvent(window, 'mousemove', event => { if (touching) { this.$dispatchTouchEvent(TouchEvent.TOUCH_MOVE, event.pageX, event.pageY, 0); } }); this.$addElementEvent(window, 'mouseup', event => { this.$dispatchTouchEvent(TouchEvent.TOUCH_END, event.pageX, event.pageY, 0); touching = false; }); this.$addElementEvent(window, 'click', event => { this.$dispatchTouchEvent(TouchEvent.TOUCH_TAP, event.pageX, event.pageY, 0); }); this.$addElementEvent(window, 'blur', () => { this.$dispatchTouchEvent(TouchEvent.TOUCH_CANCEL, 0, 0, 0); touching = false; }); } } protected $dispatchTouches(type: string, event: any): void { let touches = event['changedTouches']; for (let i = 0, l = touches.length; i < l; ++i) { let touch = touches[i]; this.$dispatchTouchEvent(type, touch.pageX, touch.pageY, touch.identifier); } } protected $dispatchTouchEvent(type: string, pageX: number, pageY: number, identifier: number): void { if (this.$ticker.paused || !this.$visible || !this.touchable) { return; } let event = TouchEvent.create(type); let scrollX = window.scrollX || 0; let scrollY = window.scrollY || 0; let width = this.$canvas.width; let height = this.$canvas.height; let bounds = this.$renderBounds; let pixelRatio = Layer.pixelRatio; let viewportBounds = this.$viewportCanvas.getBoundingClientRect(); let x = (pageX - scrollX - viewportBounds.left - bounds.x / pixelRatio) * width / bounds.width - this.$anchorX; let y = (pageY - scrollY - viewportBounds.top - bounds.y / pixelRatio) * height / bounds.height - this.$anchorY; let matrix = this.$getTransform(); let localPos = Vector.create(x, y).transform(matrix.invert()).subtract(this.$anchorX, this.$anchorY); let inside = this.$localHitTest(localPos); if (inside || type !== TouchEvent.TOUCH_START) { event.localX = localPos.x; event.localY = localPos.y; event.targetX = event.stageX = x; event.targetY = event.stageY = y; event.identifier = identifier; this.$emitTouchEvent(event, inside); } event.release(); matrix.release(); localPos.release(); } protected $calculateRenderBounds(): void { let x = 0; let y = 0; let canvas = this.$canvas; let width = canvas.width; let height = canvas.height; let scaleMode = this.$scaleMode; let bounds = this.$renderBounds; let aspectRatio = width / height; let viewportCanvas = this.$viewportCanvas; let viewportWidth = viewportCanvas.width; let viewportHeight = viewportCanvas.height; let viewportAspectRatio = viewportWidth / viewportHeight; if (scaleMode === Stage.SHOW_ALL) { if (aspectRatio > viewportAspectRatio) { width = viewportWidth; height = width / aspectRatio; } else { height = viewportHeight; width = height * aspectRatio; } } else if (scaleMode === Stage.EXACT_FIT) { width = viewportWidth; height = viewportHeight; } else if (scaleMode === Stage.NO_BORDER) { if (aspectRatio < viewportAspectRatio) { width = viewportWidth; height = width / aspectRatio; } else { height = viewportHeight; width = height * aspectRatio; } x = (viewportWidth - width) / 2; y = (viewportHeight - height) / 2; } else if (scaleMode === Stage.FIXED_WIDTH) { width = viewportWidth; height = width / aspectRatio; } else if (scaleMode === Stage.FIXED_HEIGHT) { height = viewportHeight; width = height * aspectRatio; } else if (scaleMode === Stage.FIXED_WIDE) { if (viewportWidth > viewportHeight) { width = viewportWidth; height = width / aspectRatio; } else { height = viewportHeight; width = height * aspectRatio; } } else if (scaleMode === Stage.FIXED_NARROW) { if (viewportWidth < viewportHeight) { width = viewportWidth; height = width / aspectRatio; } else { height = viewportHeight; width = height * aspectRatio; } } if (width < viewportWidth) { x = (viewportWidth - width) / 2; } if (height < viewportHeight) { y = (viewportHeight - height) / 2; } bounds.x = x; bounds.y = y; bounds.width = width; bounds.height = height; } protected $resizeCanvas(): void { super.$resizeCanvas(); this.$calculateRenderBounds(); } protected $resizeViewportCanvas(): void { let canvas = this.$viewportCanvas; let pixelRatio = Layer.pixelRatio; let viewportWidth = this.$viewportWidth || window.innerWidth; let viewportHeight = this.$viewportHeight || window.innerHeight; let canvasWidth = viewportWidth * pixelRatio; let canvasHeight = viewportHeight * pixelRatio; if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) { canvas.width = canvasWidth; canvas.height = canvasHeight; canvas.style.width = viewportWidth + 'px'; canvas.style.height = viewportHeight + 'px'; this.$calculateRenderBounds(); this.$markDirty(); this.emit(Event.VIEWPORT_RESIZE); } } protected $render(): number { if (!this.$dirty) { this.$drawCalls = 0; return 0; } let drawCalls = super.$render(); let canvas = this.$canvas; let ctx = this.$viewportContext; let bounds = this.$renderBounds; let viewportCanvas = this.$viewportCanvas; let viewportWidth = viewportCanvas.width; let viewportHeight = viewportCanvas.height; ctx.clearRect(0, 0, viewportWidth, viewportHeight); ctx.drawImage(canvas, (bounds.x + 0.5) | 0, (bounds.y + 0.5) | 0, (bounds.width + 0.5) | 0, (bounds.height + 0.5) | 0); this.$drawCalls = ++drawCalls; return drawCalls; } } interface ElementEvent { target: EventTarget, type: string, listener: (event) => void }