Kanvas = function(input){ const self = this, default_settings = { quality : 1, quality_x : 1, quality_y : 1, cells : 100, origin : 5, // Posición origen. Mirar teclado numérico para ver los diferentes valores para cada posición. frames_per_second : 60, ratio : null, // Expone la proporción de tamaño del Canvas (16/9 para pantallas WideScreen por ejemplo). Si es equivalente a falso cubrirá todo el área de la capa donde se encuentre. overwrite : false, position : "body", autostart : true, object_name : "kanvas", class : "kanvas", application : "Kanvas", x : 0, y : 0, width : 0, height : 0, color : "#000", blur : 0, italic : false, bold : false, size : 1, font : "Arial", align : "left", alpha : 1, degrees : 0, baseline : "Alphabetic", shadow_x : 0, shadow_y : 0, shadow_color : "#000", shadow_blur : 0, border_color : "#000", border_width : 0, text_italic : false, text_bold : false, text_size : 1, font_family : "Arial", text_color : "#000", text_align : "left", text_alpha : 1, rotate_x : 0, rotate_y : 0, rotate_degrees : 0, image_x : 0, image_y : 0, image_alpha : 1, rectangle_color : "#000", rectangle_alpha : 1, rectangle_x : 0, rectangle_y : 0, text_baseline : "Alphabetic", default_value : null, hash_alphabet : "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", hash_length : 7, frames_per_second_samples : 20, settings_overwrite : false, ajax_timeout : 2000 }, custom = {}, // cache = [], _screen = {x : 0, y : 0}, position = {x : 0, y : 0}, events = {}, threads = [], hashes = [], attributes = { natives : ["id", "class", "onmousemove", "tabindex", "onkeydown", "onkeyup"] }, cache = {}, frames_per_second_samples = [], delta_time = 0; let q, qx, qy, // Calidad porcentual. c, // Número de celdas en el lado más corto de la pantalla o área de trabajo rectangular. fx, fy, // Posición contra el foco. dx, dy, // Distancia desde los laterales hasta el cuadrado de trabajo. s, // Tamaño de la celda. Solo se mide un lado pues será cuadrada. mx, my, // Posición origen del lienzo. thread = null, timeout = 0, last_time = 0, started = false, cache_container, context, canvas, // cache_l = 0, thread_object = null, frames_per_second_samples_number, last_frame_per_second_sample = Date.now(), real_frames_per_seconds = 0, ajax_timeout; let item_self = this.item_self; let hash_self = this.hash_self; let object_name = this.object_name; let mouse = this.mouse = {x : 0, y : 0}; this.LOADING = 1 << 0; this.LOADED = 1 << 1; this.ERROR = 1 << 2; this.map = []; const null_or_undefined = this.null_or_undefined = value => value === undefined || value === null; this.get = (url, callback) => { let ended = false; const ajax = new XMLHttpRequest(), end = status => { if(ended) return; ended = true; typeof callback == "function" && callback(ajax.response, ajax.status, ajax.readyState, status, status == "OK"); }, date = Date.now(); ajax.open("get", url, true); ajax.timeout = ajax_timeout; ajax.onreadystatechange = () => { if(ended) return; if(ajax.readyState == 4) end((ajax.status >= 200 && ajax.status < 300) || [301, 302, 304].includes(ajax.status) ? "OK" : "HTTP_ERROR"); else if(Date.now() - date > ajax_timeout) end("FORCED_TIMEOUT"); }; ajax.send(null); ajax.ontimeout = () => end("TIMEOUT"); ajax.onabort = () => end("ABORTED"); ajax.onerror = () => end("ERROR"); return ajax; }; const allow_nulls = this.allow_nulls = nulls => typeof nulls == "boolean" ? nulls : settings(["nulls", "allow_nulls"], null, false, false); const default_value = this.default_value = (_default, nulls) => _default !== undefined && (nulls || _default !== null) ? _default : settings(["default_value", "default", "by_default"], null, null, true); this.settings_add = (inputs, overwrite, callback) => { if(inputs instanceof Array){ let loaded = 0; const end = () => ++ loaded == inputs.length && typeof callback == "function" && callback(); inputs.forEach(input => self.settings_add(input, overwrite, end)); return; }; if(typeof inputs == "object"){ typeof overwrite != "boolean" && (overwrite = settings(["settings_overwrite", "overwrite"])); for(const key in inputs) (overwrite || custom[key] === undefined) && (custom[key] = inputs[key]); inputs.autostart !== undefined && (input.autostart = inputs.autostart); typeof callback == "function" && callback(); return; }; if(typeof inputs == "string"){ let json; try{ if(json = JSON.parse(inputs)){ self.settings_add(json, overwrite, callback); return; }; }catch(exception){}; self.get(inputs, response => { try{ if(json = JSON.parse(response)){ self.settings_add(json, overwrite, callback); return; }; }catch(exception){}; typeof callback == "function" && callback(); }); return; }; typeof callback == "function" && callback(); }; const settings = this.settings = (names, inputs, _default, nulls) => { if(!names) return default_value(_default, nulls); nulls = allow_nulls(nulls); const l = (names.push ? names : names = [names]).length, m = (inputs = (inputs ? inputs.push ? inputs : [inputs] : []).concat([input, custom, default_settings])).length; for(let j = 0; j < m; j ++) if(typeof inputs[j] == "object") for(let i = 0; i < l; i ++) if(names[i] && inputs[j][names[i]] !== undefined && (nulls || inputs[j][names[i]] !== null)) return inputs[j][names[i]]; return default_value(_default, nulls); }; const threads_function = () => threads.forEach(thread => thread && thread()); const threads_start = this.threads_start = frames_per_second => thread_object === null && (thread_object = setInterval(threads_function, 1000 / (isNaN(frames_per_second) || frames_per_second < 1 ? settings(["frames_per_second", "fps"]) : frames_per_second))); const threads_stop = this.threads_stop = () => { if(thread_object === null) return; clearInterval(thread_object); thread_object = null; }; const threads_add = this.threads_add = callback => { if(typeof callback != "function") return null; let i = 0; const l = threads.length; for(; i < l; i ++) if(!threads[i]) break; threads[i] = callback; return i; }; const threads_remove = this.threads_remove = i => !isNaN(i) && threads[i] && (threads[i] = null); const is_html_object = this.is_html_object = variable => typeof variable == "object" && (variable.tagName || variable.nodeName); const preload = this.preload = (selector, callback) => { if(typeof callback != "function") return; if(!selector){ callback(null, false, "NO_SELECTOR"); return; }; if(is_html_object(selector)){ callback(selector, false, "OK"); return; }; if(!selector.substr){ callback(null, false, "BAD_TYPE"); return; }; let item; try{ if(item = document.querySelector(selector)){ callback(item, false, "OK"); return; }; }catch(exception){ callback(null, false, "BAD_SELECTOR"); return; }; const timeout = settings(["preload_timeout", "timeout"]), date = Date.now(); let preload = threads_add(() => { if(item = document.querySelector(selector)){ threads_remove(preload); callback(item, true, "OK"); }else if(Date.now() - date > timeout){ threads_remove(preload); callback(null, true, "TIMEOUT"); }; }); }; const hash = this.hash = () => { let hash, alphabet = settings(["hash_alphabet", "alphabet"]); const length = settings(["hash_length", "length"]), l = (alphabet.push ? alphabet : alphabet = alphabet.split("")).length; do{ hash = ""; while((hash += alphabet[Math.random() * l >> 0]).length < length); }while( hashes.includes(hash) || /^\d/.test(hash) || document.querySelector("." + hash + ",#" + hash + ",[name=" + hash + "]") ); hashes.push(hash); return hash; }; const set_attribute = this.set_attribute = (item, custom_attributes) => { if(!is_html_object(item) || typeof custom_attributes != "object") return; for(const name in custom_attributes) item.setAttribute((attributes.natives.includes(name) ? "" : "data-") + name.replace(/[^a-z\d-]+/g, "-"), custom_attributes[name]); }; const construct = () => { const custom_attributes = { natives : settings("attributes_natives") }; for(const key in custom_attributes){ !attributes[key] && (attributes[key] = []); (custom_attributes ? custom_attributes.push ? custom_attributes : [custom_attributes] : []).forEach(new_attribute => attributes[key].push(new_attribute)); }; object_name = self.object_name = settings("object_name"); settings("autostart") && self.start(); }; const Events = this.Events = function(){ const events = []; this.add = callback => { if(typeof callback != "function") return null; let i = 0; const l = events.length; while(i < l){ if(events[i] === null) break; i ++; }; events[i] = callback; return i; }; this.remove = i => !isNaN(i) && i >= 0 && events[i] && (events[i] = null); this.execute = (...inputs) => events.forEach(event => event && event(...inputs)); }; const range_analyze = range => !range || ( mouse.x >= range.x && mouse.x <= range.x + range.width && mouse.y >= range.y && mouse.y <= range.y + range.height ); const on_resize_method = screen => { if(_screen.x == item_self.offsetWidth && _screen.y == item_self.offsetHeight) return; const width = canvas.width = (_screen.x = item_self.offsetWidth) * q * qx, height = canvas.height = (_screen.y = item_self.offsetHeight) * q * qy; if(width < height){ s = width / c; dx = 0; dy = -(c - (c * height / width)) / 2; mx = position.x; my = position.y + dy; }else{ s = height / c; dx = -(c - (c * width / height)) / 2; dy = 0; mx = position.x + dx; my = position.y; }; //resize_methods.forEach((method) => {if(method)method();}); execute_event("resize"); }; const position_set = () => { const origin = settings("origin"); position.x = c * (.5 * ((origin - 1) % 3)); position.y = c * (1 - (.5 * ((origin - 1) / 3 >> 0))); }; const calculate_real_frames_per_seconds = () => { const now = Date.now(), time = now - last_frame_per_second_sample; frames_per_second_samples.push(time); last_frame_per_second_sample = now; while(frames_per_second_samples.length > frames_per_second_samples_number) frames_per_second_samples.shift(); real_frames_per_seconds = 1000 / (frames_per_second_samples.reduce((a, b) => a + b) / frames_per_second_samples.length); delta_time = time / 1000; }; this.start = callback => { const end = status => typeof callback == "function" && callback(status); if(started){ end(false); return false; }; started = true; q = settings(["quality", "q"]); qx = settings(["quality_x", "qx"]); qy = settings(["quality_y", "qy"]); timeout = 1000 / settings("frames_per_second"); c = settings("cells"); position_set(); threads_start(); ajax_timeout = settings("ajax_timeout"); preload(settings("position"), (position, asynchronous, error) => { if(!position){ console.error("ERROR. Position HTML for install GUI CANVAS is bad. [" + error + "]"); return; }; const _class = (hash_self = self.hash_self = hash()) + " " + settings("class"); !new RegExp("\\b" + default_settings.class + "\\b").test(_class) && (_class += " " + default_settings.class); set_attribute(item_self = self.item_self = position.appendChild(document.createElement("div")), { id : hash_self, hash : hash_self, class : _class, application : default_settings.application, onmousemove : object_name + ".check_mouse(this, event);", tabindex : 0, onkeydown : object_name + ".key_down(this, event);", onkeyup : object_name + ".key_up(this, event);", cells : c, mouse_x : 0, mouse_y : 0 }); set_attribute(cache_container = item_self.appendChild(document.createElement("div")), { class : "cache" }); set_attribute(canvas = item_self.appendChild(document.createElement("canvas")), { class : "canvas" }); context = canvas.getContext("2d"); thread = threads_add(thread_method); on_resize_thread = threads_add(on_resize_method); item_self.onclick = () => execute_event("click"); frames_per_second_samples_number = self.settings("frames_per_second_samples"); threads_add(calculate_real_frames_per_seconds); end(true); }); return true; }; // o = Origin {mx, my} const draw = this.draw = (map, context, o) => map && map.forEach((level, i) => level && (level.push && draw(level, context, o || {mx : mx, my : my}) || (level.type && components[level.type] && components[level.type](level, context, self, o || {mx : mx, my : my})))); const refresh_draw = () => { if(!context) return; context.clearRect(0, 0, canvas.width, canvas.height); draw(self.map, context); }; const thread_method = () => { const date = Date.now(); if(date - last_time < timeout) return; last_time = date; refresh_draw(); }; // Establecer posición sobre el foco de origen. // Establecer posición sobre los laterales del recuadro o área de trabajo. const value = this.value = (value, quality) => q * value * (quality || 1); // const _x = this._x = value => q * qx * value * s; // const _y = this._y = value => q * qy * value * s; const _x = this._x = value => value * s; const _y = this._y = value => value * s; this.preload_cache_items = (items, callback_per_item, callback) => { const end = status => typeof callback == "function" && callback(status); typeof items == "string" && (items = [items]); if(!(items instanceof Array)){ end(false); return; }; let fully = true, loaded = 0; const has_callback_per_item = typeof callback_per_item == "function", on_item_loaded = (url, ok) => { ok !== undefined && (cache[url].status = ok ? self.LOADED : self.ERROR); cache[url].on_load.execute(url, ok); has_callback_per_item && callback_per_item(url, ok); ok === false && fully && (fully = false); ++ loaded == items.length && end(fully); }; items.forEach(url => { if(cache[url]){ if(cache[url].status == self.LOADING) cache[url].on_load.add(has_callback_per_item); else on_item_loaded(url); return; }; const extension = ((url.match(/\.([^\.]+)/) || [])[1] || "").toLowerCase(); cache[url] = { status : self.LOADING, on_load : new Events() }; switch(extension){ case "jpg": case "jpeg": case "jpge": case "png": case "webp": case "gif": case "tif": case "tiff": case "pcx": case "bmp": const image = new Image(); cache[url].type = "image"; cache[url].image = image; image.src = url; image.onload = () => on_item_loaded(url, true); image.onerror = () => on_item_loaded(url, false); break; default: cache[url].type = "unknown"; cache[url].status = self.ERROR; on_item_loaded(url, false); break; }; }); }; const angle = this.angle = (x, y, randians) => { if(typeof x == "object"){ const line = x.push ? { x : x[0] - y[0], y : x[1] - y[1] } : { x : x.x - y.x, y : x.y - y.y }; x = line.x; y = line.y; }; let angle = Math.asin(y / ((x ** 2) + (y ** 2)) ** .5); // if(x >= 0) // angle += Math.PI / 2; // else // angle = (1.5 * Math.PI) - angle; return (x >= 0 ? angle + (Math.PI / 2) : ((1.5 * Math.PI) - angle)) * (randians ? 1 : 180 / Math.PI); }; const shadow = (data, context) => { const z = dx < dy ? _x : _y; if(!data.ok){ isNaN(data.x) && (data.x = settings(["shadow_x", "x"])); isNaN(data.y) && (data.y = settings(["shadow_y", "y"])); !data.color && (data.color = settings(["shadow_color", "color"])); isNaN(data.blur) && (data.blur = settings(["shadow_blur", "blur"])); data.ok = true; }; context.shadowOffsetX = z(data.x); context.shadowOffsetY = z(data.y); context.shadowColor = data.color; context.shadowBlur = z(data.blur); }; const border = (data, context) => { if(!data.ok){ !data.color && (data.color = settings(["border_color", "color"])); isNaN(data.width) && (data.width = settings(["border_width", "width"])); data.ok = true; }; context.strokeStyle = data.color; context.lineWidth = (dx < dy ? _x : _y)(data.width); }; const components = { rotate : (data, context, kanvas, o) => { if(data.ignore) return; if(!data.ok){ isNaN(data.x) && (data.x = settings(["rotate_x", "x"])); isNaN(data.y) && (data.y = settings(["rotate_y", "y"])); isNaN(data.degrees) && (data.degrees = settings(["rotate_degrees", "degrees"])); data.ok = true; }; // console.log(JSON.stringify(data)); // console.log(JSON.stringify([_x(data.x + mx), _y(data.y + my)])); context.save(); context.translate(_x(data.x + o.mx), _y(data.y + o.my)); context.rotate(data.degrees * Math.PI / 180); draw(data.childs, context, {mx : 0, my : 0}); context.restore(); }, image : (data, context, kanvas, o) => { if(data.ignore) return; !data.ok && !data.source && (data.source = data.src || data.url); if(cache[data.source]){ if(!data.ok){ if(cache[data.source] && cache[data.source].status == self.LOADED){ isNaN(data.swidth) && (data.swidth = cache[data.source].image.width); isNaN(data.sheight) && (data.sheight = cache[data.source].image.height); isNaN(data.width) && (data.width = data.swidth); isNaN(data.height) && (data.height = data.sheight); }; isNaN(data.x) && (data.x = settings(["image_x", "x"])); isNaN(data.y) && (data.y = settings(["image_y", "y"])); isNaN(data.alpha) && (data.alpha = settings(["image_alpha", "alpha"])); isNaN(data.sx) && (data.sx = 0); isNaN(data.sy) && (data.sy = 0); isNaN(data.mx) && (data.mx = 0); isNaN(data.my) && (data.my = 0); data.ok = true; }; if(cache[data.source].status != self.LOADED) return; const half_width = data.width / 2, half_height = data.height / 2; context.save(); context.globalAlpha = data.alpha; context.translate(_x(o.mx + data.x + data.mx + half_width), _y(o.my + data.y + data.my + half_height)); context.rotate(data.rotate * Math.PI / 180); context.drawImage( cache[data.source].image, data.sx, data.sy, data.swidth, data.sheight, _x(-half_width), _y(-half_height), _x(data.width), _y(data.height) ); draw(data.childs, context, {mx : 0, my : 0}); context.restore(); }else self.preload_cache_items([data.source]); }, // cache : (data, context, kanvas, o) => { // if(data.ignore) // return; // context.save(); // if(isNaN(data.cache_i)){ // const cache_canvas = cache_container.appendChild(document.createElement("canvas")), // cache_context = cache_canvas.getContext("2d"), // width = data.width || canvas.width, // height = data.height || canvas.height; // image = new Image(); // cache_canvas.width = width; // cache_canvas.height = height; // get_new_cache_i(data); // cache_context.save(); // // cache_context.translate(_x(-o.mx), _y(-o.my)); // draw(data.childs, cache_context, {mx : 0, my : 0}); // image.src = cache_canvas.toDataURL("image/png"); // cache[data.cache_i] = image; // cache_context.restore(); // cache_canvas.remove(); // }; // context.drawImage(cache[data.cache_i], 0, 0); // context.restore(); // }, rectangle : (data, context, kanvas, o) => { if(data.ignore)return; const proportion = canvas.width > canvas.height ? canvas.width / canvas.height : canvas.height / canvas.width; if(!data.ok){ isNaN(data.alpha) && (data.alpha = settings(["rectangle_alpha", "alpha"])); !data.color && (data.color = settings(["rectangle_color", "color"])); isNaN(data.x) && (data.x = settings(["rectangle_x", "x"])); isNaN(data.y) && (data.y = settings(["rectangle_y", "y"])); isNaN(data.width) && (data.width = proportion * c); isNaN(data.height) && (data.height = proportion * c); isNaN(data.mx) && (data.mx = 0); isNaN(data.my) && (data.my = 0); data.ok = true; }; context.save(); data.shadow && (shadow(data.shadow, context)); context.globalAlpha = data.alpha; context.fillStyle = data.color; context.translate(_x(o.mx + data.x + data.mx), _y(o.my + data.y + data.my)); context.rotate(data.rotate * Math.PI / 180); context.translate(_x(-data.mx), _y(-data.my)); context.fillRect(_x(data.x), _y(data.y), _x(data.width), _y(data.height)); draw(data.childs, context, {mx : 0, my : 0}); context.restore(); }, text : (data, context, kanvas, o) => { if(data.ignore) return; if(!data.ok){ typeof data.italic != "boolean" && (data.italic = settings(["text_italic", "italic"])); isNaN(data.bold) && typeof data.bold != "boolean" && (data.bold = settings(["text_bold", "bold"])); isNaN(data.size) && (data.size = settings(["text_size", "size"])); data.font && (data.font_family = data.font); !data.font_family && (data.font_family = settings(["text_font_family", "text_font", "font"])); !data.align && (data.align = settings(["text_align", "align"])); isNaN(data.x) && (data.x = settings(["text_x", "x"])); isNaN(data.y) && (data.y = settings(["text_y", "y"])); !data.text && (data.text = ""); isNaN(data.alpha) && (data.alpha = settings(["text_alpha", "alpha"])); !data.baseline && (data.baseline = settings(["text_baseline", "baseline"])); data.ok = true; }; context.save(); data.shadow && (shadow(data.shadow, context)); context.globalAlpha = data.alpha; context.textAlign = data.align; context.textBaseline = data.baseline; context.font = ( (data.italic ? "italic " : "") + (data.bold ? isNaN(data.bold) ? "bold " : data.bold + " " : "") + (dx < dy ? _x : _y)(data.size) + "px " + data.font_family ); if(data.color){ context.fillStyle = data.color; context.fillText(data.text, _x(data.x + mx), _y(data.y + my)); }; if(data.border){ border(data.border, context); context.strokeText(data.text, _x(data.x + mx), _y(data.y + my)); }; context.restore(); }, block : (data, context, kanvas, o) => { if(data.ignore) return; if(!data.ok){ isNaN(data.alpha) && (data.alpha = settings(["block_alpha", "alpha"])); data.ok = true; }; context.save(); context.globalAlpha = data.alpha; draw(data.childs, context, {mx : 0, my : 0}); context.restore(); } }; this.add_components = (json, overwrite) => { if(!json) return; !json.push && (json = [json]); typeof overwrite != "bool" && (overwrite = settings("overwrite")); json.forEach((items) => { if(!items) return; if(items.push) self.add_components(items, overwrite); else if(typeof items == "object") for(let key in items) (overwrite || !components[key]) && (components[key] = items[key]); }); }; this.extends = (object, overwrite) => { for(const key in self) (overwrite || object[key] === undefined) && (object[key] = self[key]); }; this.check_mouse = (item, event) => { item_self.setAttribute("data-mouse-x", mouse.x = (event.clientX * q * qx / s) - mx); item_self.setAttribute("data-mouse-y", mouse.y = (event.clientY * q * qy / s) - my); execute_event("mouse_move"); }; const on_event = this.on_event = (name, method, range) => { if(typeof method != "function") return null; !events[name] && (events[name] = { methods : [], l : 0, ranges : [] }); let i = 0; for(; i < events[name].l; i ++) if(!events[name].methods[i]) break; events[name].methods[i] = method; events[name].ranges[i] = range; events[name].l = events[name].methods.length; return i; }; const remove_event = this.remove_event = (name, i) => events[name] && !isNaN(i) && i >= 0 && i < events[name].l && events[name].methods[i] && (events[name].methods[i] = null); const execute_event = this.execute_event = (name, input) => { if(events[name] && events[name].l) for(let i = 0; i < events[name].l; i ++) events[name].methods[i] && events[name].methods[i](range_analyze(events[name].ranges[i]), input); }; const event_range = this.event_range = (name, i) => events[name] && !isNaN(i) && i >= 0 && i < events[name].l ? events[name].ranges[i] : null; this.on_mouse_move = (method, range) => on_event("mouse_move", method, range); this.on_resize = (method, range) => on_event("resize", method, range); this.on_click = (method, range) => on_event("click", method, range); this.on_key_down = (method, range) => on_event("key_down", method, range); this.on_key_up = (method, range) => on_event("key_up", method, range); this.remove_mouse_move = i => {remove_event("mouse_move", i);}; this.remove_resize = i => {remove_event("resize", i);}; this.remove_click = i => {remove_event("click", i);}; this.remove_key_down = i => {remove_event("key_down", i);}; this.remove_key_up = i => {remove_event("key_up", i);}; this.range_mouse_move = i => event_range("mouse_move", i); this.range_resize = i => event_range("resize", i); this.range_click = i => event_range("click", i); this.range_key_down = i => event_range("key_down", i); this.range_key_up = i => event_range("key_up", i); this.key_down = (item, event) => execute_event("key_down", {code : event.keyCode}); this.key_up = (item, event) => execute_event("key_up", {code : event.keyCode}); this.get_margins = () => ({x : mx, y : my}); this.get_position = () => ({x : position.x, y : position.y}); this.get_cells = () => c; this.get_canvas_distance = () => ({x : dx, y : dy}); this.get_cell_size = () => s; this.get_real_frames_per_second = () => real_frames_per_seconds; this.delta_time = () => delta_time; construct(); };