"use strict"; /** * @typedef {Object} KanvasItemType * @property {!string} type */ /** * @typedef {Object} KanvasItemPosition * @property {!number} [x = 0] * @property {!number} [y = 0] */ /** * @typedef {Object} KanvasItemSize * @property {!number} [width = 0] * @property {!number} [height = 0] */ /** * @typedef {Object} KanvasItemCommon * @property {!number} [alpha = 1] * @property {!number} [rotation = 0] */ /** * @typedef {Object} KanvasShapeBase * @property {!string} [color = "#000"] * @property {!number} [border_width = 0] * @property {!string} [border_color = "#000"] * @property {!number} [border_alpha = 1] * @property {!string} [shadow = "#000"] * @property {!number} [shadow_blur = 0] */ /** * @typedef {Object} KanvasObjectChildren * @property {!Array.} [children = []] */ /** * @typedef {Object} KanvasCircleSize * @property {!number} [radius = 0] */ // DSL inputs properties. /** * @typedef {KanvasItemPosition} KanvasBase * @typedef {KanvasItemPosition} KanvasCache * @typedef {KanvasItemPosition & KanvasItemSize} KanvasClean * @typedef {KanvasItemPosition & KanvasItemSize & KanvasShapeBase & KanvasItemCommon} KanvasRectangle * @typedef {KanvasItemPosition & KanvasCircleSize & KanvasShapeBase & KanvasItemCommon} KanvasCircle */ // DSL Objects properties. /** * @typedef {KanvasItemType & KanvasItemPosition} KanvasObjectBase * @typedef {KanvasObjectBase} KanvasObjectCache * @typedef {KanvasObjectBase & KanvasItemSize & KanvasObjectChildren} KanvasObjectClean * @typedef {KanvasObjectBase & KanvasItemSize & KanvasShapeBase & KanvasItemCommon & KanvasObjectChildren} KanvasObjectRectangle * @typedef {KanvasObjectBase & KanvasCircleSize & KanvasShapeBase & KanvasItemCommon & KanvasObjectChildren} KanvasObjectCircle */ /** * @callback kanvas_dsl_callback * @param {!KanvasBase} inputs * @return {KanvasObjectBase} */ /** * @callback kanvas_shape_callback * @param {!KanvasShapeBase} inputs * @returns {boolean} */ /** * @class Kanvas * @constructor * @param {!KanvasInputs} [inputs = {}] * @returns {void} * @access public * @static */ export const Kanvas = (function(){ /** * @typedef {Object} kanvas_inputs * @property {!(HTMLElement|string)} [position = document.querySelector("body")] * @property {!number} [cells = 40] * @property {!number} [frames_per_second = 60] * @property {!number} [quality = 1] * @property {!boolean} [autobuild = true] */ /** * @typedef {Object} kanvas_build * @property {!(HTMLElement|string)} position */ /** * @callback preload_callback * @param {HTMLElement|null} position * @param {boolean} asinchronous * @return {void} */ /** * @typedef {Object} kanvas_preload_html_item * @property {!string} selector * @property {!preload_callback} callback * @property {!number} [timeout = 2000] * @property {!number} [frames_per_second = 60] */ /** * @constructs Kanvas * @param {!kanvas_inputs} [inputs = {}] * @returns {void} * @access private * @static */ const Kanvas = function({ position = document.querySelector("body"), cells = 40, frames_per_second = 60, quality = 1, autobuild = true, thread_mode = Kanvas.THREADS_MODE_ANIMATION } = {}){ /** @type {Kanvas} */ const self = this, /** @type {Object.} */ cache = { /** @type {Object.} */ canvas : {x : 0, y : 0} }, /** @type {number} */ frames_timer = 1000 / frames_per_second; /** @type {number} */ let cell_size = 0, /** @type {boolean} */ built = false, /** @type {number|null} */ thread = null; /** @type {HTMLCanvasElement|null} */ this.canvas = null; /** @type {CanvasRenderingContext2D|null} */ this.context = null; /** @type {Array.} */ this.map = []; /** @type {Kanvas.Event} */ this.threads = new Kanvas.Event(); /** * @returns {void} * @access private */ const constructor = () => { autobuild && self.build(position); }; /** * @param {!kanvas_build} [inputs = {}] * @returns {void} * @access public */ this.build = ({ position = document.querySelector("body") } = {}) => { if(built) return; built = true; self.preload(position, (position, _) => { if(position){ ( self.canvas = position.appendChild(document.createElement("canvas")) ).setAttribute("class", "kanvas"); self.context = self.canvas.getContext("2d"); thread = self.threads.add(thread_executor); }else built = false; }); }; /** * @param {!KanvasObjectBase} item * @return {void} * @access private */ const shape_common = (item, callback) => { if(item.color) self.context.fillStyle = item.color; if(item.border_width){ self.context.lineWidth = item.border_width; self.context.strokeStyle = item.border_color; // self.context.globalAlpha = item.border_alpha; }; if(item.shadow_blur){ self.context.shadowColor = item.shadow; self.context.shadowBlur = item.shadow_blur; } callback(); if(item.border_width) self.context.stroke(); if(item.color) self.context.fill(); }; /** @type {Object.} */ this.shapes = { clean : item => { self.context.clearRect(0, 0, item.width * cell_size, item.height * cell_size); return true; }, rectangle : item => { shape_common(item, () => { self.context.rect(0, 0, item.width * cell_size, item.height * cell_size); }); } }; /** * @param {!Array.} map * @returns {void} * @access private */ const draw = map => { map.forEach(item => { if(!self.shapes[item.type]) return; self.context.save(); self.context.translate(item.x * cell_size, item.y * cell_size); if(item.rotation !== undefined) self.context.rotate(item.rotation * Math.PI / 180); if(item.alpha !== undefined) self.context.globalAlpha = item.alpha; self.shapes[item.type](item) && item.children && draw(item.children); self.context.restore(); }); }; /** * @returns {void} * @access private */ const thread_executor = () => { switch(thread_mode){ case Kanvas.THREAD_MODE_INTERVAL: setInterval(thread_method, frames_timer); break; case Kanvas.THREADS_MODE_TIMEOUT: setTimeout(() => { thread_method(); thread_executor(); }, frames_timer); break; case Kanvas.THREADS_MODE_ANIMATION: /** @type {number} */ const time = Date.now(); requestAnimationFrame(() => { thread_method(); setTimeout(thread_executor, frames_timer - (Date.now() - time)); }); break; case Kanvas.THREADS_MODE_ONLY_ANIMATION: requestAnimationFrame(() => { thread_method(); thread_executor(); }); break; }; }; /** * @returns {void} * @access private */ const thread_method = () => { /** @type {boolean} */ let has_changes = false; if(cache.canvas.x != self.canvas.clientWidth){ has_changes = true; self.canvas.width = (cache.canvas.x = self.canvas.clientWidth) * quality; }; if(cache.canvas.y != self.canvas.clientHeight){ has_changes = true; self.canvas.height = (cache.canvas.y = self.canvas.clientHeight) * quality; } if(has_changes) cell_size = cache.canvas[cache.canvas.x < cache.canvas.y ? "x" : "y"] * quality / cells; self.context.clearRect(0, 0, self.canvas.width, self.canvas.height); draw(self.map); }; constructor(); }; /** @type {Object.} */ Kanvas.DSL = { /** * @param {!string} type * @param {!KanvasBase} inputs * @returns {KanvasObjectBase} * @access public */ base : (type, inputs = {}) => { /** @type {{x: number, y: number}} */ const {x = 0, y = 0} = inputs; return { type : type, x : x, y : y }; }, /** * @param {!KanvasItemCommon} inputs * @returns {KanvasItemCommon} * @access public */ item_common : (inputs = {}) => { /** @type {{alpha: number, rotation: number}} */ const {alpha = 1, rotation = 0} = inputs; return { alpha : alpha, rotation : rotation }; }, /** * @param {!KanvasShapeBase} inputs * @returns {KanvasShapeBase} * @access public */ shape_common : (inputs = {}) => { /** @type {{color: string, border_width: number, border_color: string, border_alpha: number, shadow: string}} */ const { color = "#000", border_width = 0, border_color = "#000", border_alpha = 1, shadow = "#000", shadow_blur = 0 } = inputs; return { color : color, border_width : border_width, border_color : border_color, border_alpha : border_alpha, shadow : shadow, shadow_blur : shadow_blur }; }, /** * @param {!string} type * @param {!KanvasItemSize} inputs * @returns {KanvasItemSize} */ basic_rectangle : (type, inputs = {}) => { /** @type {{width: number, height: number}} */ const {width = 0, height = 0} = inputs; return { ...Kanvas.DSL.base(type, inputs), width : width, height : height }; }, /** * @param {!KanvasClean} inputs * @return {KanvasObjectClean} * @access public */ clean : (inputs = {}) => Kanvas.DSL.basic_rectangle("clean", inputs), /** * @param {!KanvasRectangle} inputs * @return {KanvasObjectRectangle} * @access public */ rectangle : (inputs = {}) => ({ ...Kanvas.DSL.basic_rectangle("rectangle", inputs), ...Kanvas.DSL.item_common(inputs), ...Kanvas.DSL.shape_common(inputs) }), /** * @param {!KanvasCircle} inputs * @return {KanvasObjectCircle} * @access public */ circle : (inputs = {}) => { /** @type {{radius: number}} */ const {radius = 0} = inputs; return { ...Kanvas.DSL.base("circle", inputs), ...Kanvas.DSL.item_common(inputs), ...Kanvas.DSL.shape_common(inputs), radius : radius }; } }; /** @type {number} */ Kanvas.THREAD_MODE_INTERVAL = 0; /** @type {number} */ Kanvas.THREADS_MODE_TIMEOUT = 1; /** @type {number} */ Kanvas.THREADS_MODE_ANIMATION = 2; /** @type {number} */ Kanvas.THREADS_MODE_ONLY_ANIMATION = 3; /** * @class Kanvas.Event * @constructor * @returns {void} * @access public */ Kanvas.Event = function(){ /** * @callback kanvas_event_callback * @param {...(any|null)} [inputs] * @returns {void} */ /** @type {Array.} */ const events = []; /** * @returns {void} * @access private */ const constructor = () => {}; /** * @param {...(any|null)} inputs * @returns {void} * @access public */ this.execute = (...inputs) => { events.forEach(event => { event && event(...inputs); }); }; /** * @param {!kanvas_event_callback} event * @returns {number} * @access public */ this.add = event => { /** @type {number} */ let i = 0; for(; i < events.length; i ++) if(!events[i]) break; events[i] = event; return i; }; /** * @param {!number} i * @return {void} * @access public */ this.remove = i => { if(events[i]) events[i] = null; }; constructor(); }; /** * @param {?any} item * @returns {boolean} * @access public * @static */ Kanvas.is_html_item = item => item && (item.tagName || item.nodeName); /** * @param {!kanvas_preload_html_item} options * @return {void} * @access public * @static */ Kanvas.preload_html_item = ({ selector, callback, timeout = 2000, frames_per_second = 60 } = {}) => { if(!selector || !callback){ callback(null, false); return; }; if(Kanvas.is_html_item(selector)){ callback(selector, false); return; }; /** @type {HTMLElement} */ let item; try{ if(item = document.querySelector(selector)){ callback(item, false); return; }; }catch(exception){ callback(null, false); return; }; /** @type {number} */ const date = Date.now(); /** @type {number} */ let interval = setInterval(() => { if(item = document.querySelector(selector)){ clearInterval(interval); callback(item, true); }else if(Date.now() - date >= timeout){ clearInterval(interval); callback(null, true); }; }, 1000 / frames_per_second); }; return Kanvas; })();