diff --git a/src/js/actions/autoresize-canvas.js b/src/js/actions/autoresize-canvas.js new file mode 100644 index 0000000..bd2cd4a --- /dev/null +++ b/src/js/actions/autoresize-canvas.js @@ -0,0 +1,92 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Autoresize_canvas_action extends Base_action { + /** + * autoresize canvas to layer size, based on dimensions, up - always, if 1 layer - down. + * + * @param {int} width + * @param {int} height + * @param {int} layer_id + * @param {boolean} can_automate + */ + constructor(width, height, layer_id, can_automate = true) { + super('autoresize_canvas', 'Auto-resize Canvas'); + this.width = width; + this.height = height; + this.layer_id = layer_id; + this.can_automate = can_automate; + this.old_config_width = null; + this.old_config_height = null; + } + + async do() { + super.do(); + const width = this.width; + const height = this.height; + const can_automate = this.can_automate; + let need_fit = false; + let new_config_width = config.WIDTH; + let new_config_height = config.HEIGHT; + + // Resize up + if (width > new_config_width || height > new_config_height) { + const wrapper = document.getElementById('main_wrapper'); + const page_w = wrapper.clientWidth; + const page_h = wrapper.clientHeight; + + if (width > page_w || height > page_h) { + need_fit = true; + } + if (width > new_config_width) + new_config_width = parseInt(width); + if (height > new_config_height) + new_config_height = parseInt(height); + } + + // Resize down + if (config.layers.length == 1 && can_automate !== false) { + if (width < new_config_width) + new_config_width = parseInt(width); + if (height < new_config_height) + new_config_height = parseInt(height); + } + + if (new_config_width !== config.WIDTH || new_config_height !== height) { + this.old_config_width = config.WIDTH; + this.old_config_height = config.HEIGHT; + config.WIDTH = new_config_width; + config.HEIGHT = new_config_height; + app.GUI.prepare_canvas(); + } else { + throw new Error('Aborted - Resize not necessary') + } + + // Fit zoom when after short pause + // @todo - remove setTimeout + if (need_fit == true) { + await new Promise((resolve) => { + window.setTimeout(() => { + app.GUI.GUI_preview.zoom_auto(); + resolve(); + }, 100); + }); + } + } + + async undo() { + super.undo(); + if (this.old_config_width != null) { + config.WIDTH = this.old_config_width; + } + if (this.old_config_height != null) { + config.HEIGHT = this.old_config_height; + } + if (this.old_config_width != null || this.old_config_height != null) { + app.GUI.prepare_canvas(); + } + this.old_config_width = null; + this.old_config_height = null; + } +} \ No newline at end of file diff --git a/src/js/actions/base.js b/src/js/actions/base.js new file mode 100644 index 0000000..c025caa --- /dev/null +++ b/src/js/actions/base.js @@ -0,0 +1,17 @@ + +export class Base_action { + constructor(action_id, action_description) { + this.action_id = action_id; + this.action_description = action_description; + this.is_done = false; + } + do() { + this.is_done = true; + } + undo() { + this.is_done = false; + } + free() { + // Override if need to run tasks to free memory when action is discarded from history + } +} \ No newline at end of file diff --git a/src/js/actions/bundle.js b/src/js/actions/bundle.js new file mode 100644 index 0000000..9bf57e3 --- /dev/null +++ b/src/js/actions/bundle.js @@ -0,0 +1,46 @@ +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Bundle_action extends Base_action { + /** + * Groups multiple actions together in the undo/redo history, runs them all at once. + */ + constructor(bundle_id, bundle_name, actions_to_do) { + super(bundle_id, bundle_name); + this.actions_to_do = actions_to_do; + } + + async do() { + super.do(); + let error = null; + let i = 0; + for (i = 0; i < this.actions_to_do.length; i++) { + try { + await this.actions_to_do[i].do(); + } catch (e) { + error = e; + break; + } + } + // One of the actions aborted, undo all previous actions. + if (error) { + for (i--; i >= 0; i--) { + await this.actions_to_do[i].undo(); + } + throw error; + } + config.need_render = true; + } + + async undo() { + super.undo(); + for (let i = this.actions_to_do.length - 1; i >= 0; i--) { + await this.actions_to_do[i].undo(); + } + config.need_render = true; + } + + free() { + this.actions_to_do = null; + } +} \ No newline at end of file diff --git a/src/js/actions/delete-layer.js b/src/js/actions/delete-layer.js new file mode 100644 index 0000000..889d4a9 --- /dev/null +++ b/src/js/actions/delete-layer.js @@ -0,0 +1,98 @@ +import config from '../config.js'; +import app from './../app.js'; +import { Base_action } from './base.js'; + +export class Delete_layer_action extends Base_action { + /** + * removes layer + * + * @param {int} id + * @param {boolean} force - Force to delete first layer? + */ + constructor(layer_id, force) { + super('delete_layer', 'Delete Layer'); + this.layer_id = parseInt(layer_id); + this.force = force || false; + this.insert_layer_action = null; + this.select_layer_action = null; + this.delete_index = null; + this.deleted_layer = null; + } + + async do() { + super.do(); + const id = this.layer_id; + const force = this.force; + + // Determine if there is a layer to delete, abort if not + for (var i in config.layers) { + if (config.layers[i].id == id) { + this.delete_index = i; + } + } + if (this.delete_index === null) { + throw new Error('Aborted - Layer to delete not found'); + } + + if (config.layers.length == 1 && (force == undefined || force == false)) { + // Only 1 layer left + if (config.layer.type == null) { + //STOP + throw new Error('Aborted - Will not delete last layer'); + } + else { + // Delete it, but before that - create new empty layer + this.insert_layer_action = new app.Actions.Insert_layer_action(); + this.insert_layer_action.do(); + } + } + + if (config.layers.length > 1 && config.layer.id == id) { + // Select next or previous layer + try { + const select_action = new app.Actions.Select_next_layer_action(id); + await select_action.do(); + this.select_layer_action = select_action; + } catch (error) { + const select_action = new app.Actions.Select_previous_layer_action(id); + await select_action.do(); + this.select_layer_action = select_action; + } + } + + // Remove layer from list + this.deleted_layer = config.layers.splice(this.delete_index, 1)[0]; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + if (this.deleted_layer) { + config.layers.splice(this.delete_index, 0, this.deleted_layer); + this.delete_index = null; + this.deleted_layer = null; + } + if (this.select_layer_action) { + await this.select_layer_action.undo(); + this.select_layer_action = null; + } + if (this.insert_layer_action) { + await this.insert_layer_action.undo(); + this.insert_layer_action = null; + } + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + free() { + if (this.deleted_layer) { + delete this.deleted_layer.link; + } + this.insert_layer_action = null; + this.select_layer_action = null; + this.deleted_layer = null; + } +} \ No newline at end of file diff --git a/src/js/actions/index.js b/src/js/actions/index.js new file mode 100644 index 0000000..30d2bc3 --- /dev/null +++ b/src/js/actions/index.js @@ -0,0 +1,14 @@ +export { Autoresize_canvas_action } from './autoresize-canvas.js'; +export { Bundle_action } from './bundle.js'; +export { Delete_layer_action } from './delete-layer.js'; +export { Init_canvas_zoom_action } from './init-canvas-zoom.js'; +export { Insert_layer_action } from './insert-layer.js'; +export { Prepare_canvas_action } from './prepare-canvas.js'; +export { Reset_layers_action } from './reset-layers.js'; +export { Reset_selection_action } from './reset-selection.js'; +export { Select_layer_action } from './select-layer.js'; +export { Select_next_layer_action } from './select-next-layer.js'; +export { Select_previous_layer_action } from './select-previous-layer.js'; +export { Toggle_layer_visibility_action } from './toggle-layer-visibility.js'; +export { Update_config_action } from './update-config.js'; +export { Update_layer_action } from './update-layer.js'; \ No newline at end of file diff --git a/src/js/actions/init-canvas-zoom.js b/src/js/actions/init-canvas-zoom.js new file mode 100644 index 0000000..cdf75d9 --- /dev/null +++ b/src/js/actions/init-canvas-zoom.js @@ -0,0 +1,45 @@ +import app from '../app.js'; +import config from '../config.js'; +import zoomView from '../libs/zoomView.js'; +import { Base_action } from './base.js'; + +export class Init_canvas_zoom_action extends Base_action { + /** + * Resets the canvas + */ + constructor() { + super('init_canvas_zoom', 'Initialize Canvas Zoom'); + this.old_bounds = null; + this.old_context = null; + this.old_stable_dimensions = null; + } + + async do() { + super.do(); + this.old_bounds = zoomView.getBounds(); + this.old_context = zoomView.getContext(); + this.old_stable_dimensions = app.Layers.stable_dimensions; + zoomView.setBounds(0, 0, config.WIDTH, config.HEIGHT); + zoomView.setContext(app.Layers.ctx); + app.Layers.stable_dimensions = [ + config.WIDTH, + config.HEIGHT + ]; + } + + async undo() { + super.undo(); + zoomView.setBounds(this.old_bounds.top, this.old_bounds.left, this.old_bounds.right, this.old_bounds.bottom); + zoomView.setContext(this.old_context); + app.Layers.stable_dimensions = this.old_stable_dimensions; + this.old_bounds = null; + this.old_context = null; + this.old_stable_dimensions = null; + } + + free() { + this.old_bounds = null; + this.old_context = null; + this.old_stable_dimensions = null; + } +} \ No newline at end of file diff --git a/src/js/actions/insert-layer.js b/src/js/actions/insert-layer.js new file mode 100644 index 0000000..b1d6f96 --- /dev/null +++ b/src/js/actions/insert-layer.js @@ -0,0 +1,205 @@ +import app from './../app.js'; +import config from './../config.js'; +import { Base_action } from './base.js'; + +export class Insert_layer_action extends Base_action { + /** + * Creates new layer + * + * @param {object} settings + * @param {boolean} can_automate + */ + constructor(settings, can_automate = true) { + super('insert_layer', 'Insert Layer'); + this.settings = settings; + this.can_automate = can_automate; + this.previous_auto_increment = null; + this.previous_selected_layer = null; + this.inserted_layer_id = null; + this.update_layer_action = null; + this.delete_layer_action = null; + this.autoresize_canvas_action = null; + } + + async do() { + super.do(); + this.previous_auto_increment = app.Layers.auto_increment; + this.previous_selected_layer = config.layer; + let autoresize_as = null; + + // Default data + const layer = { + id: app.Layers.auto_increment, + parent_id: 0, + name: config.TOOL.name.charAt(0).toUpperCase() + config.TOOL.name.slice(1) + ' #' + app.Layers.auto_increment, + type: null, + link: null, + x: 0, + y: 0, + width: 0, + width_original: null, + height: 0, + height_original: null, + visible: true, + is_vector: false, + hide_selection_if_active: false, + opacity: 100, + order: app.Layers.auto_increment, + composition: 'source-over', + rotate: 0, + data: null, + params: {}, + status: null, + color: config.COLOR, + filters: [], + render_function: null, + }; + + // Build data + for (let i in this.settings) { + if (typeof layer[i] == "undefined") { + alertify.error('Error: wrong key: ' + i); + continue; + } + layer[i] = this.settings[i]; + } + + // Prepare image + let image_load_promise; + if (layer.type == 'image') { + + if(layer.name.toLowerCase().indexOf('.svg') == layer.name.length - 4){ + // We have svg + layer.is_vector = true; + } + + if (config.layers.length == 1 && config.layer.width == 0 + && config.layer.height == 0 && config.layer.data == null) { + // Remove first empty layer + + this.delete_layer_action = new app.Actions.Delete_layer_action(config.layer.id, true); + this.delete_layer_action.do(); + } + + if (layer.link == null) { + if (typeof layer.data == 'object') { + // Load actual image + if (layer.width == 0) + layer.width = layer.data.width; + if (layer.height == 0) + layer.height = layer.data.height; + layer.link = layer.data.cloneNode(true); + layer.link.onload = function () { + config.need_render = true; + }; + layer.data = null; + autoresize_as = [config.layer.width, config.layer.height]; + need_autoresize = true; + } + else if (typeof layer.data == 'string') { + image_load_promise = new Promise((resolve, reject) => { + // Try loading as imageData + layer.link = new Image(); + layer.link.onload = () => { + // Update dimensions + if (layer.width == 0) + layer.width = layer.link.width; + if (layer.height == 0) + layer.height = layer.link.height; + if (layer.width_original == null) + layer.width_original = layer.width; + if (layer.height_original == null) + layer.height_original = layer.height; + // Free data + layer.data = null; + autoresize_as = [layer.width, layer.height, layer.id, this.can_automate]; + config.need_render = true; + resolve(); + }; + layer.link.onerror = (error) => { + resolve(error); + alertify.error('Sorry, image could not be loaded.'); + }; + layer.link.src = layer.data; + layer.link.crossOrigin = "Anonymous"; + }); + } + else { + alertify.error('Error: can not load image.'); + } + } + } + + if (this.settings != undefined && config.layers.length > 0 + && config.layer.width == 0 && config.layer.height == 0 + && config.layer.data == null && layer.type != 'image' && this.can_automate !== false) { + // Update existing layer, because it's empty + delete layer.name; + this.update_layer_action = new app.Actions.Update_layer_action(config.layer.id, layer); + await this.update_layer_action.do(); + } + else { + // Create new layer + config.layers.push(layer); + config.layer = app.Layers.get_layer(layer.id); + app.Layers.auto_increment++; + + if (config.layer == null) { + config.layer = config.layers[0]; + } + + this.inserted_layer_id = layer.id; + } + + if (layer.id >= app.Layers.auto_increment) + app.Layers.auto_increment = layer.id + 1; + + if (image_load_promise) { + await image_load_promise; + } + + if (autoresize_as) { + this.autoresize_canvas_action = new app.Actions.Autoresize_canvas_action(...autoresize_as); + try { + await this.autoresize_canvas_action.do(); + } catch(error) { + this.autoresize_canvas_action = null; + } + } + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + app.Layers.auto_increment = this.previous_auto_increment; + if (this.autoresize_canvas_action) { + await this.autoresize_canvas_action.undo(); + this.autoresize_canvas_action = null; + } + if (this.inserted_layer_id) { + await new app.Actions.Delete_layer_action(this.inserted_layer_id, true).do(); + this.inserted_layer_id = null; + } + if (this.update_layer_action) { + await this.update_layer_action.undo(); + this.update_layer_action = null; + } + if (this.delete_layer_action) { + await this.delete_layer_action.undo(); + this.delete_layer_action = null; + } + config.layer = this.previous_selected_layer; + this.previous_selected_layer = null; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + free() { + this.delete_layer_action = null; + this.update_layer_action = null; + this.previous_selected_layer = null; + } +} \ No newline at end of file diff --git a/src/js/actions/prepare-canvas.js b/src/js/actions/prepare-canvas.js new file mode 100644 index 0000000..bb6039f --- /dev/null +++ b/src/js/actions/prepare-canvas.js @@ -0,0 +1,29 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Prepare_canvas_action extends Base_action { + /** + * Resizes/renders the canvas at the specified step. Usually used on both sides of a config update action. + * + * @param {boolean} call_when + */ + constructor(call_when = 'undo') { + super('prepare_canvas', 'Prepare Canvas'); + this.call_when = call_when; + } + + async do() { + super.do(); + if (this.call_when === 'do') { + app.GUI.prepare_canvas(); + } + } + + async undo() { + super.undo(); + if (this.call_when === 'undo') { + app.GUI.prepare_canvas(); + } + } +} \ No newline at end of file diff --git a/src/js/actions/reset-layers.js b/src/js/actions/reset-layers.js new file mode 100644 index 0000000..b89a425 --- /dev/null +++ b/src/js/actions/reset-layers.js @@ -0,0 +1,53 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Reset_layers_action extends Base_action { + /* + * removes all layers + */ + constructor(auto_insert) { + super('reset_layers', 'Reset Layers'); + this.auto_insert = auto_insert; + this.previous_auto_increment = null; + this.delete_actions = null; + this.insert_action = null; + } + async do() { + super.do(); + const auto_insert = this.auto_insert; + this.previous_auto_increment = app.Layers.auto_increment; + + this.delete_actions = []; + for (let i = config.layers.length - 1; i >= 0; i--) { + const delete_action = new app.Actions.Delete_layer_action(config.layers[i].id, true); + await delete_action.do(); + this.delete_actions.push(delete_action); + } + app.Layers.auto_increment = 1; + + if (auto_insert != undefined && auto_insert === true) { + const settings = {}; + this.insert_action = new app.Actions.Insert_layer_action(settings); + await this.insert_action.do(); + } + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + async undo() { + super.undo(); + if (this.insert_action) { + await this.insert_action.undo(); + this.insert_action = null; + } + for (let i = this.delete_actions.length - 1; i >= 0; i--) { + await this.delete_actions[i].undo(); + } + app.Layers.auto_increment = this.previous_auto_increment; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + +} \ No newline at end of file diff --git a/src/js/actions/reset-selection.js b/src/js/actions/reset-selection.js new file mode 100644 index 0000000..a614185 --- /dev/null +++ b/src/js/actions/reset-selection.js @@ -0,0 +1,40 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Reset_selection_action extends Base_action { + /** + * Sets the selection to empty + */ + constructor() { + super('reset_selection', 'Reset Selection'); + this.settings_reference = null; + this.old_settings_data = null; + } + + async do() { + super.do(); + this.settings_reference = app.Layers.Base_selection.find_settings(); + this.old_settings_data = this.settings_reference.data; + this.settings_reference.data = { + x: null, + y: null, + width: null, + height: null + }; + config.need_render = true; + } + + async undo() { + super.undo(); + this.settings_reference.data = this.old_settings_data; + this.settings_reference = null; + this.old_settings_data = null; + config.need_render = true; + } + + free() { + this.settings_reference = null; + this.old_settings_data = null; + } +} \ No newline at end of file diff --git a/src/js/actions/select-layer.js b/src/js/actions/select-layer.js new file mode 100644 index 0000000..93e1f5b --- /dev/null +++ b/src/js/actions/select-layer.js @@ -0,0 +1,57 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Select_layer_action extends Base_action { + /** + * marks layer as selected, active + * + * @param {int} layer_id + */ + constructor(layer_id, ignore_same_selection = false) { + super('select_layer', 'Select Layer'); + this.reset_selection_action = null; + this.layer_id = parseInt(layer_id); + this.ignore_same_selection = ignore_same_selection; + this.old_layer = null; + } + + async do() { + super.do(); + + let old_layer = config.layer; + let new_layer = app.Layers.get_layer(this.layer_id); + + if (old_layer !== new_layer) { + this.old_layer = old_layer; + config.layer = new_layer; + } else if (!this.ignore_same_selection) { + throw new Error('Aborted - Layer already selected'); + } + + this.reset_selection_action = new app.Actions.Reset_selection_action(); + await this.reset_selection_action.do(); + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + + if (this.reset_selection_action) { + await this.reset_selection_action.undo(); + this.reset_selection_action = null; + } + + config.layer = this.old_layer; + this.old_layer = null; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + free() { + this.old_layer = null; + } +} \ No newline at end of file diff --git a/src/js/actions/select-next-layer.js b/src/js/actions/select-next-layer.js new file mode 100644 index 0000000..9d7eaaf --- /dev/null +++ b/src/js/actions/select-next-layer.js @@ -0,0 +1,33 @@ +import app from './../app.js'; +import config from './../config.js'; +import { Base_action } from './base.js'; + +export class Select_next_layer_action extends Base_action { + constructor(reference_layer_id) { + super('select_next_layer', 'Select Next Layer'); + this.reference_layer_id = reference_layer_id; + this.old_config_layer = null; + } + + async do() { + super.do(); + const next_layer = app.Layers.find_next(this.reference_layer_id); + if (!next_layer) { + throw new Error('Aborted - Next layer to select not found'); + } + this.old_config_layer = config.layer; + config.layer = next_layer; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + config.layer = this.old_config_layer; + this.old_config_layer = null; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } +} \ No newline at end of file diff --git a/src/js/actions/select-previous-layer.js b/src/js/actions/select-previous-layer.js new file mode 100644 index 0000000..246940d --- /dev/null +++ b/src/js/actions/select-previous-layer.js @@ -0,0 +1,33 @@ +import app from './../app.js'; +import config from './../config.js'; +import { Base_action } from './base.js'; + +export class Select_previous_layer_action extends Base_action { + constructor(reference_layer_id) { + super('select_previous_layer', 'Select Previous Layer'); + this.reference_layer_id = reference_layer_id; + this.old_config_layer = null; + } + + async do() { + super.do(); + const previous_layer = app.Layers.find_previous(this.reference_layer_id); + if (!previous_layer) { + throw new Error('Aborted - Previous layer to select not found'); + } + this.old_config_layer = config.layer; + config.layer = previous_layer; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + config.layer = this.old_config_layer; + this.old_config_layer = null; + + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } +} \ No newline at end of file diff --git a/src/js/actions/toggle-layer-visibility.js b/src/js/actions/toggle-layer-visibility.js new file mode 100644 index 0000000..910dabb --- /dev/null +++ b/src/js/actions/toggle-layer-visibility.js @@ -0,0 +1,37 @@ +import app from '../app.js'; +import config from '../config.js'; +import { Base_action } from './base.js'; + +export class Toggle_layer_visibility_action extends Base_action { + /** + * toggle layer visibility + * + * @param {int} layer_id + */ + constructor(layer_id) { + super('toggle_layer_visibility', 'Toggle Layer Visibility'); + this.layer_id = parseInt(layer_id); + this.old_visible = null; + } + + async do() { + super.do(); + const layer = app.Layers.get_layer(this.layer_id); + this.old_visible = layer.visible; + if (layer.visible == false) + layer.visible = true; + else + layer.visible = false; + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } + + async undo() { + super.undo(); + const layer = app.Layers.get_layer(this.layer_id); + layer.visible = this.old_visible; + this.old_visible = null; + app.Layers.render(); + app.GUI.GUI_layers.render_layers(); + } +} \ No newline at end of file diff --git a/src/js/actions/update-config.js b/src/js/actions/update-config.js new file mode 100644 index 0000000..72d242c --- /dev/null +++ b/src/js/actions/update-config.js @@ -0,0 +1,37 @@ +import app from './../app.js'; +import config from './../config.js'; +import { Base_action } from './base.js'; + +export class Update_config_action extends Base_action { + /** + * Updates the app config with the provided settings + * + * @param {object} settings + */ + constructor(settings) { + super('update_config', 'Update Config'); + this.settings = settings; + this.old_settings = {}; + } + + async do() { + super.do(); + for (let i in this.settings) { + this.old_settings[i] = config[i]; + config[i] = this.settings[i]; + } + } + + async undo() { + super.undo(); + for (let i in this.old_settings) { + config[i] = this.old_settings[i]; + } + this.old_settings = {}; + } + + free() { + this.settings = null; + this.old_settings = null; + } +} \ No newline at end of file diff --git a/src/js/actions/update-layer.js b/src/js/actions/update-layer.js new file mode 100644 index 0000000..0a729e0 --- /dev/null +++ b/src/js/actions/update-layer.js @@ -0,0 +1,54 @@ +import app from './../app.js'; +import config from './../config.js'; +import { Base_action } from './base.js'; + +export class Update_layer_action extends Base_action { + /** + * Updates an existing layer with the provided settings + * + * @param {string} layer_id + * @param {object} settings + */ + constructor(layer_id, settings) { + super('update_layer', 'Update Layer'); + this.layer_id = layer_id; + this.settings = settings; + this.reference_layer = null; + this.old_settings = {}; + } + + async do() { + super.do(); + this.reference_layer = app.Layers.get_layer(this.layer_id); + + if (!this.reference_layer) { + throw new Error('Aborted - layer with specified id doesn\'t exist'); + } + + for (let i in this.settings) { + if (i == 'id') + continue; + if (i == 'order') + continue; + this.old_settings[i] = this.reference_layer[i]; + this.reference_layer[i] = this.settings[i]; + } + } + + async undo() { + super.undo(); + if (this.reference_layer) { + for (let i in this.old_settings) { + this.reference_layer[i] = this.old_settings[i]; + } + this.old_settings = {}; + } + this.reference_layer = null; + } + + free() { + this.settings = null; + this.old_settings = null; + this.reference_layer = null; + } +} \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..0772ae1 --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,11 @@ +// Store singletons for easy access +export default { + GUI: null, + Tools: null, + Layers: null, + Config: null, + State: null, + FileOpen: null, + FileSave: null, + Actions: null +}; \ No newline at end of file diff --git a/src/js/core/base-layers.js b/src/js/core/base-layers.js index a36cb91..65154e6 100644 --- a/src/js/core/base-layers.js +++ b/src/js/core/base-layers.js @@ -3,6 +3,7 @@ * author: Vilius L. */ +import app from './../app.js'; import config from './../config.js'; import Base_gui_class from './base-gui.js'; import Base_selection_class from './base-selection.js'; @@ -66,7 +67,8 @@ class Base_layers_class { */ init() { this.init_zoom_lib(); - this.insert({}); + + new app.Actions.Insert_layer_action({}).do(); var sel_config = { enable_background: false, @@ -315,151 +317,9 @@ class Base_layers_class { * @param {boolean} can_automate */ async insert(settings, can_automate = true) { - var _this = this; - - return new Promise(function(resolve, reject) { - var resolvable = false; - var need_autoresize = false; - - //default data - var layer = { - id: _this.auto_increment, - parent_id: 0, - name: _this.Helper.ucfirst(config.TOOL.name) + ' #' + _this.auto_increment, - type: null, - link: null, - x: 0, - y: 0, - width: 0, - width_original: null, - height: 0, - height_original: null, - visible: true, - is_vector: false, - hide_selection_if_active: false, - opacity: 100, - order: _this.auto_increment, - composition: 'source-over', - rotate: 0, - data: null, - params: {}, - status: null, - color: config.COLOR, - filters: [], - render_function: null, - }; - - //build data - for (var i in settings) { - if (typeof layer[i] == "undefined") { - alertify.error('Error: wrong key: ' + i); - continue; - } - layer[i] = settings[i]; - } - - //prepare image - if (layer.type == 'image') { - - if(layer.name.toLowerCase().indexOf('.svg') == layer.name.length - 4){ - //we have svg - layer.is_vector = true; - } - - if (config.layers.length == 1 && config.layer.width == 0 - && config.layer.height == 0 && config.layer.data == null) { - //remove first empty layer? - _this.delete(config.layer.id, true); - } - - if (layer.link == null) { - if (typeof layer.data == 'object') { - //load actual image - if (layer.width == 0) - layer.width = layer.data.width; - if (layer.height == 0) - layer.height = layer.data.height; - layer.link = layer.data.cloneNode(true); - layer.link.onload = function () { - config.need_render = true; - }; - layer.data = null; - need_autoresize = true; - } - else if (typeof layer.data == 'string') { - //try loading as imageData - resolvable = true; - layer.link = new Image(); - layer.link.onload = function () { - //update dimensions - if (layer.width == 0) - layer.width = layer.link.width; - if (layer.height == 0) - layer.height = layer.link.height; - if (layer.width_original == null) - layer.width_original = layer.width; - if (layer.height_original == null) - layer.height_original = layer.height; - //free data - - layer.data = null; - _this.autoresize(layer.width, layer.height, layer.id, can_automate); - _this.render(); - layer.link.onload = function () { - config.need_render = true; - }; - resolve(true); - }; - layer.link.onerror = function () { - alertify.error('Sorry, image could not be loaded.'); - }; - layer.link.src = layer.data; - layer.link.crossOrigin = "Anonymous"; - } - else { - alertify.error('Error: can not load image.'); - } - } - } - - if (settings != undefined && config.layers.length > 0 - && config.layer.width == 0 && config.layer.height == 0 - && config.layer.data == null && layer.type != 'image' && can_automate !== false) { - //update existing layer, because its empty - for (var i in layer) { - if (i == 'id') - continue; - if (i == 'name') - continue; - if (i == 'order') - continue; - config.layer[i] = layer[i]; - } - } - else { - //create new layer - config.layers.push(layer); - config.layer = _this.get_layer(layer.id); - _this.auto_increment++; - - if (config.layer == null) { - config.layer = config.layers[0]; - } - } - - if (layer.id >= _this.auto_increment) - _this.auto_increment = layer.id + 1; - - if (need_autoresize == true) { - _this.autoresize(config.layer.width, config.layer.height); - } - - _this.render(); - _this.Base_gui.GUI_layers.render_layers(); - if(resolvable == false){ - resolve(true); - } - }); + return app.State.do_action( + new app.Actions.Insert_layer_action(settings, can_automate) + ); } /** @@ -470,44 +330,10 @@ class Base_layers_class { * @param {int} layer_id * @param {boolean} can_automate */ - autoresize(width, height, layer_id, can_automate = true) { - var _this = this; - var need_fit = false; - - //resize up - if (width > config.WIDTH || height > config.HEIGHT) { - - var wrapper = document.getElementById('main_wrapper'); - var page_w = wrapper.clientWidth; - var page_h = wrapper.clientHeight; - - if (width > page_w || height > page_h) { - need_fit = true; - } - if (width > config.WIDTH) - config.WIDTH = parseInt(width); - if (height > config.HEIGHT) - config.HEIGHT = parseInt(height); - } - - //resize down - if (config.layers.length == 1 && can_automate !== false) { - if (width < config.WIDTH) - config.WIDTH = parseInt(width); - if (height < config.HEIGHT) - config.HEIGHT = parseInt(height); - } - - this.Base_gui.prepare_canvas(); - - //fit zoom when after short pause - //@todo - remove setTimeout - if (need_fit == true) { - window.setTimeout(myCallback, 100); - function myCallback() { - _this.Base_gui.GUI_preview.zoom_auto(); - } - } + 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) + ); } /** @@ -535,60 +361,19 @@ class Base_layers_class { * @param {int} id * @param {boolean} force - Force to delete first layer? */ - delete(id, force) { - id = parseInt(id); - if (config.layers.length == 1 && (force == undefined || force == false)) { - //only 1 layer left - if (config.layer.type == null) { - //STOP - return; - } - else { - //delete it, but before that - create new empty layer - this.insert(); - } - } - - if (config.layer.id == id) { - //select previous layer - config.layer = this.find_next(id); - if (config.layer == null) - config.layer = this.find_previous(id); - } - - for (var i in config.layers) { - if (config.layers[i].id == id) { - //delete - - if (config.layers[i].type == 'image') { - //clean image - config.layers[i].link = null; - } - - config.layers.splice(i, 1); - } - } - - this.render(); - this.Base_gui.GUI_layers.render_layers(); + async delete(id, force) { + return app.State.do_action( + new app.Actions.Delete_layer_action(id, force) + ); } /* * removes all layers */ - reset_layers(auto_insert) { - for (var i = config.layers.length - 1; i >= 0; i--) { - this.delete(config.layers[i].id, true); - } - this.auto_increment = 1; - - if (auto_insert != undefined && auto_insert === true) { - var settings = {}; - this.insert(settings); - } - - this.render(); - this.Base_gui.GUI_layers.render_layers(); + async reset_layers(auto_insert) { + return app.State.do_action( + new app.Actions.Reset_layers_action(auto_insert) + ); } /** @@ -596,17 +381,10 @@ class Base_layers_class { * * @param {int} id */ - toggle_visibility(id) { - id = parseInt(id); - var link = this.get_layer(id); - - if (link.visible == false) - link.visible = true; - else - link.visible = false; - - this.render(); - this.Base_gui.GUI_layers.render_layers(); + async toggle_visibility(id) { + return app.State.do_action( + new app.Actions.Toggle_layer_visibility_action(id) + ); } /* @@ -621,13 +399,10 @@ class Base_layers_class { * * @param {int} id */ - select(id) { - id = parseInt(id); - config.layer = this.get_layer(id); - this.Base_selection.reset_selection(); - - this.render(); - this.Base_gui.GUI_layers.render_layers(); + async select(id) { + return app.State.do_action( + new app.Actions.Select_layer_action(id) + ); } /** diff --git a/src/js/core/base-state.js b/src/js/core/base-state.js index d0a0dac..692dbac 100644 --- a/src/js/core/base-state.js +++ b/src/js/core/base-state.js @@ -10,6 +10,9 @@ import Helper_class from './../libs/helpers.js'; import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js'; var instance = null; +let action_history = []; +let action_history_index = 0; +let action_history_max = 50; /** * Undo state class. Supports multiple levels undo. @@ -36,18 +39,71 @@ class Base_state_class { set_events() { document.addEventListener('keydown', (event) => { - var code = event.code; + const key = event.key; if (this.Helper.is_input(event.target)) return; - if (code == "KeyZ" && (event.ctrlKey == true || event.metaKey)) { - //undo + if (key == "z" && (event.ctrlKey == true || event.metaKey)) { + // Undo this.undo(); event.preventDefault(); } + if (key == "y" && (event.ctrlKey == true || event.metaKey)) { + // Redo + this.redo(); + event.preventDefault(); + } }, false); } + async do_action(action) { + try { + await action.do(); + } catch (error) { + // Action aborted. This could be expected behavior if detected that the action shouldn't run. + return { status: 'aborted', reason: error }; + } + // Remove all redo actions from history + if (action_history_index < action_history.length) { + action_history = action_history.slice(0, action_history_index); + const freed_actions = action_history.slice(action_history_index, action_history.length); + for (let freed_action of freed_actions) { + freed_action.free(); + } + } + // Add the new action to history + action_history.push(action); + if (action_history.length > action_history_max) { + action_history.shift(); + } else { + action_history_index++; + } + return { status: 'completed' }; + } + + can_redo() { + return action_history_index < action_history.length; + } + + can_undo() { + return action_history_index > 0; + } + + async redo_action() { + if (this.can_redo()) { + const action = action_history[action_history_index]; + await action.do(); + action_history_index++; + } + } + + async undo_action() { + if (this.can_undo()) { + action_history_index--; + await action_history[action_history_index].undo(); + } + } + save() { this.optimize(); @@ -105,6 +161,8 @@ class Base_state_class { * supports multiple levels undo system */ undo() { + this.undo_action(); + /* if (this.enabled == false || this.layers_archive[0] == undefined) { //not saved yet alertify.error('Undo is not available.'); @@ -145,6 +203,11 @@ class Base_state_class { this.Base_layers.select(data.layer_active); this.layers_archive.shift(); //remove used state + */ + } + + redo() { + this.redo_action(); } /** diff --git a/src/js/core/gui/gui-layers.js b/src/js/core/gui/gui-layers.js index e5a2992..68312ec 100644 --- a/src/js/core/gui/gui-layers.js +++ b/src/js/core/gui/gui-layers.js @@ -3,6 +3,7 @@ * author: Vilius L. */ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../base-layers.js'; import Helper_class from './../../libs/helpers.js'; @@ -43,8 +44,9 @@ class GUI_layers_class { var target = event.target; if (target.id == 'insert_layer') { //new layer - window.State.save(); - _this.Base_layers.insert(); + app.State.do_action( + new app.Actions.Insert_layer_action() + ); } else if (target.id == 'layer_up') { //move layer up @@ -58,18 +60,23 @@ class GUI_layers_class { } else if (target.id == 'visibility') { //change visibility - _this.Base_layers.toggle_visibility(target.dataset.id); + return app.State.do_action( + new app.Actions.Toggle_layer_visibility_action(target.dataset.id) + ); } else if (target.id == 'delete') { //delete layer - window.State.save(); - _this.Base_layers.delete(target.dataset.id); + app.State.do_action( + new app.Actions.Delete_layer_action(target.dataset.id) + ); } else if (target.id == 'layer_name') { //select layer if (target.dataset.id == config.layer.id) return; - _this.Base_layers.select(target.dataset.id); + app.State.do_action( + new app.Actions.Select_layer_action(target.dataset.id) + ); } else if (target.id == 'delete_filter') { //delete filter diff --git a/src/js/libs/zoomView.js b/src/js/libs/zoomView.js index 96e706e..10c9f4c 100644 --- a/src/js/libs/zoomView.js +++ b/src/js/libs/zoomView.js @@ -7,7 +7,7 @@ const zoomView = (() => { var im = invMatrix; // alias var scale = 1; // current scale const bounds = { - topLeft: 0, + top: 0, left: 0, right: 200, bottom: 200, @@ -39,6 +39,9 @@ const zoomView = (() => { getPosition() { return { x: pos.x, y: pos.y }; }, + getContext() { + return ctx; + }, getBounds() { return bounds; }, diff --git a/src/js/main.js b/src/js/main.js index b2be959..6f2085f 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -12,6 +12,7 @@ import './../css/menu.css'; import './../css/print.css'; import './../../node_modules/alertifyjs/build/css/alertify.min.css'; //js +import app from './app.js'; import config from './config.js'; import './core/components/index.js'; import Base_gui_class from './core/base-gui.js'; @@ -20,9 +21,10 @@ import Base_tools_class from './core/base-tools.js'; import Base_state_class from './core/base-state.js'; import File_open_class from './modules/file/open.js'; import File_save_class from './modules/file/save.js'; +import * as Actions from './actions/index.js'; window.addEventListener('load', function (e) { - //initiate app + // Initiate app var Layers = new Base_layers_class(); var Base_tools = new Base_tools_class(true); var GUI = new Base_gui_class(); @@ -30,14 +32,24 @@ window.addEventListener('load', function (e) { var File_open = new File_open_class(); var File_save = new File_save_class(); - //register as global for quick or external access + // Register singletons in app module + app.Actions = Actions; + app.Config = config; + app.FileOpen = File_open; + app.FileSave = File_save; + app.GUI = GUI; + app.Layers = Layers; + app.State = Base_state; + app.Tools = Base_tools; + + // Register as global for quick or external access window.Layers = Layers; window.AppConfig = config; window.State = Base_state; // window.State.save(); window.FileOpen = File_open; window.FileSave = File_save; - //render all + // Render all GUI.load_modules(); GUI.load_default_values(); GUI.render_main_gui(); diff --git a/src/js/modules/file/new.js b/src/js/modules/file/new.js index 4ad879b..69527bb 100644 --- a/src/js/modules/file/new.js +++ b/src/js/modules/file/new.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_gui_class from './../../core/base-gui.js'; import Base_layers_class from './../../core/base-layers.js'; @@ -87,39 +88,40 @@ class File_new_class { width = dim[0]; height = dim[1]; } - if (transparency == true) - config.TRANSPARENCY = true; - else - config.TRANSPARENCY = false; - config.WIDTH = parseInt(width); - config.HEIGHT = parseInt(height); - config.ALPHA = 255; - config.COLOR = '#008000'; - config.mouse = {}; - config.visible_width = null; - config.visible_height = null; - this.Base_gui.prepare_canvas(); + // Prepare layers + app.State.do_action( + new app.Actions.Bundle_action('new_file', 'New File', [ + new app.Actions.Prepare_canvas_action('undo'), + new app.Actions.Update_config_action({ + TRANSPARENCY: !!transparency, + WIDTH: parseInt(width), + HEIGHT: parseInt(height), + ALPHA: 255, + COLOR: '#008000', + mouse: {}, + visible_width: null, + visible_height: null + }), + new app.Actions.Prepare_canvas_action('do'), + new app.Actions.Reset_layers_action(), + new app.Actions.Init_canvas_zoom_action(), + new app.Actions.Insert_layer_action({}) + ]) + ); - //prepare layers - this.Base_layers.reset_layers(); - this.Base_layers.init_zoom_lib(); - this.Base_layers.insert({}); - config.need_render = true; - - //last resolution + // Last resolution var last_resolution = JSON.stringify([config.WIDTH, config.HEIGHT]); this.Helper.setCookie('last_resolution', last_resolution); - //save_resolution + // Save resolution if (save_resolution) { this.Helper.setCookie('save_resolution', 1); } else { this.Helper.setCookie('save_resolution', 0); } - - //transparency + // Save transparency if (transparency) { this.Helper.setCookie('transparency', 1); } diff --git a/src/js/modules/file/open.js b/src/js/modules/file/open.js index 41fd6bb..234ad20 100644 --- a/src/js/modules/file/open.js +++ b/src/js/modules/file/open.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Base_gui_class from './../../core/base-gui.js'; @@ -58,7 +59,9 @@ class File_open_class { type: 'image', data: data, }; - this.Base_layers.insert(new_layer); + app.State.do_action( + new app.Actions.Insert_layer_action(new_layer) + ); } open_file() { @@ -125,8 +128,12 @@ class File_open_class { width_original: width, height_original: height, }; - this.Base_layers.insert(new_layer); - _this.Base_layers.autoresize(width, height); + app.State.do_action( + new app.Actions.Bundle_action('open_file_webcam', 'Open File Webcam', [ + new app.Actions.Insert_layer_action(new_layer), + new app.Actions.Autoresize_canvas_action(width, height) + ]) + ); //destroy if(track != null){ @@ -207,8 +214,12 @@ class File_open_class { width_original: img.width, height_original: img.height, }; - _this.Base_layers.insert(new_layer); - _this.Base_layers.autoresize(img.width, img.height); + app.State.do_action( + new app.Actions.Bundle_action('open_file_data_url', 'Open File Data URL', [ + new app.Actions.Insert_layer_action(new_layer), + new app.Actions.Autoresize_canvas_action(img.width, img.height) + ]) + ); img.onload = function () { config.need_render = true; }; @@ -280,7 +291,9 @@ class File_open_class { data: event.target.result, order: order, }; - _this.Base_layers.insert(new_layer); + app.State.do_action( + new app.Actions.Insert_layer_action(new_layer) + ); _this.extract_exif(this.file); } else { @@ -365,8 +378,12 @@ class File_open_class { img.onload = function () { config.need_render = true; }; - _this.Base_layers.insert(new_layer); - _this.Base_layers.autoresize(img.width, img.height); + app.State.do_action( + new app.Actions.Bundle_action('open_file_url', 'Open File URL', [ + new app.Actions.Insert_layer_action(new_layer), + new app.Actions.Autoresize_canvas_action(img.width, img.height) + ]) + ); }; img.onerror = function (ex) { alertify.error('Sorry, image could not be loaded. Try copy image and paste it.'); @@ -415,12 +432,19 @@ class File_open_class { } } + const actions = []; + //set attributes - config.ZOOM = 1; - config.WIDTH = parseInt(json.info.width); - config.HEIGHT = parseInt(json.info.height); - this.Base_layers.reset_layers(); - this.Base_gui.prepare_canvas(); + actions.push( + new app.Actions.Prepare_canvas_action('undo'), + new app.Actions.Update_config_action({ + ZOOM: 1, + WIDTH: parseInt(json.info.width), + HEIGHT: parseInt(json.info.height) + }), + new app.Actions.Reset_layers_action(), + new app.Actions.Prepare_canvas_action('do'), + ); for (var i in json.layers) { var value = json.layers[i]; @@ -434,12 +458,18 @@ class File_open_class { } } } - - this.Base_layers.insert(value, false); + actions.push( + new app.Actions.Insert_layer_action(value, false) + ); } - if(json.info.layer_active != undefined) { - this.Base_layers.select(json.info.layer_active); + if (json.info.layer_active != undefined) { + actions.push( + new app.Actions.Select_layer_action(json.info.layer_active, true) + ); } + app.State.do_action( + new app.Actions.Bundle_action('open_json_file', 'Open JSON File', actions) + ); } extract_exif(object) { diff --git a/src/js/modules/file/save.js b/src/js/modules/file/save.js index dfed148..33a132f 100644 --- a/src/js/modules/file/save.js +++ b/src/js/modules/file/save.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Helper_class from './../../libs/helpers.js'; @@ -112,10 +113,10 @@ class File_save_class { if (config.layers[i].visible == false) continue; - this.Base_layers.select(config.layers[i].id); + new app.Actions.Select_layer_action(config.layers[i].id, true).do(); _this.save_action(params, true); } - this.Base_layers.select(active_layer); + new app.Actions.Select_layer_action(active_layer, true).do(); } else { _this.save_action(params); diff --git a/src/js/modules/layer/delete.js b/src/js/modules/layer/delete.js index 3d6a3d4..c4a444f 100644 --- a/src/js/modules/layer/delete.js +++ b/src/js/modules/layer/delete.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; @@ -8,8 +9,9 @@ class Layer_delete_class { } delete() { - window.State.save(); - this.Base_layers.delete(config.layer.id); + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); } } diff --git a/src/js/modules/layer/differences.js b/src/js/modules/layer/differences.js index a957245..755808d 100644 --- a/src/js/modules/layer/differences.js +++ b/src/js/modules/layer/differences.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Dialog_class from './../../libs/popup.js'; @@ -79,12 +80,13 @@ class Layer_differences_class { //show if (canvas_preview == undefined) { //main - window.State.save(); var params = []; params.type = 'image'; params.name = 'Differences'; params.data = canvas.toDataURL("image/png"); - this.Base_layers.insert(params); + app.State.do_action( + new app.Actions.Insert_layer_action(params) + ); } else { //preview diff --git a/src/js/modules/layer/duplicate.js b/src/js/modules/layer/duplicate.js index da56eb1..d5dfe72 100644 --- a/src/js/modules/layer/duplicate.js +++ b/src/js/modules/layer/duplicate.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Helper_class from './../../libs/helpers.js'; @@ -56,7 +57,11 @@ class Layer_duplicate_class { params.link = config.layer.link.cloneNode(true); } - this.Base_layers.insert(params); + app.State.do_action( + new app.Actions.Bundle_action('duplicate_layer', 'Duplicate Layer', [ + new app.Actions.Insert_layer_action(params) + ]) + ); } } diff --git a/src/js/modules/layer/flatten.js b/src/js/modules/layer/flatten.js index cc33311..3bced7f 100644 --- a/src/js/modules/layer/flatten.js +++ b/src/js/modules/layer/flatten.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js'; @@ -39,15 +40,20 @@ class Layer_flatten_class { params.type = 'image'; params.name = 'Merged'; params.data = canvas.toDataURL("image/png"); - this.Base_layers.insert(params); - //remove all layers + //remove rest of layers + let delete_actions = []; for (var i = config.layers.length - 1; i >= 0; i--) { - if (config.layers[i].id == config.layer.id) - continue; - - this.Base_layers.delete(config.layers[i].id); + delete_actions.push(new app.Actions.Delete_layer_action(config.layers[i].id)); } + console.log(delete_actions); + // Run actions + app.State.do_action( + new app.Actions.Bundle_action('flatten_image', 'Flatten Image', [ + new app.Actions.Insert_layer_action(params), + ...delete_actions + ]) + ); canvas.width = 1; canvas.height = 1; diff --git a/src/js/modules/layer/merge.js b/src/js/modules/layer/merge.js index a2d563c..99df8cc 100644 --- a/src/js/modules/layer/merge.js +++ b/src/js/modules/layer/merge.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js'; import Base_layers_class from './../../core/base-layers.js'; @@ -42,11 +43,13 @@ class Layer_merge_class { params.name = config.layer.name + ' + merged'; params.order = current_order; params.data = canvas.toDataURL("image/png"); - this.Base_layers.insert(params); - - //remove old layer - this.Base_layers.delete(current_id); - this.Base_layers.delete(previous_id); + app.State.do_action( + new app.Actions.Bundle_action('merge_layers', 'Merge Layers', [ + new app.Actions.Insert_layer_action(params), + new app.Actions.Delete_layer_action(current_id), + new app.Actions.Delete_layer_action(previous_id) + ]) + ); canvas.width = 1; canvas.height = 1; diff --git a/src/js/modules/layer/new.js b/src/js/modules/layer/new.js index 450a4ac..7f1ad12 100644 --- a/src/js/modules/layer/new.js +++ b/src/js/modules/layer/new.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import GUI_tools_class from './../../core/gui/gui-tools.js'; @@ -31,9 +32,10 @@ class Layer_new_class { }, false); } - new () { - window.State.save(); - this.Base_layers.insert(); + new() { + app.State.do_action( + new app.Actions.Insert_layer_action() + ); } new_selection() { @@ -83,7 +85,9 @@ class Layer_new_class { type: 'image', data: canvas.toDataURL("image/png"), }; - this.Base_layers.insert(params, false); + app.State.do_action( + new app.Actions.Insert_layer_action(params, false) + ); this.Selection.on_leave(); this.GUI_tools.activate_tool('select'); diff --git a/src/js/modules/layer/raster.js b/src/js/modules/layer/raster.js index ceb7ce6..05f0ef4 100644 --- a/src/js/modules/layer/raster.js +++ b/src/js/modules/layer/raster.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js'; @@ -13,8 +14,6 @@ class Layer_raster_class { var current_layer = config.layer; var current_id = current_layer.id; - window.State.save(); - //show var params = { type: 'image', @@ -26,9 +25,12 @@ class Layer_raster_class { height: canvas.height, opacity: current_layer.opacity, }; - this.Base_layers.insert(params, false); - - this.Base_layers.delete(current_id); + app.State.do_action( + new app.Actions.Bundle_action('convert_to_raster', 'Convert to Raster', [ + new app.Actions.Insert_layer_action(params, false), + new app.Actions.Delete_layer_action(current_id) + ]) + ); } } diff --git a/src/js/modules/tools/borders.js b/src/js/modules/tools/borders.js index ce52334..887a819 100644 --- a/src/js/modules/tools/borders.js +++ b/src/js/modules/tools/borders.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Dialog_class from './../../libs/popup.js'; @@ -60,8 +61,6 @@ class Tools_borders_class { } add_borders(params) { - window.State.save(); - //create borders layer this.layer = { name: 'Borders', @@ -75,7 +74,11 @@ class Tools_borders_class { height: config.HEIGHT, is_vector: true, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('add_borders', 'Add Borders', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); } } diff --git a/src/js/modules/tools/keypoints.js b/src/js/modules/tools/keypoints.js index 99e6425..c8886bb 100644 --- a/src/js/modules/tools/keypoints.js +++ b/src/js/modules/tools/keypoints.js @@ -1,3 +1,4 @@ +import app from './../../app.js'; import config from './../../config.js'; import Base_layers_class from './../../core/base-layers.js'; import Helper_class from './../../libs/helpers.js'; @@ -158,7 +159,11 @@ class Tools_keypoints_class { params.y = parseInt(clone.dataset.y); params.width = clone.width; params.height = clone.height; - this.Base_layers.insert(params); + app.State.do_action( + new app.Actions.Bundle_action('keypoints', 'Key-Points', [ + new app.Actions.Insert_layer_action(params) + ]) + ); clone.width = 1; clone.height = 1; diff --git a/src/js/tools/animation.js b/src/js/tools/animation.js index a52726a..427b8d5 100644 --- a/src/js/tools/animation.js +++ b/src/js/tools/animation.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -18,6 +19,7 @@ class Animation_class extends Base_tools_class { this.name = 'animation'; this.intervalID = null; this.index = 0; + this.toggle_layer_visibility_action = new app.Actions.Toggle_layer_visibility_action(); this.disable_selection(ctx); } @@ -102,7 +104,8 @@ class Animation_class extends Base_tools_class { //show 1 if (config.layers[this.index] != undefined) { - _this.Base_layers.toggle_visibility(config.layers[this.index].id); + this.toggle_layer_visibility_action.layer_id = config.layers[this.index].id; + this.toggle_layer_visibility_action.do(); } //change index diff --git a/src/js/tools/brush.js b/src/js/tools/brush.js index b72112d..0e94c0e 100644 --- a/src/js/tools/brush.js +++ b/src/js/tools/brush.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -98,8 +99,6 @@ class Brush_class extends Base_tools_class { if (mouse.valid == false || mouse.click_valid == false) return; - window.State.save(); - var params_hash = this.get_params_hash(); if (config.layer.type != this.name || params_hash != this.params_hash) { @@ -118,7 +117,11 @@ class Brush_class extends Base_tools_class { rotate: null, is_vector: true, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('new_brush_layer', 'New Brush Layer', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); this.params_hash = params_hash; } diff --git a/src/js/tools/circle.js b/src/js/tools/circle.js index bdf57d5..10f0cba 100644 --- a/src/js/tools/circle.js +++ b/src/js/tools/circle.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -65,8 +66,6 @@ class Circle_class extends Base_tools_class { if (mouse.valid == false || mouse.click_valid == false) return; - window.State.save(); - //register new object - current layer is not ours or params changed this.layer = { type: this.name, @@ -85,7 +84,11 @@ class Circle_class extends Base_tools_class { //disable rotate this.layer.rotate = null; } - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('new_circle_layer', 'New Circle Layer', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); } mousemove(e) { @@ -131,7 +134,9 @@ class Circle_class extends Base_tools_class { if (width == 0 && height == 0) { //same coordinates - cancel - this.Base_layers.delete(config.layer.id); + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); return; } diff --git a/src/js/tools/fill.js b/src/js/tools/fill.js index 2d0c74c..e8b1caf 100644 --- a/src/js/tools/fill.js +++ b/src/js/tools/fill.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -117,7 +118,11 @@ class Fill_class extends Base_tools_class { params.y = parseInt(canvas.dataset.y) || 0; params.width = canvas.width; params.height = canvas.height; - this.Base_layers.insert(params); + app.State.do_action( + new app.Actions.Bundle_action('fill', 'Fill', [ + new app.Actions.Insert_layer_action(params) + ]) + ); } //prevent crash bug on touch screen - hard to explain and debug diff --git a/src/js/tools/gradient.js b/src/js/tools/gradient.js index 630e05a..82e30ff 100644 --- a/src/js/tools/gradient.js +++ b/src/js/tools/gradient.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -74,8 +75,6 @@ class Gradient_class extends Base_tools_class { is_vector = true; } - window.State.save(); - //register new object - current layer is not ours or params changed this.layer = { type: this.name, @@ -93,7 +92,11 @@ class Gradient_class extends Base_tools_class { center_y: mouse.y, }, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('gradient', 'Gradient', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); } mousemove(e) { @@ -135,7 +138,9 @@ class Gradient_class extends Base_tools_class { if (width == 0 && height == 0) { //same coordinates - cancel - this.Base_layers.delete(config.layer.id); + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); return; } diff --git a/src/js/tools/line.js b/src/js/tools/line.js index d40f7c1..28685b9 100644 --- a/src/js/tools/line.js +++ b/src/js/tools/line.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -64,8 +65,6 @@ class Line_class extends Base_tools_class { if (mouse.valid == false || mouse.click_valid == false) return; - window.State.save(); - //register new object - current layer is not ours or params changed this.layer = { type: this.name, @@ -77,7 +76,11 @@ class Line_class extends Base_tools_class { rotate: null, is_vector: true, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('line', 'Line', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); } mousemove(e) { @@ -116,7 +119,9 @@ class Line_class extends Base_tools_class { if (width == 0 && height == 0) { //same coordinates - cancel - this.Base_layers.delete(config.layer.id); + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); return; } diff --git a/src/js/tools/pencil.js b/src/js/tools/pencil.js index 71850cd..ccba19c 100644 --- a/src/js/tools/pencil.js +++ b/src/js/tools/pencil.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -95,7 +96,11 @@ class Pencil_class extends Base_tools_class { rotate: null, is_vector: true, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('new_pencil_layer', 'New Pencil Layer', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); this.params_hash = params_hash; } else { diff --git a/src/js/tools/rectangle.js b/src/js/tools/rectangle.js index 5ca5478..ab9ba2c 100644 --- a/src/js/tools/rectangle.js +++ b/src/js/tools/rectangle.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -64,8 +65,6 @@ class Rectangle_class extends Base_tools_class { if (mouse.valid == false || mouse.click_valid == false) return; - window.State.save(); - //register new object - current layer is not ours or params changed this.layer = { type: this.name, @@ -76,7 +75,11 @@ class Rectangle_class extends Base_tools_class { y: mouse.y, is_vector: true, }; - this.Base_layers.insert(this.layer); + app.State.do_action( + new app.Actions.Bundle_action('rectangle', 'Rectangle', [ + new app.Actions.Insert_layer_action(this.layer) + ]) + ); } mousemove(e) { @@ -142,7 +145,9 @@ class Rectangle_class extends Base_tools_class { if (width == 0 && height == 0) { //same coordinates - cancel - this.Base_layers.delete(config.layer.id); + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); return; } diff --git a/src/js/tools/select.js b/src/js/tools/select.js index df4d3c8..8641346 100644 --- a/src/js/tools/select.js +++ b/src/js/tools/select.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import Base_tools_class from './../core/base-tools.js'; import Base_layers_class from './../core/base-layers.js'; @@ -64,9 +65,9 @@ class Select_tool_class extends Base_tools_class { //keyboard actions document.addEventListener('keydown', (e) => { - if (config.TOOL.name != _this.name) + if (config.TOOL.name != this.name) return; - if (_this.POP.active == true) + if (this.POP.active == true) return; if (this.Helper.is_input(e.target)) return; @@ -74,24 +75,26 @@ class Select_tool_class extends Base_tools_class { //up if (k == 38) { - _this.move(0, -1, e); + this.move(0, -1, e); } //down else if (k == 40) { - _this.move(0, 1, e); + this.move(0, 1, e); } //right else if (k == 39) { - _this.move(1, 0, e); + this.move(1, 0, e); } //left else if (k == 37) { - _this.move(-1, 0, e); + this.move(-1, 0, e); } if (k == 46) { //delete - if (config.TOOL.name == _this.name) { - _this.Base_layers.delete(config.layer.id); + if (config.TOOL.name == this.name) { + app.State.do_action( + new app.Actions.Delete_layer_action(config.layer.id) + ); } } }); @@ -166,7 +169,9 @@ class Select_tool_class extends Base_tools_class { var canvas = this.Base_layers.convert_layer_to_canvas(value.id, null, false); if (this.check_hit_region(e, canvas.getContext("2d")) == true) { - this.Base_layers.select(value.id); + app.State.do_action( + new app.Actions.Select_layer_action(value.id) + ); break; } } diff --git a/src/js/tools/text.js b/src/js/tools/text.js index 995a178..6b50469 100644 --- a/src/js/tools/text.js +++ b/src/js/tools/text.js @@ -1,3 +1,4 @@ +import app from './../app.js'; import config from './../config.js'; import zoomView from './../libs/zoomView.js'; import Base_tools_class from './../core/base-tools.js'; @@ -2037,14 +2038,17 @@ class Text_class extends Base_tools_class { this.selecting = true; this.layer = existingLayer; const editor = this.get_editor(this.layer); - this.Base_layers.select(existingLayer.id); editor.trigger_cursor_start(this.layer, -1 + mouse.x - this.layer.x, mouse.y - this.layer.y); + app.State.do_action( + new app.Actions.Bundle_action('select_text_layer', 'Select Text Layer', [ + new app.Actions.Select_layer_action(existingLayer.id) + ]) + ); this.Base_selection.set_selection(this.layer.x, this.layer.y, this.layer.width, this.layer.height); } else { // Create a new text layer this.creating = true; - window.State.save(); const layer = { type: this.name, params: { @@ -2062,7 +2066,11 @@ class Text_class extends Base_tools_class { rotate: null, is_vector: true, }; - this.Base_layers.insert(layer); + app.State.do_action( + new app.Actions.Bundle_action('new_text_layer', 'New Text Layer', [ + new app.Actions.Insert_layer_action(layer) + ]) + ); this.layer = config.layer; this.Base_selection.set_selection(mouse.x, mouse.y, 0, 0); }