From 2c4717a9f96376a2160844a6b43f1dcece7f8b97 Mon Sep 17 00:00:00 2001 From: Karlen Manaseryan Date: Mon, 22 Nov 2021 11:12:37 +0400 Subject: [PATCH 1/2] Issue #192 - Isolated shadow from clipped layer --- src/js/core/base-layers.js | 154 ++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/src/js/core/base-layers.js b/src/js/core/base-layers.js index d54b92c..00e7cab 100644 --- a/src/js/core/base-layers.js +++ b/src/js/core/base-layers.js @@ -175,36 +175,8 @@ class Base_layers_class { config.HEIGHT, config.WIDTH ); - const ctxForSourceAtop = newCanvas.getContext("2d"); - let hasSourceAtopLayer = false; - this.ctx.save(); - - //render main canvas - for (var i = layers_sorted.length - 1; i >= 0; i--) { - var value = layers_sorted[i]; - const nextValue = layers_sorted[i - 1]; - - if ( - value.composition === "source-atop" || - (nextValue && nextValue.composition === "source-atop") - ) { - hasSourceAtopLayer = true; - ctxForSourceAtop.globalAlpha = value.opacity / 100; - ctxForSourceAtop.globalCompositeOperation = - value.composition; - this.render_object(ctxForSourceAtop, value); - } else { - this.ctx.globalAlpha = value.opacity / 100; - this.ctx.globalCompositeOperation = value.composition; - this.render_object(this.ctx, value); - } - } - - if (hasSourceAtopLayer) { - this.ctx.restore(); - this.ctx.drawImage(newCanvas, 0, 0); - } + this.renderObjects(this.ctx, newCanvas, layers_sorted); //grid this.Base_gui.draw_grid(this.ctx); @@ -267,6 +239,69 @@ class Base_layers_class { return newCanvas; } + /** + * Renders objects based on the provided layers + * @param {canvas.context} ctx - Main canvas context where it needs to be rendered + * @param {canvas} tempCanvas - A temporary canvas which is a copy of the original canvas, but will be used if there will be needed to isolate an effect from others + * @param {Object[]} layers - Array of layers + * @param {Function} prepare - An optional function to prepare temporary canvas before the render if needed + * @param {Function} shouldSkip - An optional boolean function for skipping those layers which are not needed to be rendered + */ + renderObjects(ctx, tempCanvas, layers, prepare, shouldSkip) { + const tempCtx = tempCanvas.getContext("2d"); + let hasSourceAtopLayer = false; + + // Prepare the temporary canvas if needed + prepare && prepare(tempCtx); + this.ctx.save(); + + for (var i = layers.length - 1; i >= 0; i--) { + var layer = layers[i]; + const nextLayer = layers[i - 1]; + + // Skip the layer if not needed to be rendered + if (shouldSkip && shouldSkip(layer)) { + continue; + } + + // If the layer or next layer has clip masking effect (source-atop). + // If there are such layers, this will make sure that layers will be rendered + // in an isolated temporary canvas + if ( + layer.composition === "source-atop" || + (nextLayer && nextLayer.composition === "source-atop") + ) { + hasSourceAtopLayer = true; + // Apply the effect in a isolated temporary canvas + tempCtx.globalAlpha = layer.opacity / 100; + tempCtx.globalCompositeOperation = layer.composition; + + // If the next layer has the clip masking effect then + // isolated the shadow filter from temporary canvas and keep that in the original canvas + if (nextLayer?.composition === "source-atop") { + // Render the layer + this.render_object(ctx, layer); + // Then remove the shadow (if it exists) from the render process in the temporary canvas + const filters = layer.filters.filter((filter) => { + return filter.name !== "shadow"; + }); + this.render_object(tempCtx, { ...layer, filters }); + } else { + this.render_object(tempCtx, layer); + } + } else { + ctx.globalAlpha = layer.opacity / 100; + ctx.globalCompositeOperation = layer.composition; + this.render_object(ctx, layer); + } + } + + if (hasSourceAtopLayer) { + ctx.restore(); + ctx.drawImage(tempCanvas, 0, 0); + } + } + render_preview(layers) { var w = this.Base_gui.GUI_preview.PREVIEW_SIZE.w; var h = this.Base_gui.GUI_preview.PREVIEW_SIZE.h; @@ -278,42 +313,9 @@ class Base_layers_class { this.ctx_preview.scale(w / config.WIDTH, h / config.HEIGHT); const newCanvas = this.createNewCanvas(this.ctx_preview); - const ctxForSourceAtop = newCanvas.getContext("2d"); - ctxForSourceAtop.scale(w / config.WIDTH, h / config.HEIGHT); - let hasSourceAtopLayer = false; - - for (var i = layers.length - 1; i >= 0; i--) { - const value = layers[i]; - const nextValue = layers[i - 1]; - - if (value.visible == false) { - //not visible - continue; - } - if (value.type == null) { - //empty type - continue; - } - - if ( - value.composition === "source-atop" || - (nextValue && nextValue.composition === "source-atop") - ) { - hasSourceAtopLayer = true; - ctxForSourceAtop.globalAlpha = value.opacity / 100; - ctxForSourceAtop.globalCompositeOperation = value.composition; - this.render_object(ctxForSourceAtop, value); - } else { - this.ctx_preview.globalAlpha = value.opacity / 100; - this.ctx_preview.globalCompositeOperation = value.composition; - this.render_object(this.ctx_preview, value); - } - } - - if (hasSourceAtopLayer) { - this.ctx_preview.restore(); - this.ctx_preview.drawImage(newCanvas, 0, 0); - } + this.renderObjects(this.ctx_preview, newCanvas, layers, (tempCtx) => { + tempCtx.scale(w / config.WIDTH, h / config.HEIGHT); + }); this.ctx_preview.restore(); this.Base_gui.GUI_preview.render_preview_active_zone(); @@ -701,22 +703,20 @@ class Base_layers_class { * @param {boolean} is_preview Optional */ convert_layers_to_canvas(ctx, layer_id = null, is_preview = true) { - var layers_sorted = this.get_sorted_layers(); - for (var i = layers_sorted.length - 1; i >= 0; i--) { - var value = layers_sorted[i]; - + const newCanvas = this.createNewCanvas( + ctx, + ctx.canvas.width, + ctx.canvas.height + ); + const layers_sorted = this.get_sorted_layers(); + this.renderObjects(ctx, newCanvas, layers_sorted, null, (value) => { if (value.visible == false || value.type == null) { - continue; + return true; } if (layer_id != null && value.id != layer_id) { - continue; + return true; } - - ctx.globalAlpha = value.opacity / 100; - ctx.globalCompositeOperation = value.composition; - - this.render_object(ctx, value, is_preview); - } + }); } /** * exports (active) layer to canvas for saving From ee53f86e833ac34ecdbce1ae95ae435db9148e68 Mon Sep 17 00:00:00 2001 From: Karlen Manaseryan Date: Sun, 28 Nov 2021 22:01:17 +0400 Subject: [PATCH 2/2] Issue #192 - formatting --- src/js/core/base-layers.js | 1712 +++++++++++++++++------------------- 1 file changed, 802 insertions(+), 910 deletions(-) diff --git a/src/js/core/base-layers.js b/src/js/core/base-layers.js index 437926a..6faa11d 100644 --- a/src/js/core/base-layers.js +++ b/src/js/core/base-layers.js @@ -43,916 +43,808 @@ var instance = null; * - render_function (function) */ class Base_layers_class { - constructor() { - //singleton - if (instance) { - return instance; - } - instance = this; - - this.Base_gui = new Base_gui_class(); - this.Helper = new Helper_class(); - this.Image_trim = new Image_trim_class(); - this.View_ruler = new View_ruler_class(); - - this.canvas = document.getElementById("canvas_minipaint"); - this.ctx = document - .getElementById("canvas_minipaint") - .getContext("2d"); - this.ctx_preview = document - .getElementById("canvas_preview") - .getContext("2d"); - this.last_zoom = 1; - this.auto_increment = 1; - this.stable_dimensions = []; - this.debug_rendering = false; - this.render_success = null; - this.disabled_filter_id = null; - } - - /** - * do preparation on start - */ - init() { - this.init_zoom_lib(); - - new app.Actions.Insert_layer_action({}).do(); - - var sel_config = { - enable_background: false, - enable_borders: true, - enable_controls: false, - enable_rotation: false, - enable_move: false, - data_function: function () { - return config.layer; - }, - }; - this.Base_selection = new Base_selection_class( - this.ctx, - sel_config, - "main" - ); - - this.render(true); - } - - init_zoom_lib() { - zoomView.setBounds(0, 0, config.WIDTH, config.HEIGHT); - zoomView.setContext(this.ctx); - this.stable_dimensions = [config.WIDTH, config.HEIGHT]; - } - - pre_render() { - this.ctx.save(); - zoomView.canvasDefault(); - this.ctx.clearRect( - 0, - 0, - config.WIDTH * config.ZOOM, - config.HEIGHT * config.ZOOM - ); - } - - after_render() { - config.need_render = false; - config.need_render_changed_params = false; - this.ctx.restore(); - zoomView.canvasDefault(); - } - - /** - * renders all layers objects on main canvas - * - * @param {bool} force - */ - render(force) { - var _this = this; - if (force !== true) { - //request render and exit - config.need_render = true; - return; - } - - if ( - this.stable_dimensions[0] != config.WIDTH || - this.stable_dimensions[1] != config.HEIGHT - ) { - //dimensions changed - re-init zoom lib - this.init_zoom_lib(); - } - - if (config.need_render == true) { - this.render_success = null; - - if (this.debug_rendering === true) { - console.log("Rendering..."); - } - - if (this.last_zoom != config.ZOOM) { - //change zoom - zoomView.scaleAt( - this.Base_gui.GUI_preview.zoom_data.x, - this.Base_gui.GUI_preview.zoom_data.y, - config.ZOOM / this.last_zoom - ); - } else if ( - this.Base_gui.GUI_preview.zoom_data.move_pos != - null - ) { - //move visible window - var pos = - this.Base_gui.GUI_preview.zoom_data - .move_pos; - var pos_global = zoomView.toScreen(pos); - zoomView.move(-pos_global.x, -pos_global.y); - this.Base_gui.GUI_preview.zoom_data.move_pos = - null; - } - - //prepare - this.pre_render(); - - //take data - var layers_sorted = this.get_sorted_layers(); - - zoomView.apply(); - - const newCanvas = this.createNewCanvas( - this.ctx, - config.HEIGHT, - config.WIDTH - ); - - this.renderObjects(this.ctx, newCanvas, layers_sorted); - - //grid - this.Base_gui.draw_grid(this.ctx); - - //guides - this.Base_gui.draw_guides(this.ctx); - - //render selected object controls - this.Base_selection.draw_selection(); - - //active tool overlay - this.render_overlay(); - - //render preview - this.render_preview(layers_sorted); - - //reset - this.after_render(); - - this.last_zoom = config.ZOOM; - - this.Base_gui.GUI_details.render_details(); - this.View_ruler.render_ruler(); - - if (this.render_success === false) { - alertify.error("Rendered with errors."); - } - } - - requestAnimationFrame(function () { - _this.render(force); - }); - } - - render_overlay() { - var render_class = config.TOOL.name; - var render_function = "render_overlay"; - - if ( - typeof this.Base_gui.GUI_tools.tools_modules[ - render_class - ].object[render_function] != "undefined" - ) { - this.Base_gui.GUI_tools.tools_modules[ - render_class - ].object[render_function](this.ctx); - } - } - - /** - * Creates a fresh new canvas with the same height and width as the provided one - * @param {canvas.context} ctx - * @param {number} [h] - * @param {number} [w] - */ - createNewCanvas(ctx, h, w) { - const newCanvas = document.createElement("canvas"); - newCanvas.height = h || ctx.canvas.height; - newCanvas.width = w || ctx.canvas.width; - return newCanvas; - } - - /** - * Renders objects based on the provided layers - * @param {canvas.context} ctx - Main canvas context where it needs to be rendered - * @param {canvas} tempCanvas - A temporary canvas which is a copy of the original canvas, but will be used if there will be needed to isolate an effect from others - * @param {Object[]} layers - Array of layers - * @param {Function} prepare - An optional function to prepare temporary canvas before the render if needed - * @param {Function} shouldSkip - An optional boolean function for skipping those layers which are not needed to be rendered - */ - renderObjects(ctx, tempCanvas, layers, prepare, shouldSkip) { - const tempCtx = tempCanvas.getContext("2d"); - let hasSourceAtopLayer = false; - - // Prepare the temporary canvas if needed - prepare && prepare(tempCtx); - this.ctx.save(); - - for (var i = layers.length - 1; i >= 0; i--) { - var layer = layers[i]; - const nextLayer = layers[i - 1]; - - // Skip the layer if not needed to be rendered - if (shouldSkip && shouldSkip(layer)) { - continue; - } - - // If the layer or next layer has clip masking effect (source-atop). - // If there are such layers, this will make sure that layers will be rendered - // in an isolated temporary canvas - if ( - layer.composition === "source-atop" || - (nextLayer && - nextLayer.composition === "source-atop") - ) { - hasSourceAtopLayer = true; - // Apply the effect in a isolated temporary canvas - tempCtx.globalAlpha = layer.opacity / 100; - tempCtx.globalCompositeOperation = - layer.composition; - - // If the next layer has the clip masking effect then - // isolated the shadow filter from temporary canvas and keep that in the original canvas - if (nextLayer?.composition === "source-atop") { - // Render the layer - this.render_object(ctx, layer); - // Then remove the shadow (if it exists) from the render process in the temporary canvas - const filters = layer.filters.filter( - (filter) => { - return ( - filter.name !== - "shadow" - ); - } - ); - this.render_object(tempCtx, { - ...layer, - filters, - }); - } else { - this.render_object(tempCtx, layer); - } - } else { - ctx.globalAlpha = layer.opacity / 100; - ctx.globalCompositeOperation = - layer.composition; - this.render_object(ctx, layer); - } - } - - if (hasSourceAtopLayer) { - ctx.restore(); - ctx.drawImage(tempCanvas, 0, 0); - } - } - - render_preview(layers) { - var w = this.Base_gui.GUI_preview.PREVIEW_SIZE.w; - var h = this.Base_gui.GUI_preview.PREVIEW_SIZE.h; - - this.ctx_preview.save(); - this.ctx_preview.clearRect(0, 0, w, h); - - //prepare scale - this.ctx_preview.scale(w / config.WIDTH, h / config.HEIGHT); - - const newCanvas = this.createNewCanvas(this.ctx_preview); - this.renderObjects( - this.ctx_preview, - newCanvas, - layers, - (tempCtx) => { - tempCtx.scale( - w / config.WIDTH, - h / config.HEIGHT - ); - } - ); - - this.ctx_preview.restore(); - this.Base_gui.GUI_preview.render_preview_active_zone(); - } - - /** - * export current layers to given canvas - * - * @param {canvas.context} ctx - * @param {object} object - * @param {boolean} is_preview - */ - render_object(ctx, object, is_preview) { - if (object.visible == false || object.type == null) return; - - this.pre_render_object(ctx, object); - - //example with canvas object - other types should overwrite this method - if (object.type == "image") { - //image - default behavior - ctx.save(); - - ctx.translate( - object.x + object.width / 2, - object.y + object.height / 2 - ); - ctx.rotate((object.rotate * Math.PI) / 180); - // TODO - Not sure why the check should be with null, - // if nothing will break, then better to check if it's just truthy - ctx.drawImage( - object.link_canvas != null - ? object.link_canvas - : object.link, - -object.width / 2, - -object.height / 2, - object.width, - object.height - ); - - ctx.restore(); - } else { - //call render function from other module - var render_class = object.render_function[0]; - var render_function = object.render_function[1]; - if ( - typeof this.Base_gui.GUI_tools.tools_modules[ - render_class - ] != "undefined" - ) { - this.Base_gui.GUI_tools.tools_modules[ - render_class - ].object[render_function]( - ctx, - object, - is_preview - ); - } else { - this.render_success = false; - console.log( - "Error: unknown layer type: " + - object.type - ); - } - } - - this.after_render_object(ctx, object); - } - - /** - * Gets called before render_object starts it's job - * @param {canvas.context} ctx - * @param {object} object - */ - pre_render_object(ctx, object) { - //apply pre-filters - for (var i in object.filters) { - var filter = object.filters[i]; - if (filter.id == this.disabled_filter_id) { - continue; - } - - filter.name = filter.name.replace( - "drop-shadow", - "shadow" - ); - - //find filter - var found = false; - for (var i in this.Base_gui.modules) { - if ( - i.indexOf("effects") == -1 || - i.indexOf("abstract") > -1 - ) - continue; - - var filter_class = this.Base_gui.modules[i]; - var module_name = i.split("/").pop(); - if (module_name == filter.name) { - //found it - found = true; - filter_class.render_pre( - ctx, - filter, - object - ); - } - } - if (found == false) { - this.render_success = false; - console.log( - "Error: can not find filter: " + - filter.name - ); - } - } - } - - /** - * Gets called after when render_object finishes it's job - * @param {canvas.context} ctx - * @param {object} object - */ - after_render_object(ctx, object) { - //apply post-filters - for (var i in object.filters) { - var filter = object.filters[i]; - if (filter.id == this.disabled_filter_id) { - continue; - } - filter.name = filter.name.replace( - "drop-shadow", - "shadow" - ); - - //find filter - var found = false; - for (var i in this.Base_gui.modules) { - if ( - i.indexOf("effects") == -1 || - i.indexOf("abstract") > -1 - ) - continue; - - var filter_class = this.Base_gui.modules[i]; - var module_name = i.split("/").pop(); - if (module_name == filter.name) { - //found it - found = true; - filter_class.render_post( - ctx, - filter, - object - ); - } - } - if (found == false) { - this.render_success = false; - console.log( - "Error: can not find filter: " + - filter.name - ); - } - } - } - - /** - * creates new layer - * - * @param {array} settings - * @param {boolean} can_automate - */ - async insert(settings, can_automate = true) { - return app.State.do_action( - new app.Actions.Insert_layer_action( - settings, - can_automate - ) - ); - } - - /** - * autoresize layer, based on dimensions, up - always, if 1 layer - down. - * - * @param {int} width - * @param {int} height - * @param {int} layer_id - * @param {boolean} can_automate - */ - async autoresize(width, height, layer_id, can_automate = true) { - return app.State.do_action( - new app.Actions.Autoresize_canvas_action( - width, - height, - layer_id, - can_automate - ) - ); - } - - /** - * returns layer - * - * @param {int} id - * @returns {object} - */ - get_layer(id) { - if (id == undefined) { - id = config.layer.id; - } - for (var i in config.layers) { - if (config.layers[i].id == id) { - return config.layers[i]; - } - } - alertify.error("Error: can not find layer with id:" + id); - return null; - } - - /** - * removes layer - * - * @param {int} id - * @param {boolean} force - Force to delete first layer? - */ - async delete(id, force) { - return app.State.do_action( - new app.Actions.Delete_layer_action(id, force) - ); - } - - /* - * removes all layers - */ - async reset_layers(auto_insert) { - return app.State.do_action( - new app.Actions.Reset_layers_action(auto_insert) - ); - } - - /** - * toggle layer visibility - * - * @param {int} id - */ - async toggle_visibility(id) { - return app.State.do_action( - new app.Actions.Toggle_layer_visibility_action(id) - ); - } - - /* - * renew layers HTML - */ - refresh_gui() { - this.Base_gui.GUI_layers.render_layers(); - } - - /** - * marks layer as selected, active - * - * @param {int} id - */ - async select(id) { - return app.State.do_action( - new app.Actions.Select_layer_action(id) - ); - } - - /** - * change layer opacity - * - * @param {int} id - * @param {int} value 0-100 - */ - async set_opacity(id, value) { - value = parseInt(value); - if (value < 0 || value > 100) { - //reset - value = 100; - } - return app.State.do_action( - new app.Actions.Update_layer_action(id, { - opacity: value, - }) - ); - } - - /** - * clear layer data - * - * @param {int} id - */ - async layer_clear(id) { - return app.State.do_action( - new app.Actions.Clear_layer_action(id) - ); - } - - /** - * move layer up or down - * - * @param {int} id - * @param {int} direction - */ - async move(id, direction) { - return app.State.do_action( - new app.Actions.Reorder_layer_action(id, direction) - ); - } - - /** - * clone and sort. - */ - get_sorted_layers() { - return config.layers.concat().sort( - //sort function - (a, b) => b.order - a.order - ); - } - - /** - * checks if layer empty - * - * @param {int} id - * @returns {Boolean} - */ - is_layer_empty(id) { - var link = this.get_layer(id); - - if ( - (link.width == 0 || link.width === null) && - (link.height == 0 || link.height === null) && - link.data == null - ) { - return true; - } - - return false; - } - - /** - * find next layer - * - * @param {int} id layer id - * @returns {layer|null} - */ - find_next(id) { - id = parseInt(id); - var link = this.get_layer(id); - var layers_sorted = this.get_sorted_layers(); - - var last = null; - for (var i = layers_sorted.length - 1; i >= 0; i--) { - var value = layers_sorted[i]; - - if (last != null && last.id == link.id) { - return value; - } - last = value; - } - - return null; - } - - /** - * find previous layer - * - * @param {int} id layer id - * @returns {layer|null} - */ - find_previous(id) { - id = parseInt(id); - var link = this.get_layer(id); - var layers_sorted = this.get_sorted_layers(); - - var last = null; - for (var i in layers_sorted) { - var value = layers_sorted[i]; - - if (last != null && last.id == link.id) { - return value; - } - last = value; - } - - return null; - } - - /** - * returns global position, for example if canvas is zoomed, it will convert relative mouse position to absolute - * at 100% zoom. - * - * @param {int} x - * @param {int} y - * @returns {object} keys: x, y - */ - get_world_coords(x, y) { - return zoomView.toWorld(x, y); - } - - /** - * register new live filter - * - * @param {int} layer_id - * @param {string} name - * @param {object} params - */ - add_filter(layer_id, name, params) { - return app.State.do_action( - new app.Actions.Add_layer_filter_action( - layer_id, - name, - params - ) - ); - } - - /** - * delete live filter - * - * @param {int} layer_id - * @param {string} filter_id - */ - delete_filter(layer_id, filter_id) { - return app.State.do_action( - new app.Actions.Delete_layer_filter_action( - layer_id, - filter_id - ) - ); - } - - /** - * exports all layers to canvas for saving - * - * @param {canvas.context} ctx - * @param {int} layer_id Optional - * @param {boolean} is_preview Optional - */ - convert_layers_to_canvas(ctx, layer_id = null, is_preview = true) { - const newCanvas = this.createNewCanvas( - ctx, - ctx.canvas.width, - ctx.canvas.height - ); - const layers_sorted = this.get_sorted_layers(); - this.renderObjects( - ctx, - newCanvas, - layers_sorted, - null, - (value) => { - if ( - value.visible == false || - value.type == null - ) { - return true; - } - if (layer_id != null && value.id != layer_id) { - return true; - } - } - ); - } - /** - * exports (active) layer to canvas for saving - * - * @param {int} layer_id or current layer by default - * @param {boolean} actual_area used for resized image. Default is false. - * @param {boolean} can_trim default is true - * @returns {canvas} - */ - convert_layer_to_canvas(layer_id, actual_area = false, can_trim) { - if (actual_area == null) actual_area = false; - if (layer_id == null) layer_id = config.layer.id; - var link = this.get_layer(layer_id); - var offset_x = 0; - var offset_y = 0; - - //create tmp canvas - var canvas = document.createElement("canvas"); - if (actual_area === true && link.type == "image") { - canvas.width = link.width_original; - canvas.height = link.height_original; - can_trim = false; - } else { - canvas.width = Math.max(link.width, config.WIDTH); - canvas.height = Math.max(link.height, config.HEIGHT); - } - - //add data - if (actual_area === true && link.type == "image") { - canvas.getContext("2d").drawImage(link.link, 0, 0); - } else { - this.render_object(canvas.getContext("2d"), link); - } - - //trim - if ( - (can_trim == true || can_trim == undefined) && - link.type != null - ) { - var trim_info = this.Image_trim.get_trim_info(layer_id); - if ( - trim_info.left > 0 || - trim_info.top > 0 || - trim_info.right > 0 || - trim_info.bottom > 0 - ) { - offset_x = trim_info.left; - offset_y = trim_info.top; - - var w = - canvas.width - - trim_info.left - - trim_info.right; - var h = - canvas.height - - trim_info.top - - trim_info.bottom; - if (w > 1 && h > 1) { - this.Helper.change_canvas_size( - canvas, - w, - h, - offset_x, - offset_y - ); - } - } - } - - canvas.dataset.x = offset_x; - canvas.dataset.y = offset_y; - - return canvas; - } - - /** - * updates layer image data - * - * @param {canvas} canvas - * @param {int} layer_id (optional) - */ - update_layer_image(canvas, layer_id) { - return app.State.do_action( - new app.Actions.Update_layer_image_action( - canvas, - layer_id - ) - ); - } - - /** - * returns canvas dimensions. - * - * @returns {object} - */ - get_dimensions() { - return { - width: config.WIDTH, - height: config.HEIGHT, - }; - } - - /** - * returns all layers - * - * @returns {array} - */ - get_layers() { - return config.layers; - } - - /** - * disabled filter by id - * - * @param filter_id - */ - disable_filter(filter_id) { - this.disabled_filter_id = filter_id; - } - - /** - * finds layer filter by filter ID - * - * @param filter_id - * @param filter_name - * @param layer_id - * @returns {object} - */ - find_filter_by_id(filter_id, filter_name, layer_id) { - if (typeof layer_id == "undefined") { - var layer = config.layer; - } else { - var layer = this.get_layer(layer_id); - } - - var filter = {}; - for (var i in layer.filters) { - if ( - layer.filters[i].name == filter_name && - layer.filters[i].id == filter_id - ) { - return layer.filters[i].params; - } - } - - return filter; - } + constructor() { + //singleton + if (instance) { + return instance; + } + instance = this; + + this.Base_gui = new Base_gui_class(); + this.Helper = new Helper_class(); + this.Image_trim = new Image_trim_class(); + this.View_ruler = new View_ruler_class(); + + this.canvas = document.getElementById("canvas_minipaint"); + this.ctx = document.getElementById("canvas_minipaint").getContext("2d"); + this.ctx_preview = document + .getElementById("canvas_preview") + .getContext("2d"); + this.last_zoom = 1; + this.auto_increment = 1; + this.stable_dimensions = []; + this.debug_rendering = false; + this.render_success = null; + this.disabled_filter_id = null; + } + + /** + * do preparation on start + */ + init() { + this.init_zoom_lib(); + + new app.Actions.Insert_layer_action({}).do(); + + var sel_config = { + enable_background: false, + enable_borders: true, + enable_controls: false, + enable_rotation: false, + enable_move: false, + data_function: function () { + return config.layer; + }, + }; + this.Base_selection = new Base_selection_class( + this.ctx, + sel_config, + "main" + ); + + this.render(true); + } + + init_zoom_lib() { + zoomView.setBounds(0, 0, config.WIDTH, config.HEIGHT); + zoomView.setContext(this.ctx); + this.stable_dimensions = [config.WIDTH, config.HEIGHT]; + } + + pre_render() { + this.ctx.save(); + zoomView.canvasDefault(); + this.ctx.clearRect( + 0, + 0, + config.WIDTH * config.ZOOM, + config.HEIGHT * config.ZOOM + ); + } + + after_render() { + config.need_render = false; + config.need_render_changed_params = false; + this.ctx.restore(); + zoomView.canvasDefault(); + } + + /** + * renders all layers objects on main canvas + * + * @param {bool} force + */ + render(force) { + var _this = this; + if (force !== true) { + //request render and exit + config.need_render = true; + return; + } + + if ( + this.stable_dimensions[0] != config.WIDTH || + this.stable_dimensions[1] != config.HEIGHT + ) { + //dimensions changed - re-init zoom lib + this.init_zoom_lib(); + } + + if (config.need_render == true) { + this.render_success = null; + + if (this.debug_rendering === true) { + console.log("Rendering..."); + } + + if (this.last_zoom != config.ZOOM) { + //change zoom + zoomView.scaleAt( + this.Base_gui.GUI_preview.zoom_data.x, + this.Base_gui.GUI_preview.zoom_data.y, + config.ZOOM / this.last_zoom + ); + } else if (this.Base_gui.GUI_preview.zoom_data.move_pos != null) { + //move visible window + var pos = this.Base_gui.GUI_preview.zoom_data.move_pos; + var pos_global = zoomView.toScreen(pos); + zoomView.move(-pos_global.x, -pos_global.y); + this.Base_gui.GUI_preview.zoom_data.move_pos = null; + } + + //prepare + this.pre_render(); + + //take data + var layers_sorted = this.get_sorted_layers(); + + zoomView.apply(); + + const newCanvas = this.createNewCanvas( + this.ctx, + config.HEIGHT, + config.WIDTH + ); + + this.renderObjects(this.ctx, newCanvas, layers_sorted); + + //grid + this.Base_gui.draw_grid(this.ctx); + + //guides + this.Base_gui.draw_guides(this.ctx); + + //render selected object controls + this.Base_selection.draw_selection(); + + //active tool overlay + this.render_overlay(); + + //render preview + this.render_preview(layers_sorted); + + //reset + this.after_render(); + + this.last_zoom = config.ZOOM; + + this.Base_gui.GUI_details.render_details(); + this.View_ruler.render_ruler(); + + if (this.render_success === false) { + alertify.error("Rendered with errors."); + } + } + + requestAnimationFrame(function () { + _this.render(force); + }); + } + + render_overlay() { + var render_class = config.TOOL.name; + var render_function = "render_overlay"; + + if ( + typeof this.Base_gui.GUI_tools.tools_modules[render_class].object[ + render_function + ] != "undefined" + ) { + this.Base_gui.GUI_tools.tools_modules[render_class].object[ + render_function + ](this.ctx); + } + } + + /** + * Creates a fresh new canvas with the same height and width as the provided one + * @param {canvas.context} ctx + * @param {number} [h] + * @param {number} [w] + */ + createNewCanvas(ctx, h, w) { + const newCanvas = document.createElement("canvas"); + newCanvas.height = h || ctx.canvas.height; + newCanvas.width = w || ctx.canvas.width; + return newCanvas; + } + + /** + * Renders objects based on the provided layers + * @param {canvas.context} ctx - Main canvas context where it needs to be rendered + * @param {canvas} tempCanvas - A temporary canvas which is a copy of the original canvas, but will be used if there will be needed to isolate an effect from others + * @param {Object[]} layers - Array of layers + * @param {Function} prepare - An optional function to prepare temporary canvas before the render if needed + * @param {Function} shouldSkip - An optional boolean function for skipping those layers which are not needed to be rendered + */ + renderObjects(ctx, tempCanvas, layers, prepare, shouldSkip) { + const tempCtx = tempCanvas.getContext("2d"); + let hasSourceAtopLayer = false; + + // Prepare the temporary canvas if needed + prepare && prepare(tempCtx); + this.ctx.save(); + + for (var i = layers.length - 1; i >= 0; i--) { + var layer = layers[i]; + const nextLayer = layers[i - 1]; + + // Skip the layer if not needed to be rendered + if (shouldSkip && shouldSkip(layer)) { + continue; + } + + // If the layer or next layer has clip masking effect (source-atop). + // If there are such layers, this will make sure that layers will be rendered + // in an isolated temporary canvas + if ( + layer.composition === "source-atop" || + (nextLayer && nextLayer.composition === "source-atop") + ) { + hasSourceAtopLayer = true; + // Apply the effect in a isolated temporary canvas + tempCtx.globalAlpha = layer.opacity / 100; + tempCtx.globalCompositeOperation = layer.composition; + + // If the next layer has the clip masking effect then + // isolated the shadow filter from temporary canvas and keep that in the original canvas + if (nextLayer?.composition === "source-atop") { + // Render the layer + this.render_object(ctx, layer); + // Then remove the shadow (if it exists) from the render process in the temporary canvas + const filters = layer.filters.filter((filter) => { + return filter.name !== "shadow"; + }); + this.render_object(tempCtx, { + ...layer, + filters, + }); + } else { + this.render_object(tempCtx, layer); + } + } else { + ctx.globalAlpha = layer.opacity / 100; + ctx.globalCompositeOperation = layer.composition; + this.render_object(ctx, layer); + } + } + + if (hasSourceAtopLayer) { + ctx.restore(); + ctx.drawImage(tempCanvas, 0, 0); + } + } + + render_preview(layers) { + var w = this.Base_gui.GUI_preview.PREVIEW_SIZE.w; + var h = this.Base_gui.GUI_preview.PREVIEW_SIZE.h; + + this.ctx_preview.save(); + this.ctx_preview.clearRect(0, 0, w, h); + + //prepare scale + this.ctx_preview.scale(w / config.WIDTH, h / config.HEIGHT); + + const newCanvas = this.createNewCanvas(this.ctx_preview); + this.renderObjects(this.ctx_preview, newCanvas, layers, (tempCtx) => { + tempCtx.scale(w / config.WIDTH, h / config.HEIGHT); + }); + + this.ctx_preview.restore(); + this.Base_gui.GUI_preview.render_preview_active_zone(); + } + + /** + * export current layers to given canvas + * + * @param {canvas.context} ctx + * @param {object} object + * @param {boolean} is_preview + */ + render_object(ctx, object, is_preview) { + if (object.visible == false || object.type == null) return; + + this.pre_render_object(ctx, object); + + //example with canvas object - other types should overwrite this method + if (object.type == "image") { + //image - default behavior + ctx.save(); + + ctx.translate(object.x + object.width / 2, object.y + object.height / 2); + ctx.rotate((object.rotate * Math.PI) / 180); + // TODO - Not sure why the check should be with null, + // if nothing will break, then better to check if it's just truthy + ctx.drawImage( + object.link_canvas != null ? object.link_canvas : object.link, + -object.width / 2, + -object.height / 2, + object.width, + object.height + ); + + ctx.restore(); + } else { + //call render function from other module + var render_class = object.render_function[0]; + var render_function = object.render_function[1]; + if ( + typeof this.Base_gui.GUI_tools.tools_modules[render_class] != + "undefined" + ) { + this.Base_gui.GUI_tools.tools_modules[render_class].object[ + render_function + ](ctx, object, is_preview); + } else { + this.render_success = false; + console.log("Error: unknown layer type: " + object.type); + } + } + + this.after_render_object(ctx, object); + } + + /** + * Gets called before render_object starts it's job + * @param {canvas.context} ctx + * @param {object} object + */ + pre_render_object(ctx, object) { + //apply pre-filters + for (var i in object.filters) { + var filter = object.filters[i]; + if (filter.id == this.disabled_filter_id) { + continue; + } + + filter.name = filter.name.replace("drop-shadow", "shadow"); + + //find filter + var found = false; + for (var i in this.Base_gui.modules) { + if (i.indexOf("effects") == -1 || i.indexOf("abstract") > -1) continue; + + var filter_class = this.Base_gui.modules[i]; + var module_name = i.split("/").pop(); + if (module_name == filter.name) { + //found it + found = true; + filter_class.render_pre(ctx, filter, object); + } + } + if (found == false) { + this.render_success = false; + console.log("Error: can not find filter: " + filter.name); + } + } + } + + /** + * Gets called after when render_object finishes it's job + * @param {canvas.context} ctx + * @param {object} object + */ + after_render_object(ctx, object) { + //apply post-filters + for (var i in object.filters) { + var filter = object.filters[i]; + if (filter.id == this.disabled_filter_id) { + continue; + } + filter.name = filter.name.replace("drop-shadow", "shadow"); + + //find filter + var found = false; + for (var i in this.Base_gui.modules) { + if (i.indexOf("effects") == -1 || i.indexOf("abstract") > -1) continue; + + var filter_class = this.Base_gui.modules[i]; + var module_name = i.split("/").pop(); + if (module_name == filter.name) { + //found it + found = true; + filter_class.render_post(ctx, filter, object); + } + } + if (found == false) { + this.render_success = false; + console.log("Error: can not find filter: " + filter.name); + } + } + } + + /** + * creates new layer + * + * @param {array} settings + * @param {boolean} can_automate + */ + async insert(settings, can_automate = true) { + return app.State.do_action( + new app.Actions.Insert_layer_action(settings, can_automate) + ); + } + + /** + * autoresize layer, based on dimensions, up - always, if 1 layer - down. + * + * @param {int} width + * @param {int} height + * @param {int} layer_id + * @param {boolean} can_automate + */ + async autoresize(width, height, layer_id, can_automate = true) { + return app.State.do_action( + new app.Actions.Autoresize_canvas_action( + width, + height, + layer_id, + can_automate + ) + ); + } + + /** + * returns layer + * + * @param {int} id + * @returns {object} + */ + get_layer(id) { + if (id == undefined) { + id = config.layer.id; + } + for (var i in config.layers) { + if (config.layers[i].id == id) { + return config.layers[i]; + } + } + alertify.error("Error: can not find layer with id:" + id); + return null; + } + + /** + * removes layer + * + * @param {int} id + * @param {boolean} force - Force to delete first layer? + */ + async delete(id, force) { + return app.State.do_action(new app.Actions.Delete_layer_action(id, force)); + } + + /* + * removes all layers + */ + async reset_layers(auto_insert) { + return app.State.do_action( + new app.Actions.Reset_layers_action(auto_insert) + ); + } + + /** + * toggle layer visibility + * + * @param {int} id + */ + async toggle_visibility(id) { + return app.State.do_action( + new app.Actions.Toggle_layer_visibility_action(id) + ); + } + + /* + * renew layers HTML + */ + refresh_gui() { + this.Base_gui.GUI_layers.render_layers(); + } + + /** + * marks layer as selected, active + * + * @param {int} id + */ + async select(id) { + return app.State.do_action(new app.Actions.Select_layer_action(id)); + } + + /** + * change layer opacity + * + * @param {int} id + * @param {int} value 0-100 + */ + async set_opacity(id, value) { + value = parseInt(value); + if (value < 0 || value > 100) { + //reset + value = 100; + } + return app.State.do_action( + new app.Actions.Update_layer_action(id, { + opacity: value, + }) + ); + } + + /** + * clear layer data + * + * @param {int} id + */ + async layer_clear(id) { + return app.State.do_action(new app.Actions.Clear_layer_action(id)); + } + + /** + * move layer up or down + * + * @param {int} id + * @param {int} direction + */ + async move(id, direction) { + return app.State.do_action( + new app.Actions.Reorder_layer_action(id, direction) + ); + } + + /** + * clone and sort. + */ + get_sorted_layers() { + return config.layers.concat().sort( + //sort function + (a, b) => b.order - a.order + ); + } + + /** + * checks if layer empty + * + * @param {int} id + * @returns {Boolean} + */ + is_layer_empty(id) { + var link = this.get_layer(id); + + if ( + (link.width == 0 || link.width === null) && + (link.height == 0 || link.height === null) && + link.data == null + ) { + return true; + } + + return false; + } + + /** + * find next layer + * + * @param {int} id layer id + * @returns {layer|null} + */ + find_next(id) { + id = parseInt(id); + var link = this.get_layer(id); + var layers_sorted = this.get_sorted_layers(); + + var last = null; + for (var i = layers_sorted.length - 1; i >= 0; i--) { + var value = layers_sorted[i]; + + if (last != null && last.id == link.id) { + return value; + } + last = value; + } + + return null; + } + + /** + * find previous layer + * + * @param {int} id layer id + * @returns {layer|null} + */ + find_previous(id) { + id = parseInt(id); + var link = this.get_layer(id); + var layers_sorted = this.get_sorted_layers(); + + var last = null; + for (var i in layers_sorted) { + var value = layers_sorted[i]; + + if (last != null && last.id == link.id) { + return value; + } + last = value; + } + + return null; + } + + /** + * returns global position, for example if canvas is zoomed, it will convert relative mouse position to absolute + * at 100% zoom. + * + * @param {int} x + * @param {int} y + * @returns {object} keys: x, y + */ + get_world_coords(x, y) { + return zoomView.toWorld(x, y); + } + + /** + * register new live filter + * + * @param {int} layer_id + * @param {string} name + * @param {object} params + */ + add_filter(layer_id, name, params) { + return app.State.do_action( + new app.Actions.Add_layer_filter_action(layer_id, name, params) + ); + } + + /** + * delete live filter + * + * @param {int} layer_id + * @param {string} filter_id + */ + delete_filter(layer_id, filter_id) { + return app.State.do_action( + new app.Actions.Delete_layer_filter_action(layer_id, filter_id) + ); + } + + /** + * exports all layers to canvas for saving + * + * @param {canvas.context} ctx + * @param {int} layer_id Optional + * @param {boolean} is_preview Optional + */ + convert_layers_to_canvas(ctx, layer_id = null, is_preview = true) { + const newCanvas = this.createNewCanvas( + ctx, + ctx.canvas.width, + ctx.canvas.height + ); + const layers_sorted = this.get_sorted_layers(); + this.renderObjects(ctx, newCanvas, layers_sorted, null, (value) => { + if (value.visible == false || value.type == null) { + return true; + } + if (layer_id != null && value.id != layer_id) { + return true; + } + }); + } + /** + * exports (active) layer to canvas for saving + * + * @param {int} layer_id or current layer by default + * @param {boolean} actual_area used for resized image. Default is false. + * @param {boolean} can_trim default is true + * @returns {canvas} + */ + convert_layer_to_canvas(layer_id, actual_area = false, can_trim) { + if (actual_area == null) actual_area = false; + if (layer_id == null) layer_id = config.layer.id; + var link = this.get_layer(layer_id); + var offset_x = 0; + var offset_y = 0; + + //create tmp canvas + var canvas = document.createElement("canvas"); + if (actual_area === true && link.type == "image") { + canvas.width = link.width_original; + canvas.height = link.height_original; + can_trim = false; + } else { + canvas.width = Math.max(link.width, config.WIDTH); + canvas.height = Math.max(link.height, config.HEIGHT); + } + + //add data + if (actual_area === true && link.type == "image") { + canvas.getContext("2d").drawImage(link.link, 0, 0); + } else { + this.render_object(canvas.getContext("2d"), link); + } + + //trim + if ((can_trim == true || can_trim == undefined) && link.type != null) { + var trim_info = this.Image_trim.get_trim_info(layer_id); + if ( + trim_info.left > 0 || + trim_info.top > 0 || + trim_info.right > 0 || + trim_info.bottom > 0 + ) { + offset_x = trim_info.left; + offset_y = trim_info.top; + + var w = canvas.width - trim_info.left - trim_info.right; + var h = canvas.height - trim_info.top - trim_info.bottom; + if (w > 1 && h > 1) { + this.Helper.change_canvas_size(canvas, w, h, offset_x, offset_y); + } + } + } + + canvas.dataset.x = offset_x; + canvas.dataset.y = offset_y; + + return canvas; + } + + /** + * updates layer image data + * + * @param {canvas} canvas + * @param {int} layer_id (optional) + */ + update_layer_image(canvas, layer_id) { + return app.State.do_action( + new app.Actions.Update_layer_image_action(canvas, layer_id) + ); + } + + /** + * returns canvas dimensions. + * + * @returns {object} + */ + get_dimensions() { + return { + width: config.WIDTH, + height: config.HEIGHT, + }; + } + + /** + * returns all layers + * + * @returns {array} + */ + get_layers() { + return config.layers; + } + + /** + * disabled filter by id + * + * @param filter_id + */ + disable_filter(filter_id) { + this.disabled_filter_id = filter_id; + } + + /** + * finds layer filter by filter ID + * + * @param filter_id + * @param filter_name + * @param layer_id + * @returns {object} + */ + find_filter_by_id(filter_id, filter_name, layer_id) { + if (typeof layer_id == "undefined") { + var layer = config.layer; + } else { + var layer = this.get_layer(layer_id); + } + + var filter = {}; + for (var i in layer.filters) { + if ( + layer.filters[i].name == filter_name && + layer.filters[i].id == filter_id + ) { + return layer.filters[i].params; + } + } + + return filter; + } } export default Base_layers_class;