Kanvas/Public/ecma/version/20250107/Kanvas.ecma.js

2535 lines
84 KiB
JavaScript
Raw Normal View History

"use strict";
/**
* @callback kanvas_default_callback
* @returns {void}
*/
/**
* @callback kanvas_thread_callback
* @param {Kanvas.ThreadModel} thread
* @returns {void}
*/
/**
* @callback kanvas_preload_callback
* @param {!HTMLElement} selector
* @param {!boolean} asynchronous
* @returns {void}
*/
/**
* @callback kanvas_event_callback
* @param {!number} i
* @param {...any} [parameters]
* @returns {any|null|void}
*/
/**
* @callback kanvas_event_mouse_callback
* @param {!number} i
* @param {!number} x
* @param {!number} y
* @param {...any} [parameters]
* @returns {any|null|void}
*/
/**
* @callback kanvas_on_change_callback
* @param {Kanvas.ObjectBase|null} child
* @returns {void}
*/
/**
* @callback kanvas_load_image_callback
* @param {!Image} image
* @param {!boolean} ok
* @returns {void}
*/
/**
* @callback kanvas_execute_callback
* @param {...(any|null)} [parameters]
* @returns {any|null}
*/
/**
* @callback kanvas_source_callback
* @param {string} source
*/
/**
* @callback kanvas_array_item_callback
* @param {any|null} item
* @param {!kanvas.kanvas_default_callback} callback
* @returns {void}
*/
/**
* @class
* @constructor
* @param {Object.<string, any|null>|Array.<Object.<string, any|null>>} inputs
* @returns {void}
* @access public
*/
export const Kanvas = (function(){
/**
* @constructs Kanvas
* @param {Object.<string, any|null>|Array.<Object.<string, any|null>>} custom
* @returns {void}
* @access public
*/
const Kanvas = function(custom){
/** @type {Kanvas} */
const self = this,
/** @type {Object.<string, any|null>} */
default_settings = {},
/** @type {Array.<number>} */
threads = [],
/** @type {Array.<number>} */
thread_times = [],
/** @type {Kanvas.Screen} */
screen = new Kanvas.Screen(),
/** @type {Object.<string, Image>} */
images_cache = {};
/** @type {number} */
let last_time = 0,
/** @type {number} */
thread_times_summatory = 0,
/** @type {number|null} */
draw_thread_i = null,
/** @type {boolean} */
screen_done = false,
/** @type {number} */
last_thread_iteration = 0;
/** @type {number} */
this.frames_per_second = 24;
/** @type {number} */
this.real_frames_per_second = 0;
/** @type {number} */
this.thread_times_samples = 3;
/** @type {number} */
this.delta = 0;
/** @type {number} */
this.cells = 40;
/** @type {number} */
this.quality = 1;
/** @type {HTMLCanvasElement|null} */
this.canvas = null;
/** @type {CanvasRenderingContext2D|null} */
this.context = null;
/** @type {number} */
this.preload_timeout = 2000;
/** @type {string} */
this.default_font_family = "monospace";
/** @type {number} */
this.x = 0;
/** @type {number} */
this.y = 0;
/** @type {number} */
this.height = 0;
/** @type {number} */
this.width = 0;
/** @type {number} */
this.cells_x = 0;
/** @type {number} */
this.cells_y = 0;
/** @type {number} */
this.cell_size = 0;
/** @type {number} */
this.start_x = .5;
/** @type {number} */
this.start_y = .5;
/** @type {HTMLElement|null} */
this.cache = null;
/** @type {HTMLElement|null} */
this.item_self = null;
/** @type {number} */
this.threads_mode = Kanvas.THREADS_MODE_ANIMATION;
/** @type {number} */
this.difference = 0;
/** @type {string} */
this.context = "2d";
/** @type {Array.<any|null>} */
this.map = [];
/** @type {boolean} */
this.shadow_cached = false;
/** @type {Kanvas.Event} */
this.on_ready = new Kanvas.Event(true);
/** @type {Kanvas.Event} */
this.on_change_screen = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_down = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_mouse_up = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_click = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_mouse_over = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_mouse_out = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_mouse_move = new Kanvas.EventMouse();
/** @type {Kanvas.Event} */
this.on_key_down = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_key_up = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_key_press = new Kanvas.Event();
/**
* @returns {void}
* @access private
*/
const constructor = () => {
/** @type {kanvas_event_callback|null} */
const on_ready_event = self.settings("on_ready");
self.preload_timeout = self.settings(["preload_timeout", "timeout"], null, self.preload_timeout);
self.quality = self.settings(["quality", "q"], null, self.quality);
self.cells = self.settings("cells", null, self.cells);
self.thread_times_samples = self.settings("thread_times_samples", null, self.thread_times_samples);
self.frames_per_second = self.settings(["frames_per_second", "fps"], null, self.frames_per_second);
self.default_font_family = self.settings("default_font_family", null, self.default_font_family);
self.start_x = self.settings("start_x", null, self.start_x);
self.start_y = self.settings("start_y", null, self.start_y);
self.threads_mode = self.settings("threads_mode", null, self.threads_mode);
self.context = self.settings("context", null, self.context);
threads_run();
self.thread_add(check_screen);
Kanvas.is_function(on_ready_event) && self.on_ready.add(on_ready_event);
self.preload(self.settings("position"), (position, asynchronous) => {
if(position){
if(position.tagName.toLowerCase() != "canvas")
self.canvas = (
self.item_self = position.appendChild(document.createElement("span"))
).appendChild(document.createElement("canvas"));
else
self.item_self = (self.canvas = position).parentNode;
self.item_self.classList.contains("kanvas") || self.item_self.classList.add("kanvas");
(self.cache = self.item_self.querySelector(".cache")) || (
self.cache = self.item_self.appendChild(document.createElement("span"))
).classList.add("cache");
start();
};
});
};
/**
* @param {!Event} event
* @param {!string} name
* @returns {void}
* @access private
*/
const check_event = (event, name) => {
/** @type {Kanvas.Event} */
const event_manager = self["on_" + name];
if(event_manager){
if(event instanceof MouseEvent || event instanceof PointerEvent)
event_manager.execute(
(event.clientX - self.width * self.start_x) / self.cell_size,
(event.clientY - self.height * self.start_y) / self.cell_size
);
else if(event instanceof KeyboardEvent){
event.key == "Tab" && event.preventDefault();
event_manager.execute(event.key);
};
};
};
/**
* @returns {void}
* @access private
*/
const start = () => {
self.thread_add({
bucle : false,
start_now : false,
callback : () => {
self.context = self.canvas.getContext(self.context, {
// willReadFrequently : true
});
self.canvas.setAttribute("tabindex", 0);
self.canvas.focus();
// [
// "mouse_down", "mouse_up", "click", "mouse_over", "mouse_out", "mouse_move",
// "key_down", "key_up", "key_press"
// ].forEach(event_name => {
// self["on_" + event_name] && self.canvas.addEventListener(event_name.replace(/[^a-z]+/g, ""), event => {
// check_event(event, event_name);
// if(event_name == "click")
// this.map.forEach(item => item && item.status && item.check_click(event));
// else if(!event_name.includes("key")){
// this.map.forEach(item => item && item.status && item["on_" + event_name].execute(item, event))
// });
// });
self.canvas.addEventListener("mousemove", event => {
self.on_mouse_move.execute(event);
self.map.forEach(item => item && item.status && item.check_mouse(event));
});
self.canvas.addEventListener("click", event => {
self.on_click.execute(event);
self.map.forEach(item => item && item.status && item.check_click(event));
});
draw_thread_i = self.thread_add(thread => {
/** @type {number} */
const x = self._(self.start_x * self.cells_x),
/** @type {number} */
y = self._(self.start_y * self.cells_y);
self.x = self._(self.start_x * self.cells_x);
self.y = self._(self.start_y * self.cells_y);
self.context.clearRect(0, 0, self.width, self.height >> 0);
// self.context.translate(x, y);
self.draw(self.context);
// self.context.translate(-x, -y);
});
const wait_for_screen = self.thread_add({
callback : () => {
if(screen_done){
self.thread_remove(wait_for_screen);
self.on_ready.autoexecute = true;
self.on_ready.execute();
};
},
bucle : true,
start_now : true
});
}
});
};
/**
* @param {!(string|Array.<string>)} keys
* @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null]
* @param {any|null} [_default = null]
* @returns {any|null}
* @access public
*/
this.settings = (keys, inputs = null, _default = null) => Kanvas.get_value(keys, [inputs, custom, default_settings], _default);
/**
* @returns {void}
* @access private
*/
const check_screen = () => {
if(!self.canvas)
return;
screen_done || (screen_done = true);
if(
screen.x != self.canvas.offsetWidth ||
screen.y != self.canvas.offsetHeight ||
screen.quality != self.quality
){
screen.x = self.canvas.offsetWidth;
screen.y = self.canvas.offsetHeight;
screen.quality = self.quality;
self.canvas.setAttribute("width", self.width = screen.x * self.quality);
self.canvas.setAttribute("height", self.height = screen.y * self.quality);
self.cell_size = self.quality * (screen.x < screen.y ? screen.x : screen.y) / self.cells;
self.cells_x = self.width / self.cell_size;
self.cells_y = self.height / self.cell_size;
self.on_change_screen.execute();
};
};
const rerun_threads = () => {
const timeout = setTimeout(() => {
clearTimeout(timeout);
threads_run();
}, 1);
};
/**
* @returns {void}
* @access private
*/
const threads_run = () => {
switch(self.threads_mode){
case Kanvas.THREADS_MODE_INTERVAL:
/** @type {number} */
const current_frames_per_second = self.frames_per_second,
/** @type {number} */
interval = setInterval(() => {
thread_method();
if(
!(self.threads_mode & Kanvas.THREADS_MODE_INTERVAL) ||
current_frames_per_second != self.frames_per_second
){
clearInterval(interval);
threads_run();
};
}, self.frames_per_second ? 1000 / self.frames_per_second : 0);
break;
case Kanvas.THREADS_MODE_TIMEOUT:
setTimeout(() => {
thread_method();
threads_run();
}, self.frames_per_second ? 1000 / self.frames_per_second : 0);
break;
case Kanvas.THREADS_MODE_ANIMATION:
default:
requestAnimationFrame(() => {
/** @type {number} */
const time = Kanvas.get_time();
if(!self.frames_per_second || time - last_thread_iteration >= 1000 / self.frames_per_second){
last_thread_iteration = time;
thread_method();
};
threads_run();
});
break;
};
};
/**
* @returns {void}
* @access private
*/
const thread_method = () => {
/** @type {number} */
const time = performance.now();
if(last_time){
/** @type {number} */
const difference = last_time ? time - last_time : 0;
self.difference = difference;
self.delta = difference / (1 / self.real_frames_per_second) / 10000;
thread_times.push(difference);
thread_times_summatory += difference;
if(thread_times.length){
while(thread_times.length > self.thread_times_samples)
thread_times_summatory -= thread_times.shift();
/** @type {number} */
const average = thread_times_summatory / thread_times.length;
average && (self.real_frames_per_second = 1000 / average);
};
};
last_time = time;
threads.forEach(thread => {
thread && thread.execute();
});
};
/**
* @param {!(kanvas_thread_callback|Object.<string, number|boolean|kanvas_thread_callback>)} inputs
* @returns {number|null}
* @access public
*/
this.thread_add = inputs => {
/** @type {number} */
const thread = new Kanvas.ThreadModel(this, inputs);
/** @type {number|null} */
let i = null;
if(thread.ok()){
/** @type {number} */
const l = threads.length;
for(i = 0; i < l; i ++)
if(!threads[i])
break;
(threads[i] = thread).i = i;
};
return i;
};
/**
* @param {!(number)} i
* @returns {void}
* @access public
*/
this.thread_remove = i => {
threads[i] && (threads[i] = null);
};
/**
* @param {!number} size
* @returns {number}
* @access public
*/
this._ = size => size * self.cell_size;
/**
* @param {!CanvasRenderingContext2D} context
* @param {Array.<Kanvas.ObjectBase|Array.<any|null>|Object.<string, any|null>|null} [level = null]
* @param {kanvas_on_change_callback|null} [on_change = null]
* @param {?Kanvas.ObjectBase} [parent = null]
* @returns {void}
* @access public
*/
this.draw = (context, level = null, on_change = null, parent = null) => {
(level || (level = self.map)).forEach((item, i) => {
if(item){
if(item.draw_level){
item.draw_level(context);
// on_change && item.on_load.add(() => {
// console.log("PASA CHANGE")
// on_change(item);
// });
}else if(typeof item == "object"){
/** @type {string|Class} */
const type = Kanvas.is_array(item) ? item[0] : item.type;
if(level[i] = (
Kanvas.is_string(type) && Kanvas.SHAPES[type] ? new Kanvas.SHAPES[type](self, parent, item, i) :
Kanvas.is_class(type) ? new type(self, parent, item, i) :
null)){
level[i].draw_level(context);
if(Kanvas.is_function(on_change)){
level[i].on_status_change.add(on_change);
on_change(level[i]);
};
}else
Kanvas.is_function(on_change) && on_change(null);
};
};
});
};
/**
* @param {!(string|HTMLElement)} selector
* @param {!kanvas_preload_callback} callback
* @returns {void}
* @access public
*/
this.preload = (selector, callback) => {
if(!Kanvas.is_function(callback))
return;
if(Kanvas.is_string(selector)){
/** @type {HTMLElement|null} */
let item;
try{
if(item = document.querySelector(selector)){
callback(item, false);
return;
};
}catch(exception){
callback(null, false);
return;
};
/** @type {number} */
const time = Kanvas.get_time(),
/** @type {number} */
preload = self.thread_add(thread => {
if(item = document.querySelector(selector)){
self.thread_remove(preload);
callback(item, true);
}else if(Kanvas.get_time() - time > self.preload_timeout){
self.thread_remove(preload);
callback(null, true);
};
});
}else if(Kanvas.is_html_object(selector))
callback(selector, false);
else
callback(null, false);
};
/**
* @param {!(string|Image)} source
* @param {!kanvas_load_image_callback} callback
* @returns {void}
* @access public
*/
this.preload_image = (source, callback) => {
if(Kanvas.is_image(source))
callback(source, ok);
else if(images_cache[source])
callback(...images_cache[source]);
else
Kanvas.load_image(source, (image, ok) => {
callback(...(images_cache[source] = [image, ok]));
});
};
constructor();
};
/**
* @returns {number}
* @access public
* @static
*/
Kanvas.get_time = () => Date.now();
// Kanvas.get_time = () => performance.now();
/**
* @constructor
* @param {!(kanvas_thread_callback|Object.<string, number|boolean|kanvas_thread_callback>)} inputs
* @returns {void}
* @access public
*/
Kanvas.ThreadModel = function(kanvas, inputs){
Kanvas.is_function(inputs) && (inputs = {callback : inputs});
/** @type {Kanvas.ThreadModel} */
const self = this,
/** @type {kanvas_thread_callback} */
callback = Kanvas.get_value(["threads_callback", "callback"], inputs),
/** @type {boolean} */
is_function = Kanvas.is_function(callback);
/** @type {number} */
let iterations = 0;
/** @type {boolean} */
this.bucle = Kanvas.get_value(["threads_bucle", "bucle"], inputs, true);
/** @type {number} */
this.timer = Kanvas.get_value(["threads_timer", "timer"], inputs, 16.6667);
/** @type {number} */
this.last_time = Kanvas.get_value(["threads_start_now", "start_now"], inputs, true) ? 0 : Kanvas.get_time();
/** @type {number|null} */
this.i = null;
/** @type {boolean} */
this.play = Kanvas.get_value(["threads_autostart", "autostart"], inputs, true);
/**
* @returns {void}
* @access public
*/
this.execute = () => {
if(is_function && self.play){
/** @type {number} */
const time = Kanvas.get_time();
if(iterations && !self.bucle)
kanvas.thread_remove(self.i);
else if(time - self.last_time >= self.timer){
self.last_time = time;
callback(self);
iterations ++;
};
};
};
/**
* @returns {void}
* @access public
*/
this.retry = () => {
iterations = 0;
};
/**
*
* @returns {boolean}
* @access public
*/
this.ok = () => is_function;
};
/** @type {number} */
Kanvas.UNLOADED = 1 << 0;
/** @type {number} */
Kanvas.LOADING = 1 << 1;
/** @type {number} */
Kanvas.LOADED = 1 << 2;
/** @type {number} */
Kanvas.ERROR = 1 << 3;
/** @type {number} */
Kanvas.UNBUILT = 1 < 4;
/** @type {number} */
Kanvas.THREADS_MODE_ANIMATION = 1 << 0;
/** @type {number} */
Kanvas.THREADS_MODE_INTERVAL = 1 << 1;
/** @type {number} */
Kanvas.THREADS_MODE_TIMEOUT = 1 << 2;
/**
* @class
* @constructor
* @param {!number} x
* @param {!number} y
* @param {!number} blur
* @param {!string} color
* @returns {void}
* @access public
* @static
*/
Kanvas.Shadow = function(x, y, blur, color){
/** @type {number} */
this.x = x;
/** @type {number} */
this.y = y;
/** @type {number} */
this.blur = blur;
/** @type {number} */
this.color = color;
};
/**
* @class
* @access public
* @static
*/
Kanvas.ObjectBase = class{
/**
* @param {!(string|Array.<string>)} keys
* @param {anmy|null} [_default = null]
* @returns {any|null}
* @access protected
*/
_get(keys, _default = null){
return Kanvas.get_value(keys, this.inputs, _default);
};
/**
* @param {!Kanvas} kanvas
* @param {?Kanvas.ObjectBase} parent
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @param {!(Object.<string, any|null>|Array)} [custom = {}]
* @param {!Array.<string>} [inputs_map = []]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1, custom = {}, inputs_map = [
"_", "x", "y", "width", "height", "background", "childs"
]){
/** @type {Object.<string, any|null>} */
this.inputs = Kanvas.get_dictionary([(
Kanvas.is_array(inputs) ? inputs = Kanvas.map_dictionary(inputs, inputs_map) :
Kanvas.is_dictionary(inputs) ? inputs :
{}), custom], false);
// /** @type {kanvas_on_change_callback|null} */
// const on_load_event = this._get("on_load", null),
// /** @type {kanvas_on_change_callback|null} */
// on_error_event = this._get("on_error", null),
// /** @type {kanvas_on_change_callback|null} */
// on_status_change_event = this._get("on_status_change", null);
/** @type {Kanvas} */
this.kanvas = kanvas;
/** @type {Object} */
this.type = Kanvas.ObjectBase;
/** @type {string} */
this.type_name = "ObjectBase";
/** @type {boolean} */
this.full = this._get("full", false);
/** @type {boolean} */
this.percentaje = this._get("percentaje", false);
/** @type {number|null} */
this.rotation = this._get(["rotate", "rotation"], null);
/** @type {string|null} */
this.background = this._get(["background", "color"], null);
/** @type {number|null} */
this.alpha = this._get("alpha", null);
/** @type {number} */
this.status = Kanvas.UNBUILT;
// /** @type {Array.<Array.<any>|Object.<string, any|null>|Object>|null} */
// this.childs = this._get("childs", null);
/** @type {number} */
this.x = this._get("x", 0);
/** @type {number} */
this.y = this._get("y", 0);
/** @type {number} */
this.width = this._get("width", 0);
/** @type {number} */
this.height = this._get("height", 0);
/** @type {boolean} */
this.print = this._get("print", inputs, false);
/** @type {Array.<Kanvas.ObjectBase|Array.<any|null>|Object.<string, any|null>} */
this.childs = this._get("childs", []);
/** @type {string} */
this.border_color = this._get("border_color", null);
/** @type {number} */
this.border_size = this._get(["border_size", "border_weight"], null);
/** @type {boolean} */
this.draw_childs = true;
/** @type {number} */
this.i = i;
/** @type {Array.<Kanvas.Shadow>} */
this.shadows = Kanvas.get_array(this._get(["shadow", "shadows"], [])).map(shadow => new Kanvas.Shadow(...shadow));
/** @type {HTMLCanvasElement|null} */
this.shadow_shape_cache = null;
/** @type {boolean} */
this.shadow_cached = false;
/** @type {boolean} */
this.is_mouse_over = false;
/** @type {Path2D|null} */
this.path = null;
/** @type {boolean} */
this.recreate_path_automatically = true;
/** @type {Kanvas.ObjectBase|null} */
this.parent = parent;
/** @type {DOMMatrix|null} */
this.matrix = null;
/** @type {Kanvas.Event} */
this.on_load = new Kanvas.Event(true);
/** @type {Kanvas.Event} */
this.on_error = new Kanvas.Event(true);
/** @type {Kanvas.Event} */
this.on_status_change = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_over = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_out = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_move = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_click = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_down = new Kanvas.Event();
/** @type {Kanvas.Event} */
this.on_mouse_up = new Kanvas.Event();
/** @type {number} */
this.childs_with_errors = 0;
/** @type {number} */
this.childs_loaded = 0;
/** @type {number} */
this.childs_loading = 0;
/** @type {number} */
this.childs_null = 0;
/** @type {number} */
this.childs_unbuilt = 0;
/** @type {boolean} */
this.full_loaded = !this.childs.length;
// on_load_event && this.on_load.add(on_load_event);
// on_error_event && this.on_error.add(on_error_event);
// on_status_change_event && this.on_status_change.add(on_status_change_event);
[
"load", "error", "status_change", "mouse_down", "mouse_up", "click",
"mouse_over", "mouse_out", "mouse_move"
].forEach(key => {
/** @type {kanvas_event_callback} */
const event = this._get("on_" + key);
Kanvas.is_function(event) && this["on_" + key].add(event);
});
};
/**
* @param {!EventMouse} event
* @returns {void}
* @access public
*/
check_mouse(event){
if(this.path){
/** @type {boolean} */
const is_over = this.kanvas.context.isPointInPath(this.path, event.clientX, event.clientY);
if(this.is_mouse_over){
if(!is_over){
this.is_mouse_over = false;
this.on_mouse_out.execute(this, event);
};
}else{
if(is_over){
this.is_mouse_over = true;
this.on_mouse_over.execute(this, event);
};
};
if(is_over)
this.on_mouse_move.execute(this, event);
else
this.draw_childs && this.childs.forEach(child => child && child.status && child.check_mouse(event));
};
};
/**
* @param {!EventMouse} event
* @returns {void}
* @access public
*/
check_click(event){
if(this.path && this.kanvas.context.isPointInPath(this.path, event.clientX, event.clientY))
this.on_click.execute(this, event);
else
this.childs.forEach(child => child.check_click(event));
};
/**
* @param {number|null} [status = null]
* @returns {void}
* @access public
*/
set_status(status = null){
status !== null && (this.status = status);
this.on_status_change.execute(this);
switch(this.status){
case Kanvas.ERROR:
this.on_error.autoexecute = true;
this.on_error.execute(this);
break;
case Kanvas.LOADED:
if(this.full_loaded){
this.on_load.autoexecute = true;
this.on_load.execute(this);
};
break;
default:
this.on_load.autoexecute = false;
this.on_error.autoexecute = false;
break;
};
};
/**
* @param {!Kanvas.ObjectBase} child
* @returns {void}
* @access public
*/
#set_child_status(child){
this.childs_loaded = 0;
this.childs_with_errors = 0;
this.childs_loading = 0;
this.childs_null = 0;
this.childs_unbuilt = 0;
this.childs.forEach(child => {
if(child === null)
this.childs_null ++;
else if(child.status !== undefined)
this.childs_unbuilt ++;
else if(child.status & Kanvas.ERROR)
this.childs_with_errors ++;
else if(child.status & Kanvas.LOADED)
this.childs_loaded ++;
else
this.childs_loading ++;
});
(this.full_loaded = this.childs.length == this.childs_null + this.childs_loaded + this.childs_with_errors) &&
self.set_status();
};
/**
* @param {number} degrees
* @returns {void}
* @access public
*/
rotate(degrees){
this.rotation = (this.rotation + degrees) % 360;
};
/**
* @param {!CanvasRenderingContext2D} context
* @param {!Kanvas.Shadow} shadow
* @returns {void}
* @access public
*/
set_shadow(context, shadow){
context.shadowOffsetX = this.kanvas._(shadow.x);
context.shadowOffsetY = this.kanvas._(shadow.y);
context.shadowBlur = this.kanvas._(shadow.blur);
context.shadowColor = shadow.color;
};
static transform_array(matrix){
return [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
};
/**
* @param {!CanvasRenderingContext2D} context
* @param {!number} x
* @param {!number} y
* @returns {void}
* @access public
*/
predraw(context, x, y){
// if(!this.shadow_cached && this.shadows.length > 1){
// if(this.shadow_shape_cache === null){
// this.shadow_shape_cache = this.kanvas.cache.appendChild(document.createElement("canvas"));
// ["width", "height"].forEach(key => {
// this.shadow_shape_cache.setAttribute(key, this.kanvas[key]);
// });
// };
// const shape_context = this.shadow_shape_cache.getContext("2d"),
// shadows_saved = this.shadows,
// /** @type {DOMMatrix} */
// matrix = context.getTransform();
// shape_context.beginPath();
// shape_context.clearRect(0, 0, this.kanvas.width, this.kanvas.height);
// shape_context.translate(matrix.e, matrix.f);
// this.shadows = [];
// this.kanvas.shadow_cached = true;
// this.kanvas.draw(shape_context, [this]);
// shape_context.translate(-matrix.e, -matrix.f);
// shape_context.closePath();
// context.save();
// context.translate(-matrix.e, -matrix.f);
// shadows_saved.forEach(shadow => {
// context.beginPath();
// this.set_shadow(context, shadow);
// context.drawImage(this.shadow_shape_cache, 0, 0);
// context.closePath();
// });
// context.translate(matrix.e, matrix.f);
// context.restore();
// this.kanvas.shadow_cached = false;
// this.shadows = shadows_saved;
// };
context.save();
context.beginPath();
this.matrix = new DOMMatrix(this.parent ? Kanvas.ObjectBase.transform_array(this.parent.matrix) : [1, 0, 0, 1, this.kanvas.x, this.kanvas.y]);
this.matrix.translateSelf(x, y);
if(this.rotation){
/** @type {number} */
const x = this.width ? this.kanvas._(this.width / 2) : 0,
/** @type {number} */
y = this.height ? this.kanvas._(this.height / 2) : 0;
this.matrix.translateSelf(x, y);
this.matrix.rotateSelf(this.rotation);
this.matrix.translateSelf(-x, -y);
};
if(this.shadows.length < 2){
if(!this.kanvas.shadow_cached && this.shadow_shape_cache){
this.shadow_shape_cache.remove();
this.shadow_shape_cache = null;
};
if(this.shadows.length == 1)
this.set_shadow(context, this.shadows[0]);
};
this.background !== null && (context.fillStyle = this.background);
if(!this.kanvas.shadow_cached){
this.alpha !== null && (context.globalAlpha = this.alpha);
this.border_size && (context.lineWidth = this.kanvas._(this.border_size));
this.border_color && (context.strokeStyle = this.border_color);
};
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw(context, x, y){};
/**
* @param {!CanvasRenderingContext2D} context
* @param {!Array.<Path2D>} [shape = []]
* @returns {void}
* @access public
*/
fill(context, shape = []){
(this.kanvas.shadow_cached || this.background) && context.fill(...shape);
this.border_size && context.stroke(...shape);
};
/**
* @param {!CanvasRenderingContext2D} context
* @param {!number} x
* @param {!number} y
* @returns {void}
* @access public
*/
postdraw(context, x, y){
if(this.path){
/** @type {Array.<Path2D>} */
const shape = (this.path ? [this.path] : []).map(shape => {
const real = new Path2D();
real.addPath(shape, this.matrix);
return real;
});
this.path && (this.path = shape[0]);
this.fill(context, shape);
};
// this.matrix = new DOMMatrix(context.getTransform());
context.closePath();
// this.type_name == "Position" && console.log(JSON.stringify([this.draw_childs, this.childs.length]));
if(this.draw_childs && this.childs.length){
this.path || (this.path = new Path2D());
this.kanvas.draw(context, this.childs, child => {
child && child.path && this.path.addPath(child.path);
this.#set_child_status(child);
}, this);
};
if(this.shadows.length){
// image.src = this.kanvas.canvas.toDataURL();
// context.save();
// context.clip(this.path);
// const data = context.getImageData(0, 0, this.kanvas.width, this.kanvas.height);
// context.restore();
this.shadows.forEach(shadow => {
const subcanvas = document.createElement("canvas"),
subcontext = subcanvas.getContext("2d");
subcanvas.setAttribute("width", this.kanvas.width);
subcanvas.setAttribute("height", this.kanvas.height);
subcontext.beginPath();
subcontext.styleFill = "#000";
this.set_shadow(subcontext, shadow);
subcontext.fill(this.path);
subcontext.globalCompositeOperation = "destination-out";
subcontext.beginPath();
subcontext.fillStyle = "black";
subcontext.fill(this.path);
subcontext.globalCompositeOperation = "source-over";
subcontext.clip(this.path);
subcontext.clearRect(0, 0, this.kanvas.width, this.kanvas.height);
subcontext.closePath();
context.drawImage(subcanvas, 0, 0);
subcanvas.remove();
// context.save();
// this.set_shadow(context, shadow);
// context.beginPath();
// context.fillStyle = "black";
// context.fill(this.path);
// context.restore();
// context.globalCompositeOperation = "source-atop";
// context.beginPath();
// context.fillStyle = "black";
// context.fill(this.path);
// context.closePath();
// context.fillStyle = "transparent";
// context.fill(this.path);
// context.globalCompositeOperation = "source-over";
// context.clip(this.path);
// context.clearRect(0, 0, this.kanvas.width, this.kanvas.height);
// context.transform(1, 0, 0, 1, 0, 0);
// context.closePath();
// context.save();
// this.set_shadow(context, shadow);
// context.fillStyle = "#000";
// context.fill(this.path);
// context.globalCompositeOperation = "destination-out";
// context.fill(this.path);
// context.globalCompositeOperation = "source-over";
// context.restore();
// context.save();
// this.set_shadow(context, shadow);
// context.fillStyle = "#000";
// context.fill(this.path);
// context.restore();
// context.save();
// context.clip(this.path);
// context.clearRect(0, 0, this.kanvas.width, this.kanvas.height);
// context.restore();
});
// context.save();
// context.clip(this.path);
// context.drawImage(image, 0, 0);
// context.restore();
// context.putImageData(data, 0, 0);
};
// context.translate(-x, -y);
context.restore();
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw_level(context){
if(this.status == Kanvas.LOADED){
/** @type {number} */
const x = this.kanvas._(this.x),
/** @type {number} */
y = this.kanvas._(this.y);
this.recreate_path_automatically && (this.path = new Path2D());
this.predraw(context, x, y);
this.draw(context, x, y);
this.postdraw(context, x, y);
};
};
};
/** @type {Object.<string, Kanvas.ObjectBase>} */
Kanvas.SHAPES = {
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Rectangle : class extends Kanvas.ObjectBase{
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i);
this.type = Kanvas.SHAPES.Rectangle;
this.type_name = "Rectangle";
/** @type {Array.<number>} */
this.radius = Kanvas.get_array(this._get("radius", []));
/** @type {boolean} */
this.use_modern_mode = this._get("use_modern_mode", true);
this.set_status(Kanvas.LOADED);
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw(context, x, y){
if(this.radius.every(radius => !radius))
this.path.rect(0, 0, this.kanvas._(this.width), this.kanvas._(this.height));
else{
if(this.use_modern_mode)
this.path.roundRect(0, 0, this.kanvas._(this.width), this.kanvas._(this.height), this.radius.map(radius => this.kanvas._(radius)));
else{
switch(this.radius.length){
case 1:
this.radius.push(...[0, 0, 0].map(_ => this.radius[0]));
break;
case 2:
this.radius.push(...this.radius);
break;
case 3:
this.radius.push(this.radius[1]);
break;
};
/** @type {number} */
const radius_top_left = this.kanvas._(this.radius[0]),
/** @type {number} */
radius_top_right = this.kanvas._(this.radius[1]),
/** @type {number} */
radius_bottom_right = this.kanvas._(this.radius[2]),
/** @type {number} */
radius_bottom_left = this.kanvas._(this.radius[3]),
/** @type {number} */
width = this.kanvas._(this.width),
/** @type {number} */
height = this.kanvas._(this.height);
this.path.moveTo(radius_top_left, 0);
this.path.lineTo(width - radius_top_right, 0);
this.path.quadraticCurveTo(width, 0, width, radius_top_right);
this.path.lineTo(width, height - radius_bottom_right);
this.path.quadraticCurveTo(width, height, width - radius_bottom_right, height);
this.path.lineTo(radius_bottom_left, height);
this.path.quadraticCurveTo(0, height, 0, height - radius_bottom_left);
this.path.lineTo(0, radius_top_left);
this.path.quadraticCurveTo(0, 0, radius_top_left, 0);
};
};
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Ellipse : class extends Kanvas.ObjectBase{
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, ["_", "x", "y", "radius", "from", "to"]);
this.type = Kanvas.SHAPES.Ellipse;
this.type_name = "Ellipse";
/** @type {number|Array.<number, number>} */
this.radius = this._get("radius");
/** @type {number} */
this.from = this._get(["from", "start"], 0);
/** @type {number} */
this.to = this._get(["to", "end"], 360);
this.set_status(Kanvas.LOADED);
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw(context, x, y){
if(Kanvas.is_array(this.radius))
this.path.ellipse(0, 0, this.kanvas._(radius[0]), this.kanvas._(radius[1]), 0, Kanvas.to_radians(this.from), Kanvas.to_radians(this.to));
else
this.path.arc(0, 0, this.kanvas._(this.radius), Kanvas.to_radians(this.from), Kanvas.to_radians(this.to));
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Text : class extends Kanvas.ObjectBase{
static x = {
left : 0,
right : 1,
center : .5,
start : 0,
end : 1
};
static y = {
top : 0,
hanging : .1,
middle : .5,
alphabetic : .8,
ideographic : .9,
bottom : 1
};
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, [
"_", "x", "y", "text", "size", "font", "color", "align", "baseline", "childs"
]);
this.type = Kanvas.SHAPES.Text;
this.type_name = "Text";
/** @type {string} */
this.text = this._get("text");
/** @type {number} */
this.size = this._get("size", 1);
/** @type {string} */
this.font = this._get(["font", "family", "font_family"], this.kanvas.default_font_family);
/** @type {string|number|null} */
this.weight = this._get("weight", null);
/** @type {string|null} */
this.style = this._get("style", null);
/** @type {boolean} */
this.bold = this._get("bold", false);
/** @type {boolean} */
this.italic = this._get("italic", false);
/** @type {boolean} */
this.oblique = this._get("oblique", false);
/** @type {string|number} */
this.baseline = this._get(["baseline", "base_line", "vertical_align"], null);
/** @type {string} */
this.align = this._get("align", null);
this.set_status(Kanvas.LOADED);
};
/**
* @param {!CanvasRenderingContext2D} context
* @param {!number} x
* @param {!number} y
* @returns {void}
* @access public
*/
predraw(context, x, y){
super.predraw(context, x, y);
context.font = (
(this.weight ? this.weight + " " : this.bold ? "bold " : "") +
(this.style ? this.style + " " : this.italic ? "italic " : this.oblique ? "oblique " : "") +
this.kanvas._(this.size) + "px " +
this.font
);
this.align && (context.textAlign = this.align);
this.baseline && (context.textBaseline = this.baseline);
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw(context, x, y){
/** @type {number} */
const width = context.measureText(this.text).width,
/** @type {number} */
height = this.kanvas._(this.size),
/** @type {number} */
_x = -Kanvas.SHAPES.Text.x[this.align || "start"] * width,
/** @type {number} */
_y = -Kanvas.SHAPES.Text.y[this.baseline || "alphabetic"] * height;
context.setTransform(...Kanvas.ObjectBase.transform_array(this.matrix));
context[this.background ? "fillText" : "strokeText"](this.text, 0, 0);
this.fill(context);
context.setTransform(1, 0, 0, 1, 0, 0);
this.path.moveTo(_x, _y);
this.path.lineTo(width + _x, _y);
this.path.lineTo(width + _x, height + _y);
this.path.lineTo(_x, height + _y);
};
postdraw(context, x, y){
this.background !== null && (context.fillStyle = "transparent");
if(!this.kanvas.shadow_cached){
this.border_size && (context.lineWidth = 0);
this.border_color && (context.strokeStyle = "transparent");
};
super.postdraw(context, x, y);
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Image : class extends Kanvas.ObjectBase{
/**
* @returns {void}
* @access public
*/
set_size(){
this.width === null && (this.width = this.image.width);
this.height === null && (this.height = this.image.height);
};
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, ["_", "source", "x", "y"].concat(...(Kanvas.is_array(inputs) ? [
inputs.length > 4 ? ["width", "height"] : [],
inputs.length > 6 ? ["sub_x", "sub_y", "sub_width", "sub_height"] : []
] : []), ["childs"]));
this.type = Kanvas.SHAPES.Image;
this.type_name = "Image";
/** @type {Image|null} */
this.image = this._get(["image", "object", "image_object", "image_item", "item"], null);
/** @type {string|null} */
this.source = this._get(["source", "src", "link", "url", "path"], null);
/** @type {number} */
this.sub_x = this._get(["sub_x", "subx", "_x"], null);
/** @type {number} */
this.sub_y = this._get(["sub_y", "suby", "_y"], null);
/** @type {number} */
this.sub_width = this._get(["sub_width", "_width"], null);
/** @type {number} */
this.sub_height = this._get(["sub_height", "_height"], null);
this.set_status(Kanvas.LOADING);
this.kanvas.preload_image(this.image || this.source, (image, ok) => {
this.image || (this.image = image);
if(ok){
this.set_size();
this.set_status(Kanvas.LOADED);
}else
this.set_status(Kanvas.ERROR);
});
};
/**
* @param {!CanvasRenderingContext2D} context
* @returns {void}
* @access public
*/
draw(context, x, y){
const width = this.kanvas._(this.width),
height = this.kanvas._(this.height);
this.path.rect(0, 0, width, height);
context.save();
context.setTransform(...Kanvas.ObjectBase.transform_array(this.matrix));
context.drawImage(...[this.image].concat((
this.sub_x !== null &&
this.sub_y !== null &&
this.sub_width !== null &&
this.sub_height !== null
) ? [
this.sub_x, this.sub_y, this.sub_width, this.sub_height
] : [], [0, 0, width, height]));
context.setTransform(1, 0, 0, 1, 0, 0);
context.restore();
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Cache : class extends Kanvas.ObjectBase{
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, [
"_", "x", "y", "width", "height", "childs"
]);
this.type = Kanvas.SHAPES.Cache;
this.type_name = "Cache";
this.draw_childs = false;
this.full_loaded = true;
this.recreate_path_automatically = false;
this.status = Kanvas.LOADED;
/** @type {boolean} */
this.built = false;
/** @type {Image|null} */
this.cache = null;
};
predraw(context, x, y){
super.predraw(context, x, y);
if(!this.built){
this.build();
this.built = true;
};
};
/**
* @returns {void}
* @access public
*/
build(){
this.set_status(Kanvas.LOADING);
/** @type {HTMLCanvasElement} */
const canvas = document.createElement("canvas"),
/** @type {CanvasRenderingContext2D} */
context = canvas.getContext("2d"),
width = this.kanvas._(this.width),
height = this.kanvas._(this.height);
/** @type {number} */
let loading = this.childs.reduce((total, child) => total + (child ? 1 : 0), 0),
/** @type {number} */
done = 0,
/** @type {boolean} */
synchronous = true;
this.path = new Path2D();
const end = () => {
if(++ done != loading)
return;
if(!synchronous){
this.path = new Path2D();
context.clearRect(0, 0, width, height);
console.log(this.childs);
this.kanvas.draw(context, this.childs, child => {
child && this.path.addPath(child.path);
}, {
matrix : new DOMMatrix([1, 0, 0, 1, 0, 0])
});
};
/** @type {string} */
const data = canvas.toDataURL("image/webp", 1);
Kanvas.load_image(data, (image, ok) => {
this.cache = image;
this.set_status(ok ? Kanvas.LOADED : Kanvas.ERROR);
// console.log([this.status, this.full_loaded, this.childs.length, this.childs_null, this.childs_loaded, this.childs_with_errors, this]);
});
canvas.remove();
};
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
this.kanvas.draw(context, this.childs, child => {
console.log(child);
if(child){
this.path.addPath(child.path);
child.on_load.add(end);
}else
end();
}, {
matrix : new DOMMatrix([1, 0, 0, 1, 0, 0])
});
synchronous = false;
};
/**
* @returns {void}
* @access public
*/
draw(context, x, y){
if(this.cache){
this.path.rect(0, 0, this.cache.width, this.cache.height);
context.save();
context.setTransform(...Kanvas.ObjectBase.transform_array(this.matrix));
context.drawImage(this.cache, 0, 0);
context.setTransform(1, 0, 0, 1, 0, 0);
context.restore();
};
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Position : class extends Kanvas.ObjectBase{
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, ["_", "x", "y", "childs"]);
this.type = Kanvas.SHAPES.Position;
this.type_name = "Position";
this.status = Kanvas.LOADED;
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Shape : class extends Kanvas.ObjectBase{
/** @type {Array.<string|null>} */
static methods = [null, null, "line", null, "quadraticCurve", "arc", "bezierCurve"];
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, ["_", "x", "y", "shape"]);
this.type = Kanvas.SHAPES.Shape;
this.type_name = "Shape";
/** @type {Array.<Array.<number>>} */
this.shape = this._get(["shape", "setps", "marks"], []).filter(dot => (
Kanvas.is_array(dot) &&
[2, 4, 5, 6].includes(dot.length) &&
dot.every(value => Kanvas.is_number(value))
));
this.status = Kanvas.LOADED;
};
/**
* @returns {void}
* @access public
*/
draw(context, x, y){
this.path.moveTo(0, 0);
this.shape.length > 2 && this.shape.forEach(dot => {
this.methods[dot.length] && this.path[this.methods[dot.length] + "To"](...dot);
});
};
},
/**
* @class
* @extends Kanvas.ObjectBase
* @access public
* @static
*/
Sprite : class extends Kanvas.ObjectBase{
/**
* @param {!Kanvas} kanvas
* @param {!(Object.<string, any|null>|Array)} inputs
* @param {!number} [i = -1]
* @access public
*/
constructor(kanvas, parent, inputs, i = -1){
super(kanvas, parent, inputs, i, {}, ["_", "x", "y", "sources", "map", "childs"]);
const images = this._get(["images", "image"], {}),
_i = this._get(["i", "index", "position"], 0);
this.type = Kanvas.SHAPES.Sprite;
this.type_name = "Sprite";
/** @type {Array.<string>} */
this.sources = Kanvas.get_array(this._get(["sources", "source", "src", "links", "urls", "paths", "link", "url", "path"], []));
/** @type {Object.<string, Image>} */
this.images = (
Kanvas.is_dictionary(images) ? images :
(Kanvas.is_array(images) ? images : [images]).reduce((dictionary, image) => {
if(Kanvas.is_image(image))
dictionary[image.src] = image;
else if(Kanvas.is_string(image) && !this.sources.includes(image))
this.sources.push(image);
return dictionary;
}, {})
);
/** @type {Object.<string, Array.<Kanvas.SpritesMap>>} */
this.map = Kanvas.SpritesMap.process(this._get("map"), source => {
this.sources.includes(source) || this.sources.push(source)
});
/** @type {string|null} */
this.group = this._get("group", null);
/** @type {number} */
this.i = 0;
/** @type {number} */
this.frames_per_second = this._get(["frames_per_second", "fps"], 12);
/** @type {boolean} */
this.autoplay = this._get("autoplay", true);
/** @type {number} */
this.last_next = 0;
_i === null || this.next(_i);
this.status = Kanvas.LOADING;
Kanvas.execute_array(this.sources, (source, callback) => {
Kanvas.load_image(source, (image, ok) => {
this.images[source] = ok ? image : null;
Kanvas.execute(callback);
});
}, () => {
this.status = Kanvas.LOADED;
});
};
/**
* @param {!string} group
* @param {!number} [i = 0]
* @param {boolean|null} [autoplay = null]
* @returns {void}
* @access public
*/
set(group, i = 0, autoplay = null){
this.last_i_time = performance.now();
this.group = group;
this.autoplay = autoplay === null ? this.autoplay : autoplay;
this.next(i);
};
next(i = null){
this.i = i === null ? (this.i + i) % this.map[this.group].length : i;
this.width = this.map[this.group][this.i].width;
this.height = this.map[this.group][this.i].height;
};
/**
* @returns {void}
* @access public
*/
draw(context, x, y){
if(this.group && this.map[this.group]){
if(this.autoplay && this.map[this.group].length > 1){
const next = performance.now() / (1000 / this.frames_per_second) >> 0;
if(next != this.last_next){
this.next((this.i + next - this.last_next) % this.map[this.group].length);
this.last_next = next;
};
};
/** @type {Kanvas.SpritesMap} */
const sprite = this.map[this.group][this.i],
x = this.kanvas._(sprite.x),
y = this.kanvas._(sprite.y),
width = this.kanvas._(sprite.width),
height = this.kanvas._(sprite.height);
this.path = new Path2D();
this.path.rect(x, y, width, height);
if(this.images[sprite.source]){
context.save();
context.setTransform(...Kanvas.ObjectBase.transform_array(this.matrix));
context.drawImage(
this.images[sprite.source],
sprite.sub_x, sprite.sub_y, sprite.sub_width, sprite.sub_height,
0, 0, width, height
);
context.setTransform(1, 0, 0, 1, 0, 0);
context.restore();
};
};
};
}
};
/**
* @class
* @constructor
* @param {!(string|number|Array.<string|number>|Object.<string, string|number>)} inputs
* @returns {void}
* @access public
* @static
*/
Kanvas.SpritesMap = (function(){
/**
* @constructs Kanvas.SpritesMap
* @param {!(string|number|Array.<string|number>|Object.<string, string|number>)} inputs
* @returns {void}
* @access private
*/
const SpritesMap = function(...inputs){
/** @type {Kanvas.SpritesMap} */
const self = this;
/** @type {string|null} */
this.source = null;
/** @type {number} */
this.sub_x = 0;
/** @type {number} */
this.sub_y = 0;
/** @type {number} */
this.sub_width = 0;
/** @type {number} */
this.sub_height = 0;
/** @type {number} */
this.x = 0;
/** @type {number} */
this.y = 0;
/** @type {number} */
this.width = 0;
/** @type {number} */
this.height = 0;
const constructor = () => {
if(inputs.length){
inputs.length == 1 && (inputs = inputs[0]);
if(Kanvas.is_array(inputs))
[self.source, self.sub_x, self.sub_y, self.sub_width, self.sub_height, self.width, self.height, self.x, self.y] = inputs;
else if(Kanvas.is_dictionary(inputs)){
self.source = Kanvas.get_value(["source", "src", "link", "url", "path"], inputs, self.source);
self.sub_x = Kanvas.get_value(["sub_x", "subx", "left", "horizontal"], inputs, self.sub_x);
self.sub_y = Kanvas.get_value(["sub_y", "suby", "top", "vertical"], inputs, self.sub_y);
self.sub_width = Kanvas.get_value("sub_width", inputs, self.sub_width);
self.sub_height = Kanvas.get_value("sub_height", inputs, self.sub_height);
self.width = Kanvas.get_value("width", inputs, self.width || self.sub_width);
self.height = Kanvas.get_value("height", inputs, self.height || self.sub_height);
self.x = Kanvas.get_value("width", inputs, self.x);
self.y = Kanvas.get_value("height", inputs, self.y);
};
};
};
constructor();
};
/**
* @param {!Object.<string, Array.<Array.<string|number>|Kanvas.SpritesMap>>} map
* @param {!kanvas_source_callback} [source_callback]
* @returns {Object.<string, Array.<Kanvas.SpritesMap>>}
* @access public
* @static
*/
SpritesMap.process = (map, source_callback) => {
/** @type {Object.<string, Array.<Kanvas.SpritesMap>>} */
const processed = {};
for(const key in map)
if(Kanvas.is_array(map[key])){
processed[key] = [];
map[key].forEach(sprite => {
if(Kanvas.is_array(sprite) && sprite[1] == "pattern"){
/** @type {number} */
const l = sprite.length,
/** @type {[string, number, number, number, number, number, number, number, number, number, number, number, number]} */
[source, sub_width, sub_height, sub_x, sub_y, width, height, x, y, margin_x, margin_y, padding_x, padding_y, length] = [
sprite[0],
...sprite.slice(2, 4),
...(l > 5 ? sprite.slice(4, 6) : [0, 0]),
...(l > 7 ? sprite.slice(6, 8) : [0, 0]),
...(l > 9 ? sprite.slice(8, 10) : [0, 0]),
...(l > 11 ? sprite.slice(10, 11) : [0, 0]),
...(l > 13 ? sprite.slice(12, 14) : [0, 0]),
...(l % 2 ? sprite.slice(-1) : [0])
];
/** @type {number} */
let k = 0,
/** @type {boolean} */
ended = false;
Kanvas.execute(source_callback, source);
for(let i = 0; i < sub_x && !ended; i ++)
for(let j = 0; j < sub_y && !ended; j ++){
processed[key].push(new Kanvas.SpritesMap([
source,
padding_x + j * sub_width + j * margin_x,
padding_y + i * sub_height + i * margin_y,
sub_width,
sub_height,
width,
height,
x,
y
]));
length && ++ k == length && (ended = true);
};
}else
processed[key].push(sprite instanceof Kanvas.SpritesMap ? sprite : new Kanvas.SpritesMap(sprite));
});
};
return processed;
};
return SpritesMap;
})();
/**
* @class
* @constructor
* @returns {void}
* @access public
* @static
*/
Kanvas.Event = function(unique = false){
/** @type {Kanvas.Event} */
const self = this,
/** @type {Array.<kanvas_event_callback>} */
events = [];
/** @type {boolean} */
this.autoexecute = false;
/** @type {number} */
this.length = 0;
/** @type {boolean} */
this.unique = unique;
/**
* @param {...any} [parameters]
* @returns {Array.<any|null>}
* @access public
*/
this.execute = (...parameters) => events.map((event, i) => {
let response = null;
if(event){
response = event(...parameters, i);
self.unique && (events[i] = null);
};
return response;
});
/**
* @param {!kanvas_event_callback} callback
* @returns {number|null}
* @access public
*/
this.add = callback => {
/** @type {number|null} */
let i = null;
if(Kanvas.is_function(callback)){
/** @type {number} */
const l = events.length;
for(i = 0; i < l; i ++)
if(!events[i])
break;
this.length ++;
events[i] = callback;
self.autoexecute && callback();
};
return i;
};
/**
* @param {!number} i
* @returns {boolean}
* @access public
*/
this.remove = i => {
if(Kanvas.is_index(i) && events[i]){
events[i] = null;
this.length --;
return true;
};
return false;
};
};
/**
* @class
* @constructor
* @param {!number} x
* @param {!number} y
* @param {!number} radius
* @returns {void}
* @access public
* @static
*/
Kanvas.Circle = function(i, x, y, radius){
/** @type {Kanvas.Circle} */
const self = this;
/** @type {number} */
this.i = i;
/** @type {string} */
this.type = "circle";
/** @type {number} */
this.x = x;
/** @type {number} */
this.y = y;
/** @type {number} */
this.radius = radius;
/**
* @param {!number} x
* @param {!number} y
* @returns {boolean}
* @access public
*/
this.check = (x, y) => (
(x - self.x) ** 2 +
(y - self.y) ** 2
) ** .5 <= self.radius;
};
/**
* @class
* @constructor
* @param {!number} x
* @param {!number} y
* @param {!number} width
* @param {!number} height
* @returns {void}
* @access public
* @static
*/
Kanvas.Rectangle = function(i, x, y, width, height){
/** @type {Kanvas.Rectangle} */
const self = this;
/** @type {number} */
this.i = i;
/** @type {string} */
this.type = "rectangle";
/** @type {number} */
this.x = x;
/** @type {number} */
this.y = y;
/** @type {number} */
this.width = width;
/** @type {number} */
this.height = height;
/**
* @param {!number} x
* @param {!number} y
* @returns {boolean}
* @access public
*/
this.check = (x, y) => (
x >= self.x && x <= self.x + self.width &&
y >= self.y && y <= self.y + self.height
);
};
/**
* @class
* @constructor
* @returns {void}
* @access public
* @static
*/
Kanvas.EventMouse = function(){
/** @type {Kanvas.Event} */
const event = new Kanvas.Event(),
/** @type {Array.<Kanvas.Circle|Kanvas.Rectangle|null>} */
properties = [];
/**
* @param {...any} [parameters]
* @returns {Array.<any|null>}
* @access public
*/
this.execute = event.execute;
/**
* @param {!kanvas_event_mouse_callback} callback
* @param {!number} x
* @param {!number} y
* @param {...any} size
* @returns {number|null}
* @access public
*/
this.add = (callback, x, y, ...size) => {
/** @type {number|null} */
const i = event.add((x, y, ...parameters) => {
properties[i].check(x, y) && callback(x, y, ...parameters);
});
i !== null && (properties[i] = new Kanvas[
size.length == 1 ? "Circle" :
"Rectangle"](i, x, y, ...size));
return properties[i];
};
/**
* @param {!(Kanvas.Rectangle|Kanvas.Circle)} property
* @returns {boolean}
* @access public
*/
this.remove = property => {
if(property && Kanvas.number(property.i) && event.remove(property.i)){
properties[property.i] = null;
return true;
};
return false;
};
};
/**
* @constructor
* @returns {void}
* @access public
* @static
*/
Kanvas.Screen = function(){
/** @type {number} */
this.x = 0;
/** @type {number} */
this.y = 0;
};
/**
* @param {any|null} value
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_array = value => value instanceof Array;
/**
* @param {any|null} value
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_dictionary = value => value && value.constructor == Object;
/**
* @param {any|null} value
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_string = value => typeof value == "string";
/**
* @param {any|null} value
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_function = value => typeof value == "function";
/**
* @param {!(string|Array.<string>)} value
* @returns {Array.<string>}
* @access public
* @static
*/
Kanvas.get_keys = keys => (Kanvas.is_array(keys) ? keys : [keys]).filter((key, i, keys) => Kanvas.is_string(key) && keys.indexOf(key) == i);
/**
* @param {!(string|Array.<string>)} value
* @param {!(Object.<string, any|null>|Array<any|null>)} inputs
* @param {any|null|undefined} [_default = undefined]
* @returns {any|null|undefined}
* @access public
* @static
*/
Kanvas.get_value = (keys, inputs, _default = undefined) => {
/** @type {any|null|undefined} */
let response;
Kanvas.is_array(keys) || (keys = Kanvas.get_keys(keys));
return (
Kanvas.is_dictionary(inputs) ? keys.some(key => (response = inputs[key]) !== undefined) :
Kanvas.is_array(inputs) ? inputs.some(subinputs => (response = Kanvas.get_value(keys, subinputs)) !== undefined) :
false) ? response : _default;
};
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_class = item => typeof item == "function" && item.toString().startsWith("class ");
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_html_object = item => item && (item.tagName || item.nodeName);
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_number = item => typeof item == "number";
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_integer = item => Kanvas.is_number(item) && item == item >> 0;
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_index = item => Kanvas.is_integer(item) && item >= 0;
/**
* @param {any|null|Array} item
* @returns {Array.<any|null>}
* @access public
* @static
*/
Kanvas.get_array = item => Kanvas.is_array(item) ? item : [item];
/**
* @param {!(Object.<string, any|null>|Array)} dictionaries
* @param {!boolean} [overwrite = false]
* @param {!Object.<string, any|null>} [dictionary = {}]
* @returns {Object.<string, any|null>}
* @access public
* @static
*/
Kanvas.get_dictionary = (dictionaries, overwrite = false, dictionary = {}) => {
Kanvas.get_array(dictionaries).forEach(value => {
if(Kanvas.is_array(value))
Kanvas.get_dictionary(value, overwrite, dictionary);
else if(Kanvas.is_dictionary(value)){
for(const key in value)
(overwrite || dictionary[key] === undefined) && (dictionary[key] = value[key]);
};
});
return dictionary;
};
/**
* @param {!Array.<any|null>} array
* @param {!Array.<string>} map
* @returns {Object.<string, any|null>}
* @access public
* @static
*/
Kanvas.map_dictionary = (array, map) => map.reduce((dictionary, key, i) => {
dictionary[key] = array[i];
return dictionary;
}, {});
/**
* @param {any|null} value
* @param {!Array.<any|null>} nested_array
* @param {number|null} [_default = null]
* @returns {number|null}
* @access public
* @static
*/
Kanvas.get_i_from_nested_array = (value, nested_array, _default = null) => {
/** @type {number} */
const l = nested_array.length;
for(let i = 0; i < l; i ++)
if(nested_array[i].includes(value))
return i;
return _default;
};
/**
* @param {!string} source
* @param {!kanvas_load_image_callback} callback
* @returns {void}
* @access public
* @static
*/
Kanvas.load_image = (source, callback) => {
/** @type {Image} */
const image = new Image();
image.src = source;
image.onload = () => callback(image, true);
image.onerror = () => callback(image, false);
};
/**
* @param {!number} degrees
* @returns {number}
* @access public
* @static
*/
Kanvas.to_radians = degrees => 2 * Math.PI * degrees / 360;
/**
* @param {!kanvas_execute_callback} method
* @param {...any} parameters
* @returns {any|null}
* @access public
* @static
*/
Kanvas.execute = (method, ...parameters) => Kanvas.is_function(method) && method(...parameters);
/**
* @param {!Array.<any|null>} array
* @param {!kanvas_array_item_callback} callback_per_item
* @param {!kanvas_default_callback} [callback_end = null]
* @returns {void}
* @access public
* @static
*/
Kanvas.execute_array = (array, callback_per_item, callback_end = null) => {
/** @type {number} */
const l = array.length;
if(!l){
Kanvas.execute(callback_end);
return;
};
/** @type {number} */
let loaded = 0;
array.forEach(item => {
Kanvas.execute(callback_per_item, item, () => {
++ loaded == l && Kanvas.execute(callback_end);
});
});
};
/**
* @param {any|null} item
* @returns {boolean}
* @access public
* @static
*/
Kanvas.is_image = item => item instanceof Image;
return Kanvas;
})();