572 lines
15 KiB
JavaScript
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;
|
|
})(); |