Kanvas/Public/ecma/Kanvas.ecma.js

572 lines
15 KiB
JavaScript

"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.<KanvasObjectBase>} [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.<string, any>} */
cache = {
/** @type {Object.<string, number>} */
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.<KanvasObjectBase>} */
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.<string, kanvas_shape_callback>} */
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.<KanvasObjectBase>} 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.<string, kanvas_dsl_callback>} */
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.<kanvas_event_callback>} */
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;
})();