Source: renderers/CanvasRenderer.js

import { Utils } from '../common/Utils'
import { MathUtils } from '../math/MathUtils';
import { AlignType } from '../const';

const __ = {
	private: Symbol('private'),
}

// #region Private Functions

// #endregion

/**
 * @class CanvasRenderer
 * The canvas renderer.
 * @memberof THING
 */
class CanvasRenderer {

	/**
	 * The renderer to draw on canvas.
	 * @param {Object} param The initial parameters.
	 */
	constructor(param = {}) {
		this[__.private] = {};
		let _private = this[__.private];

		_private.renderer = null;

		let width = param['width'] || 1;
		let height = param['height'] || 1;

		let renderer = Utils.createObject('CanvasRenderer');
		renderer.resize(width, height);

		_private.renderer = renderer;
	}

	// #region Private

	_limitSize(size) {
		let renderCapabilities = Utils.getCurrentApp().renderCapabilities;
		let maxTextureSize = renderCapabilities.maxTextureSize;

		let width = size[0];
		let height = size[1];
		if (width > maxTextureSize || height > maxTextureSize) {
			// Keep size ratio
			if (width > height) {
				width = Math.min(maxTextureSize, MathUtils.ceilPowerOfTwo(width));
				height = Math.floor(height * (width / size[0]));
			}
			else {
				height = Math.min(maxTextureSize, MathUtils.ceilPowerOfTwo(height));
				width = Math.floor(width * (height / size[1]));
			}
		}

		return [width, height];
	}

	_scaleRow(row, scaleFactor) {
		row.height *= scaleFactor;

		let metrics = row.metrics;
		if (metrics.actualBoundingBoxAscent) {
			metrics.actualBoundingBoxAscent *= scaleFactor;
		}

		if (metrics.actualBoundingBoxDescent) {
			metrics.actualBoundingBoxDescent *= scaleFactor;
		}

		if (metrics.fontBoundingBoxAscent) {
			metrics.fontBoundingBoxAscent *= scaleFactor;
		}

		if (metrics.fontBoundingBoxDescent) {
			metrics.fontBoundingBoxDescent *= scaleFactor;
		}

		if (metrics.actualBoundingBoxLeft) {
			metrics.actualBoundingBoxLeft *= scaleFactor;
		}

		if (metrics.actualBoundingBoxRight) {
			metrics.actualBoundingBoxRight *= scaleFactor;
		}

		if (metrics.width) {
			metrics.width *= scaleFactor;
		}

		let rowOptions = row.options;
		rowOptions.size *= scaleFactor;
	}

	// #endregion

	/**
	 * Clear canvas.
	 * @private
	 */
	clear() {
		let _private = this[__.private];

		_private.renderer.clear();
	}

	/**
	 * Resize.
	 * @param {Number} width The width.
	 * @param {Number} height The height.
	 * @private
	 */
	resize(width, height) {
		let _private = this[__.private];

		_private.renderer.resize(width, height);
	}

	/**
	 * Draw text.
	 * @param {String} text The text string.
	 * @param {Object} options The options.
	 * @param {Number} options.size The font size.
	 * @param {String} options.type The font type.
	 * @param {Number} options.lineWidth The font text width each line.
	 * @param {Number} options.lineHeight The font text height each line.
	 * @param {Number|String|Array<Number>} options.color The font color.
	 * @param {Number|String|Array<Number>} options.shadowColor The font shadow color.
	 * @param {Number} options.shadowAlpha The font shadow alpha.
	 * @param {Number} options.shadowAngle The font shadow angle.
	 * @param {Number} options.shadowBlur The font shadow blur.
	 * @param {Number} options.shadowDistance The font shadow distance.
	 * @param {AlignType} options.alignType The font align type.
	 * @param {Boolean} [options.richText=false] True indicates use rich text.
	 * @param {Boolean} [options.stroke=false] True indicates use storke mode.
	 * @param {Boolean} [options.powerOfTwo=false] True indicates use power of 2 size to fit.
	 * @returns {Boolean}
	 * @private
	 */
	drawText(text, options) {
		let _private = this[__.private];
		let renderer = _private.renderer;

		let result = renderer.getTextRows(text, options);
		if (!result) {
			return false;
		}

		// Limit it with max texture size
		let size = this._limitSize(result.size);

		// If the size has changed then need to re-fit text by scale factor
		if (size[0] != result.size[0] || size[1] != result.size[1]) {
			// Get the scale factor
			let scaleFactor = size[0] / result.size[0];

			result.rows.forEach(row => {
				this._scaleRow(row, scaleFactor);
			});

			result.size = size;
		}

		// Resize it by max texture size
		renderer.resize(size[0], size[1]);

		// Draw text with scale factor
		let drawingOptions = Object.assign({}, options);
		renderer.drawText(result, drawingOptions);

		return true;
	}

	/**
	 * Fill rect with color.
	 * @param {Number} x The left in pixel.
	 * @param {Number} y The top in pixel.
	 * @param {Number} width The width in pixel.
	 * @param {Number} height The height in pixel.
	 * @param {Array<Number>} color The color.
	 * @private
	 */
	fillRect(x, y, width, height, color) {
		let _private = this[__.private];

		_private.renderer.fillRect(x, y, width, height, Utils.parseColor(color));
	}

	/**
	 * Convert to image.
	 * @param {Number} width The width in pixel.
	 * @param {Number} height The height in pixel.
	 * @returns {Object}
	 * @private
	 */
	convertToImage(width, height) {
		let _private = this[__.private];

		return _private.renderer.convertToImage(width, height);
	}

	/**
	 * Save as image.
	 * @param {String} fileName The file name.
	 * @param {Number} width The width in pixel.
	 * @param {Number} height The height in pixel.
	 * @returns {Boolean}
	 * @private
	 */
	saveAsImage(fileName, width, height) {
		let image = this.convertToImage(width, height);
		if (!image) {
			return false;
		}

		return Utils.saveAsFile(fileName, image);
	}

	/**
	 * Get canvas context.
	 * @returns {Object}
	 * @private
	 */
	getContext() {
		let _private = this[__.private];

		return _private.renderer.getContext();
	}

	/**
	 * @typedef {Object} ImageDataResult
	 * @property {Number} width The width in pixel.
	 * @property {Number} height The height in pixel.
	 * @property {Uint8Array} data The pixel data.
	 * @private
	 */

	/**
	 * Get image data.
	 * @returns {ImageDataResult}
	 * @private
	 */
	getImageData() {
		let _private = this[__.private];
		let context = _private.renderer.getContext();

		let imageData = _private.renderer.getImageData(0, 0, context.width, context.height);

		return {
			width: context.width,
			height: context.height,
			data: imageData
		};
	}

	/**
	 * Check class type.
	 * @type {Boolean}
	 * @private
	 */
	get isCanvasRenderer() {
		return true;
	}

	/**
	 * Render from the html text.
	 * @param {String} html The string of DOM Node object to render.
	 * @param {Object} options - Rendering options
	 * @param {String} options.bgcolor - color for the background, any valid CSS color value.
	 * @param {Function} options.filter Should return true if passed node should be included in the output
	 *          (excluding node means excluding it's children as well). Not called on the root node.
	 * @return {Promise} A promise that is fulfilled with a canvas object.
	*/
	renderFromHTML(html, options) {
		let _private = this[__.private];
		return _private.renderer.renderFromHTML(html, options);
	}

}

export { CanvasRenderer }