Kanvas = function(settings){ const self = this, default_settings = { nulls : false, default_value : null, position : ".kanvas", preload_timeout : 2000, frames_per_second : 60, quality : 1, quality_x : 1, quality_y : 1, cells : 40, swap_and_drop_timer : 5000, font_size : 1, font_family : "Arial", settings_overwrite : false, autostart : true, autobuild : true, font_minimum_size : 12, events_cache_timer : 100 }, custom = {}, cache = {}, frames_times = [], id_length = 11, id_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", ids = [], threads = [], number_of_cells = {x : 0, y : 0}, events_cache = []; let thread = null, frames_per_second = null, canvas, context, last_frame_time = 0, frames_times_summatory = 0, swap_and_drop_timer, cache_box, default_font_size, default_font_family, settings_overwrite, built = false, started = false, font_minimum_size, events_cache_timer; this.map = []; let cells = this.cells; let cell_size = this.cell_size; let quality = this.quality; let quality_x = this.quality_x; let quality_y = this.quality_y; this.cells_x = 0; this.cells_y = 0; this.delta_time = 0; let item_self = this.item_self; let hash_self = this.hash_self; let object_name = this.object_name; this.Event = function(){ const self = this, events = [], properties = []; const construct = () => { events_cache.push(autoclean); }; this.execute = (callback, ...arguments) => events.forEach((event, i) => ( event && (typeof callback == "function" ? callback(properties[i]) : true) && event(...arguments) )); this.add = (callback, own_properties) => { let i = 0; const l = events.length; for(; i < l; i ++) if(!events[i]) break; events[i] = callback; properties[i] = own_properties; return i; }; this.remove = i => events[i] = null; const autoclean = () => { const date = Date.now(); properties.forEach((own_properties, i) => { if(own_properties && own_properties.last_used && date - own_properties.last_used > events_cache_timer){ events[i] = null; properties[i] = null; }; }); }; this.update = (i, date) => events[i] && (properties[i].last_used = date); construct(); }; this.on_screen_change = new this.Event(); this.on_ready = new this.Event(); this.on_click = new this.Event(); this.on_click_down = new this.Event(); this.on_click_up = new this.Event(); this.on_key_down = new this.Event(); this.on_key_up = new this.Event(); const construct = () => { settings = ( settings instanceof Array ? settings : typeof settings == "object" ? [settings] : []).filter(inputs => typeof inputs == "object" && !(inputs instanceof Array)); self.settings("autostart") && self.start(); }; this.start = callback => { const end = status => typeof callback == "function" && callback(status); if(started){ end(false); return false; }; started = true; settings_overwrite = self.settings(["settings_overwrite", "overwrite"]); frames_per_second = self.settings(["frames_per_second", "fps"]); events_cache_timer = self.settings("events_cache_timer"); object_name = self.object_name = self.settings("object_name"); thread = setInterval(execute, 1000 / frames_per_second); if(self.settings("autobuild")) self.build(callback); else end(true); return true; }; this.build = callback => { const position = self.settings("position"), end = status => typeof callback == "function" && callback(status); if(built){ end(false); return false; }; built = true; if(position){ if(position.tagName || position.nodeName){ end_build(position); end(true); return; }; if(typeof position != "string"){ console.error("position_not_string"); end(false); return; }; if(!position.trim()){ console.error("position_selector_empty"); end(false); return; }; let html_object; try{ if(html_object = document.querySelector(position)){ end_build(html_object); end(true); return; }; }catch(exception){ console.error(exception); console.error("position_bad_selector"); end(false); return; }; const date = Date.now(), timeout = self.settings("preload_timeout"); let interval = setInterval(() => { if(html_object = document.querySelector(position)){ clearInterval(interval); end_build(html_object); end(true); }else if(Date.now() - date > timeout){ clearInterval(interval); console.error("position_timeout"); end(false); }; }, frames_per_second); }else{ console.error("no_position"); end(false); }; return true; }; const end_build = position => { quality = self.quality = self.settings("quality"); quality_x = self.quality_x = self.settings("quality_x"); quality_y = self.quality_y = self.settings("quality_y"); cells = self.cells = self.settings("cells"); default_font_size = self.settings("font_size"); default_font_family = self.settings("font_family"); cache.quality = 0; cache.quality_x = 0; cache.quality_y = 0; cache.screen = {x : 0, y : 0}; cache.origin = {x : 0, y : 0}; item_self = self.item_self = (position || document.querySelector("body")).appendChild(document.createElement("div")); cache_box = item_self.appendChild(document.createElement("div")); canvas = item_self.appendChild(document.createElement("canvas")); hash_self = self.hash_self = self.settings(["id", "hash"]) || self.create_id(); item_self.setAttribute("id", hash_self); item_self.setAttribute("class", ["kanvas", hash_self].concat((self.settings("class") || "").split(/\s+/)).filter((key, i, array) => array.indexOf(key) == i).join(" ")); item_self.setAttribute("data-hash", hash_self); item_self.setAttribute("data-cells", cells); item_self.setAttribute("data-minimum-font-size", font_minimum_size = self.settings("font_minimum_size")); cache_box.setAttribute("class", "kanvas-cache-box"); cache_box.setAttribute("style", ` position : absolute; top : 0%; left : 0%; width : 100%; height : 100%; visibility : hidden; z-index : 10; opacity : 0; `.replace(/[\r\n\s]+/g, "")); canvas.setAttribute("class", "kanvas-ui"); canvas.setAttribute("style", ` position : absolute; top : 0%; left : 0%; width : 100%; height : 100%; z-index : 20; `.replace(/[\r\n\s]+/g, "")); canvas.onclick = event => on_click(canvas, event, "click"); canvas.onmousedown = event => on_click(canvas, event, "down"); canvas.onmouseup = event => on_click(canvas, event, "up"); canvas.onkeydown = event => on_key(canvas, event, "down"); canvas.onkeyup = event => on_key(canvas, event, "up"); context = canvas.getContext("2d"); context.id = self.create_id(); swap_and_drop_timer = self.settings("swap_and_drop_timer"); self.on_ready.execute(); }; this.nulls = nulls => typeof nulls == "boolean" ? nulls : self.settings("nulls", null, false, false); this.default_value = (_default, nulls) => _default !== undefined && (self.nulls(nulls) || _default !== null) ? _default : self.settings("default_value", null, null, true); this.settings = (names, inputs, _default, nulls) => { const l = (names = ( names instanceof Array ? names : typeof names == "string" ? [names] : [] ).filter((name, i, array) => name && typeof name == "string" && array.indexOf(name) == i)).length; if(l){ const m = (inputs = ( inputs instanceof Array ? inputs : typeof inputs == "object" ? [inputs] : []).concat(settings, custom, [default_settings])).length; nulls = self.nulls(nulls); for(let j = 0; j < m; j ++) if(inputs[j] && typeof inputs[j] == "object" && !(inputs[j] instanceof Array)) for(let i = 0; i < l; i ++) if(inputs[j][names[i]] !== undefined && (nulls || inputs[j][names[i]] !== null)) return inputs[j][names[i]]; }; return self.default_value(_default, nulls); }; this.settings_add = (inputs, overwrite) => { if(!inputs) return; if(typeof inputs == "string"){ try{ inputs = JSON.parse(inputs); }catch(exception){}; }; if(typeof inputs == "object"){ if(inputs instanceof Array) inputs.forEach(inputs, overwrite); else{ typeof overwrite != "boolean" && (overwrite = settings_overwrite); for(const key in inputs) if(overwrite || custom[key] === undefined) custom[key] = inputs[key]; }; }; }; this.create_id = () => { let id; const l = id_alphabet.length; do{ id = ""; while((id += id_alphabet[l * Math.random() >> 0]).length < id_length); }while( ids.includes(id) || !/^[a-z]/i.test(id) || document.querySelector("." + id + ",#" + id + ",[name=" + id + "]") ); ids.push(id); return id; }; const execute = () => { const date = Date.now(); let screen_changed = false; if(item_self && (cache.screen.x != item_self.offsetWidth || cache.screen.y != item_self.offsetHeight)){ screen_changed = true; cache.screen.x = item_self.offsetWidth; cache.screen.y = item_self.offsetHeight; const font_size = cache.screen[cache.screen.x < cache.screen.y ? "x" : "y"] / cells; item_self.style.fontSize = (font_size < font_minimum_size ? font_minimum_size : font_size) + "px"; }; if(canvas){ if(last_frame_time){ const frame_time = date - last_frame_time; frames_times.push(frame_time); frames_times_summatory += frame_time; self.delta_time = frame_time / 1000; while(frames_times.length > frames_per_second) frames_times_summatory -= frames_times.shift(); }; last_frame_time = date; if(screen_changed || cache.quality != quality){ const width = cache.screen.x * quality, height = cache.screen.y * quality; cache.quality = quality; canvas.setAttribute("width", width); canvas.setAttribute("height", height); cache.origin.x = width / 2; cache.origin.y = height / 2; cell_size = self.cell_size = (width > height ? height : width) / cells; number_of_cells.x = width / cell_size; number_of_cells.y = height / cell_size; this.cells_x = number_of_cells.x / 2; this.cells_y = number_of_cells.y / 2; for(const key in cache) if(cache[key] && cache[key].data) cache[key].data = null; self.on_screen_change.execute(); }; }; threads.forEach(thread => thread && thread()); if(canvas){ context.beginPath(); context.clearRect(0, 0, cache.screen.x * quality, cache.screen.y * quality); context.translate(cache.origin.x, cache.origin.y); draw(context, self.map, 0, 0); context.translate(-cache.origin.x, -cache.origin.y); for(const key in cache) if(cache[key] && cache[key].last_used && date - cache[key].last_used > swap_and_drop_timer) delete cache[key]; }; events_cache.forEach(autoclean => autoclean()); }; this.set_quality = new_quality => quality = self.quality = new_quality; this.set_quality_x = new_quality => quality_x = self.quality_x = new_quality; this.set_quality_y = new_quality => quality_y = self.quality_y = new_quality; const _x = x => x * cell_size; const _y = y => y * cell_size; const size = size => size * cell_size; const set_cache = (context, status) => { !status && cache[context.id] && cache[context.id].ok && (cache[context.id].ok = false); return status; }; const set_border = (context, inputs) => { const has_border = !!(inputs.border_color || !isNaN(inputs.border_width)); inputs.border_color && (context.strokeStyle = inputs.border_color); !isNaN(inputs.border_width) && (context.lineWidth = size(inputs.border_width)); return has_border; }; const set_background = (context, inputs) => { const has_background = !!(inputs.background || (!inputs.border_color && isNaN(inputs.border_width))); if(inputs.background){ if(inputs.background instanceof Array){ const v = inputs.background, is_linear = v.length == 5, gradient = ( is_linear ? context.createLinearGradient(_x(v[0]), _y(v[1]), _x(v[2]), _y(v[3])) : context.createRadialGradient(_x(v[0]), _y(v[1]), _x(v[2]), _y(v[3]), size(v[4]), size(v[5])) ); inputs.background[is_linear ? 4 : 6].forEach(color => gradient.addColorStop(color[0], color[1])); context.fillStyle = gradient; }else context.fillStyle = inputs.background; }; return has_background; }; const set_shadow = (context, inputs, shape_callback) => { const shadows = inputs.shadow || inputs.shadows; (shadows && shadows.length ? shadows[0] instanceof Array ? shadows : [shadows] : []).forEach(shadow => { [context.shadowOffsetX, context.shadowOffsetY, context.shadowBlur, context.shadowColor] = shadow; context.shadowOffsetX = size(shadow[0]); context.shadowOffsetY = size(shadow[1]); context.shadowBlur = size(shadow[2]); context.shadowColor = size(shadow[3]); shape_callback(); }); }; const shapes = { rectangle : (context, inputs) => { const x = _x(inputs.x), y = _y(inputs.y), width = size(inputs.width), height = size(inputs.height), has_border = set_border(context, inputs), has_background = set_background(context, inputs); set_shadow(context, inputs, () => context.rect(x, y, width, height)); has_background && context.fillRect(x, y, width, height); has_border && context.strokeRect(x, y, width, height); return set_cache(context, true); }, image : (context, inputs) => { const url = inputs.url; if(url){ const cached = cache[url]; if(!cached){ (cache[url] = { image : new Image(), loaded : false, last_used : Date.now() }).image.src = url; cache[url].image.crossOrigin = "anonymous"; cache[url].image.onload = () => cache[url].loaded = true; return set_cache(context, false); }; if(cached.loaded){ let width = inputs.width, height = inputs.height; const cut_x = inputs.cut_x || 0, cut_y = inputs.cut_y || 0, cut_width = inputs.cut_width || 0, cut_height = inputs.cut_height || 0, position_x = inputs.x || 0, position_y = inputs.y || 0, x = _x(position_x), y = _y(position_y), end_width = size(width), end_height = size(height); !width && (width = cache.quality * (cut_width || cache[url].image.width - cut_x) / cell_size); !height && (height = cache.quality * (cut_height || cache[url].image.height - cut_y) / cell_size); set_shadow(context, inputs, () => context.rect(x, y, end_width, end_height)); set_border(context, inputs); context.drawImage( cache[url].image, cut_x, cut_y, cut_width || cache[url].image.width - cut_x, cut_height || cache[url].image.height - cut_y, x, y, end_width, end_height ); cache[url].last_used = Date.now(); return set_cache(context, true); }; }; return set_cache(context, false); }, cache : (context, inputs) => { const width = inputs.width ? inputs.width * cell_size : canvas.getAttribute("width"), height = inputs.height ? inputs.height * cell_size : canvas.getAttribute("height"); let status = false; if(!cache[inputs.name]){ const subcanvas = cache_box.appendChild(document.createElement("canvas")); cache[inputs.name] = { canvas : subcanvas, data : null }; cache[inputs.name].context = subcanvas.getContext("2d"); cache[inputs.name].context.id = inputs.name; subcanvas.setAttribute("data-id", cache[inputs.name].context.id); cache[inputs.name].context.translate(inputs.x || 0, inputs.y || 0); }; if(cache[inputs.name].data){ if(cache[inputs.name].image_loaded){ context.drawImage(cache[inputs.name].image, _x(inputs.x || 0), _y(inputs.y || 0), cache[inputs.name].image.width, cache[inputs.name].image.height); status = true; }else if(!cache[inputs.name].image){ cache[inputs.name].image = new Image(); cache[inputs.name].image.src = cache[inputs.name].data; cache[inputs.name].image.onload = () => { // cache[inputs.name].canvas.remove(); cache[inputs.name].image_loaded = true; }; // cache[inputs.name].image.onerror = () => cache[inputs.name].canvas.remove(); }; }else{ cache[inputs.name].canvas.setAttribute("width", width); cache[inputs.name].canvas.setAttribute("height", height); cache[inputs.name].context.beginPath(); cache[inputs.name].context.clearRect(0, 0, width, height); cache[inputs.name].context.translate(width / 2, height / 2); cache[inputs.name].image = null; cache[inputs.name].image_loaded = false; cache[inputs.name].ok = true; draw(cache[inputs.name].context, inputs.childs, 0, 0); cache[inputs.name].context.closePath(); cache[inputs.name].ok && (cache[inputs.name].data = cache[inputs.name].canvas.toDataURL("image/png", 1.0)); }; cache[inputs.name].last_used = Date.now(); return set_cache(context, status); }, text : (context, inputs) => { const x = _x(inputs.x), y = _y(inputs.y), has_border = set_border(context, inputs), has_background = set_background(context, inputs); inputs.align && (context.textAlign = inputs.align); inputs.baseline && (context.textBaseline = inputs.baseline); !isNaN(inputs.border_width) && (context.lineWidth = size(inputs.border_width)); context.font = (inputs.style ? inputs.style + " " : "") + size(inputs.size || default_font_size) + "px " + (inputs.family || default_font_family); set_shadow(context, inputs, () => context.fillText(inputs.text, x, y)); has_background && context.fillText(inputs.text, x, y); has_border && context.strokeText(inputs.text, x, y); return true; } }; this.string_variables = (string, variables) => string.replace(/\{([^\{\}]+)\}/g, (...arguments) => variables[arguments[1]] !== undefined ? variables[arguments[1]] : arguments[0]); const draw = (context, level, x, y) => level.forEach(values => { if(values && (shapes[values.type] || ["block"].includes(values.type))){ const sub_x = _x(x + (values.margin_x || 0)), sub_y = _y(y + (values.margin_y || 0)), date = Date.now(); context.save(); context.translate(sub_x, sub_y); const transform = context.getTransform(); values.rotate && context.rotate(2 * Math.PI * values.rotate / 360); !isNaN(values.alpha) && (context.globalAlpha = values.alpha); if(values.on_click){ values.context_x = transform.e / quality; values.context_y = transform.f / quality; if(!values.last_used || date - values.last_used > events_cache_timer) values.i = self.on_click.add(eval(self.string_variables(values.on_click, {object_name : object_name})), values); else self.on_click.update(values.i, date); values.last_used = date; }; values.type != "block" && shapes[values.type](context, values); values.type != "cache" && values.childs && draw(context, values.childs, values.x, values.y); context.translate(-sub_x, -sub_y); context.restore(); }; }); this.get_real_fps = this.get_real_frames_per_second = () => frames_times_summatory ? 1000 / (frames_times_summatory / frames_times.length) : 0; this.threads_add = callback => { let i = 0; const l = threads.length; for(; i < l; i ++) if(!threads[i]) break; threads[i] = callback; return i; }; this.threads_remove = i => threads[i] = null; this.get_cells_x = () => number_of_cells.x; this.get_cells_y = () => number_of_cells.y; this.extends = object => { for(const key in self) if(object[key] === undefined) object[key] = self[key]; }; const on_click = (canvas, event, action) => { switch(action){ case "click": self.on_click.execute(properties => ( event.clientX >= properties.context_x + (properties.x * cell_size) && event.clientY >= properties.context_y + (properties.y * cell_size) && event.clientX <= properties.context_x + ((properties.x + properties.width) * cell_size) && event.clientY <= properties.context_y + ((properties.y + properties.height) * cell_size) ), canvas, event); break; }; }; construct(); };