import {Vector} from '@avocado/math'; import {Container} from './container'; import {Renderer} from './renderer'; export class Stage extends Container { constructor(size) { super(); // Container element. this.element = window.document.createElement('div'); this.element.className = 'avocado-stage'; this.element.style.position = 'relative'; this.element.style.alignItems = 'center'; this.element.style.display = 'flex'; this.element.style.height = '100%'; this.element.style.lineHeight = '0'; this.element.style.width = '100%'; // DOM parent. this.parent = undefined; // 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(); // 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. if (this.parent) { this.parent.removeChild(this.element); } this.parent = parent; // Add to new parent (if any) and focus. if (parent) { parent.appendChild(this.element); this.element.focus(); } // Recalculate size and ratio. this.onWindowResize(); } createUiLayer() { const node = window.document.createElement('div'); node.className = 'ui'; node.style.position = 'absolute'; node.style.width = `${this.size[0]}px`; node.style.height = `${this.size[1]}px`; 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); } } 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; this.ui.style.transform = `scale(${1 / this._transformRatio})`; } render() { this.renderer.render(this); } 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); } } export class StageEvent { constructor() { this._position = [-1, -1]; this._target = undefined; } get position() { return this._position; } get target() { return this._target; } }