import {compose, Property} from '@avocado/core'; import {Vector} from '@avocado/math'; import {Container} from './container'; import {Renderer} from './renderer'; const decorate = compose( Property('camera', { track: true, }), ); export class Stage extends decorate(Container) { constructor(visibleSize, visibleScale) { super(); const size = Vector.mul(visibleSize, visibleScale); // Container element. this.element = window.document.createElement('div'); this.element.className = 'avocado-stage'; this.element.style.height = '100%'; this.element.style.lineHeight = '0'; this.element.style.position = 'relative'; this.element.style.width = '100%'; // DOM parent. this.parent = undefined; // Set scale. this.scale = visibleScale; // Canvas renderer. this.renderer = new Renderer(size); this.renderer.element.style.width = '100%'; this.renderer.element.style.height = '100%'; // "real" dimensions. this.size = size; // Precalc for position/dimension transformation. this._transformRatio = 1; // UI DOM node. this.ui = this.createUiLayer(); this._queuedFindSelectors = {}; // Event handlers. this.onWindowResize = this.onWindowResize.bind(this); window.addEventListener('resize', this.onWindowResize); this.onPointerDown = this.onPointerDown.bind(this); for (const start of ['pointerdown', 'touchstart']) { this.element.addEventListener(start, this.onPointerDown); } this.onPointerMove = this.onPointerMove.bind(this); for (const move of ['pointermove', 'touchmove']) { this.element.addEventListener(move, this.onPointerMove); } this.onPointerUp = this.onPointerUp.bind(this); for (const end of ['pointerup', 'touchend']) { this.element.addEventListener(end, this.onPointerUp); } // Put the renderer and UI in the container element, and mark it // focusable. this.element.appendChild(this.renderer.element); this.element.appendChild(this.ui); this.element.tabIndex = 0; } addToDom(parent) { // Had a parent? Remove. this.removeFromDom(); this.parent = parent; // Add to new parent (if any) and focus. if (parent) { parent.appendChild(this.element); } // Recalculate size and ratio. this.onWindowResize(); } createUiLayer() { const node = window.document.createElement('div'); node.className = 'ui'; node.style.overflow = 'hidden'; node.style.position = 'absolute'; node.style.width = `${this.size[0] / this.scale[0]}px`; node.style.height = `${this.size[1] / this.scale[1]}px`; node.style.left = 0; node.style.top = 0; node.style.transformOrigin = '0 0 0'; return node; } destroy() { window.removeEventListener('resize', this.onWindowResize); this.renderer.destroy(); super.destroy(); if (this.parent) { this.parent.removeChild(this.renderer.element); } for (const start of ['pointerdown', 'touchstart']) { this.element.removeEventListener(start, this.onPointerDown); } for (const move of ['pointermove', 'touchmove']) { this.element.removeEventListener(move, this.onPointerMove); } for (const end of ['pointerup', 'touchend']) { this.element.removeEventListener(end, this.onPointerUp); } } get displaySize() { return [ this.element.style.width, this.element.style.height, ]; } findUiElement(selector) { const node = this.ui.querySelector(selector); if (node) { return Promise.resolve(node); } const queued = this._queuedFindSelectors[selector]; if (queued) { return queued.promise; } let resolve; const promise = new Promise((resolve_) => resolve = resolve_); this._queuedFindSelectors[selector] = {resolve, promise}; return promise } flushUiElements() { const selectors = Object.keys(this._queuedFindSelectors); for (let i = 0; i < selectors.length; i++) { const selector = selectors[i]; const {resolve} = this._queuedFindSelectors[selector]; resolve(this.ui.querySelector(selector)); } this._queuedFindSelectors = {}; } focus() { this.element.focus(); } onPointerDown(event) { const stageEvent = this.stageEventFromNativeEvent(event); this.emit('pointerDown', stageEvent); } onPointerMove(event) { const stageEvent = this.stageEventFromNativeEvent(event); this.emit('pointerMove', stageEvent); } onPointerUp(event) { const stageEvent = this.stageEventFromNativeEvent(event); this.emit('pointerUp', stageEvent); } onWindowResize() { if (!this.parent) { return; } // Find the biggest axe, width or height. const ratio = this.parent.clientWidth / this.parent.clientHeight; const biggest = ratio > (16 / 9) ? 'height' : 'width'; // Key parent client size by axe. const parentClient = { width: this.parent.clientWidth, height: this.parent.clientHeight, }; for (const key of ['height', 'width']) { // Biggest axe? Inherit parent axe size. if (key === biggest) { this.element.style[key] = `${parentClient[biggest]}px`; } // Otherwise, else { // Derive height from width. if ('width' === biggest) { this.element.style.height = `${parentClient[biggest] * 9 / 16}px`; } // Derive width from height. else { this.element.style.width = `${parentClient[biggest] * 16 / 9}px`; } } } // Precalc the transformation ratio and apply it to the UI layer. this._transformRatio = this.size[0] / this.element.clientWidth; const scaleFactor = 1 / this._transformRatio; const scaleX = scaleFactor * this.scale[0]; const scaleY = scaleFactor * this.scale[1]; this.ui.style.transform = `scaleX(${scaleX}) scaleY(${scaleY})`; this.emit('displaySizeChanged', this.displaySize); } removeFromDom() { if (this.parent) { this.parent.removeChild(this.element); this.parent = undefined; } } renderTick(elapsed) { super.renderTick(elapsed); 0; this.renderer.render(this); if (this.camera) { const inverseOffset = Vector.mul( this.camera.realOffset, Vector.scale(this.scale, -1), ); this.position = inverseOffset; } } resolveUiRendered() { this._uiResolve(); } stageEventFromNativeEvent(event) { event = event || window.event; const position = this.transformEventPosition(event); const stageEvent = new StageEvent(); stageEvent._position = position; stageEvent._target = event.target; return stageEvent; } transformCanvasPosition(position) { const rect = this.renderer.element.getBoundingClientRect(); const topLeft = [rect.x, rect.y]; const offset = Vector.sub(position, topLeft); return Vector.div( Vector.scale(offset, this._transformRatio), this.scale, ); } transformEventPosition(event) { let position; if (window.PointerEvent && event instanceof window.PointerEvent) { position = [event.clientX, event.clientY]; } else if (window.TouchEvent && event instanceof window.TouchEvent) { const touches = event.changedTouches; const touch = touches[0]; position = [touch.clientX, touch.clientY]; } return this.transformCanvasPosition(position); } get transformRatio() { return this._transformRatio; } } export class StageEvent { constructor() { this._position = [-1, -1]; this._target = undefined; } get position() { return this._position; } get target() { return this._target; } }