/** * @class Stage * @description Creates and handles the canvas element. included in the options * parameter is optional dependency injection used for testing against * a virtual dom. * @author Chris Peters * * @param {Integer} [width] The width of the canvas * @param {Integer} [height] The height of the canvas * @param {Object} [opts] Stage options * @param {HTMLElement} [opts.parentEl] The element with which to attach the canvas. * If none given the body is used. * @param {String} [opts.bgColor] The parent element's bg color * @param {Object} [opts.document] For testing * @param {Object} [opts.window] For testing * @param {Boolean} [opts.fill] Set to false to not maximally fill viewport. * Default is true. */ export default class Stage { constructor(width = 800, height = 600, opts = {}) { this._fill = opts.fill === undefined ? true : opts.fill; this._width = width; this._height = height; this._document = opts.document || document; this._window = opts.window || window; this._parentEl = opts.parentEl || this._document.body; this._document.documentElement.style.backgroundColor = opts.bgColor; this._createStageElements(); this._window.addEventListener('resize', this._handleResize.bind(this)); this._window.addEventListener('orientationchange', this._handleResize.bind(this)); this._handleResize(); } _createStageElements() { this._stage = this._document.createElement('div'); this._parentEl.appendChild(this._stage); this._textfield = this._document.createElement('input'); this._textfield.type = 'text'; this._textfield.style.position = 'absolute'; this._textfield.style.top = '-999px'; // TODO verify value 'none' this._textfield.autocapitalize = 'none'; this._textfield.id = 'textfield'; this._stage.appendChild(this._textfield); this._video = this._document.createElement('video'); this._video.id ='video'; this._video.style.position = 'absolute'; this._stage.appendChild(this._video); this._canvas = this._document.createElement('canvas'); this._canvas.width = this._width; this._canvas.height = this._height; this._canvas.style.position = 'absolute'; this._stage.appendChild(this._canvas); } /** * Calls _resizeElement for stage elements * * @method Stage#_handleResize */ _handleResize() { this._resizeElement(this._canvas); this._resizeElement(this._video); } /** * Decides how to handle resize based on options * * @method Stage#_resizeElement * @param {HTMLEntity} el The element to resize */ _resizeElement(el) { if (this._fill) { let { top, left, width, height } = Stage.fill( this._width, this._height, this._window.innerWidth, this._window.innerHeight ); el.style.top = `${Math.round(top)}px`; el.style.left = `${Math.round(left)}px`; el.style.width = `${Math.round(width)}px`; el.style.height = `${Math.round(height)}px`; } else { let { top, left } = Stage.center( this._width, this._height, this._window.innerWidth, this._window.innerHeight ); el.style.top = `${Math.round(top)}px`; el.style.left = `${Math.round(left)}px`; } } /** * Returns the canvas element * * @method Stage#getCanvas * @return {HTMLElement} */ getCanvas() { return this._canvas; } /** * Returns the video element * * @method Stage#getVideo * @return {HTMLElement} */ getVideo() { return this._video; } /** * Maximizes an element (with aspect ratio intact) in the viewport via CSS. * * @method Stage.fill * @param {Integer} width The element's original width attribute * @param {Integer} height The element's original height attribute * @param {Integer} viewportWidth The viewport's current width * @param {Integer} viewportHeight The viewport's current height * @return {Object} The new top, left, width, & height */ static fill(width, height, viewportWidth, viewportHeight) { const LANDSCAPE_RATIO = height / width; const PORTRAIT_RATIO = width / height; const IS_LANDSCAPE = LANDSCAPE_RATIO < PORTRAIT_RATIO ? true : false; let winLandscapeRatio = viewportHeight / viewportWidth; let winPortraitRatio = viewportWidth / viewportHeight; let offsetLeft = 0; let offsetTop = 0; let offsetWidth; let offsetHeight; if (IS_LANDSCAPE) { if (LANDSCAPE_RATIO < winLandscapeRatio) { offsetWidth = viewportWidth; offsetHeight = offsetWidth * LANDSCAPE_RATIO; offsetTop = (viewportHeight - offsetHeight) / 2; } else { offsetHeight = viewportHeight; offsetWidth = viewportHeight * PORTRAIT_RATIO; offsetLeft = (viewportWidth - offsetWidth) / 2; } } else { if (PORTRAIT_RATIO < winPortraitRatio) { offsetHeight = viewportHeight; offsetWidth = viewportHeight * PORTRAIT_RATIO; offsetLeft = (viewportWidth - offsetWidth) / 2; } else { offsetWidth = viewportWidth; offsetHeight = offsetWidth * LANDSCAPE_RATIO; offsetTop = (viewportHeight - offsetHeight) / 2; } } return { width: offsetWidth, height: offsetHeight, left: offsetLeft, top: offsetTop }; } /** * Keeps stage element centered in the viewport * * @method Stage.center * @param {Integer} width The element's original width attribute * @param {Integer} height The element's original height attribute * @param {Integer} viewportWidth The viewport's current width * @param {Integer} viewportHeight The viewport's current height * @return {Object} The top and left */ static center(width, height, viewportWidth, viewportHeight) { return { left: (viewportWidth - width) / 2, top: (viewportHeight - height) / 2 }; } }