/**
* @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
};
}
}