From 4e2777028481fced71bc87ca7fa5f9897bbeac66 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 13:06:13 -0300 Subject: [PATCH] some layer upgrades Signed-off-by: Victor Seiji Hariki --- index.html | 2 + js/index.js | 16 +++++ js/layers.js | 180 +++++++++++++++++++++++++++++++++++++++++++++------ js/util.d.ts | 52 +++++++++++++++ 4 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 js/util.d.ts diff --git a/index.html b/index.html index 6b65649..ef07ffa 100644 --- a/index.html +++ b/index.html @@ -299,6 +299,8 @@ + + diff --git a/js/index.js b/js/index.js index 3ccfe30..bf78890 100644 --- a/js/index.js +++ b/js/index.js @@ -112,6 +112,22 @@ const imgCtx = imgCanvas.getContext("2d"); const bgCanvas = document.getElementById("backgroundCanvas"); // gray bg grid const bgCtx = bgCanvas.getContext("2d"); +// Layering +const imageCollection = layers.registerCollection("image", { + name: "Image Layers", + scope: { + always: { + key: "default", + options: { + name: "Default Image Layer", + }, + }, + }, +}); + +layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true}); + +// function startup() { testHostConfiguration(); testHostConnection(); diff --git a/js/layers.js b/js/layers.js index e7bb02b..9dff0b0 100644 --- a/js/layers.js +++ b/js/layers.js @@ -4,29 +4,169 @@ * It manages canvases and their locations and sizes according to current viewport views */ +// Errors +class LayerNestedScopesError extends Error { + // For when a scope is created in another scope +} +class LayerNoScopeError extends Error { + // For when an action that requires a scope is attempted + // in a collection with no scope. +} + const layers = { - _layers: [], - layers: {}, + collections: makeWriteOnce({}, "layers.collections"), - // Registers a new layer - registerLayer: (name) => { - const layer = { - id: guid(), - name: layer, - // This is where black magic starts - // A proxy for the canvas object - canvas: new Proxy(document.createElement("canvas"), {}), + // Registers a new collection + registerCollection: (key, options = {}) => { + defaultOpt(options, { + // If collection is visible on the Layer View Toolbar + visible: true, + // Display name for the collection + name: key, + /** + * If layer creates a layer scope + * + * A layer scope is a context where one, and only one layer inside it or its + * subscopes can be active at a time. Nested scopes are not supported. + * It receives an object of type: + * + * { + * // If there must be a selected layer, pass information to create the first + * always: { + * key, + * options + * } + * } + */ + scope: null, + // Parent collection + parent: null, + }); + + // Finds the closest parent with a defined scope + const findScope = (collection = options.parent) => { + if (!collection) return null; + + if (collection.scope) return collection; + return findScope(collection._parent); }; - }, - // Deletes a layer - deleteLayer: (layer) => { - if (typeof layer === "object") { - layers._layers = layers._layers.filter((l) => l.id === layer.id); - delete layers[layer.id]; - } else if (typeof layer === "string") { - layers._layers = layers._layers.filter((l) => l.id === layer); - delete layers[layer]; - } + // Path used for logging purposes + const _logpath = options.parent + ? options.parent + "." + key + : "layers.collections." + key; + + // If we have a scope already, we can't add a new scope + if (options.scope && findScope()) + throw new LayerNestedScopesError(`Layer scopes must not be nested`); + + const collection = makeWriteOnce( + { + _parent: options.parent, + _logpath, + _layers: [], + layers: {}, + + name: options.name, + + scope: options.scope, + // Registers a new layer + registerLayer: (key, options = {}) => { + defaultOpt(options, { + // Display name for the layer + name: key, + }); + + // Path used for logging purposes + const _layerlogpath = _logpath + ".layers." + key; + const layer = makeWriteOnce( + { + _logpath: _layerlogpath, + id: guid(), + name: options.name, + + state: new Proxy( + {visible: true}, + { + set(obj, opt, val) { + switch (opt) { + case "visible": + layer.canvas.style.display = val ? "block" : "none"; + break; + } + obj[opt] = val; + }, + } + ), + + // This is where black magic will take place in the future + // A proxy for the canvas object + canvas: new Proxy(document.createElement("canvas"), {}), + + // Activates this layer in the scope + activate: () => { + const scope = findScope(collection); + if (scope) { + scope.active = layer; + console.debug( + `[layers] Layer ${layer._logpath} now active in scope ${scope._logpath}` + ); + } + }, + + // Deactivates this layer in the scope + deactivate: () => { + const scope = findScope(collection); + if (scope && scope.active === layer) scope.active = null; + console.debug(); + }, + }, + _layerlogpath + ); + + // Add to indexers + collection._layers.push(layer); + collection.layers[key] = layer; + + console.info( + `[layers] Layer '${layer.name}' at ${layer._logpath} registered` + ); + return layer; + }, + + // Deletes a layer + deleteLayer: (layer) => { + collection._layers.splice( + collection._layers.findIndex( + (l) => l.id === layer || l.id === layer.id + ), + 1 + ); + if (typeof layer === "object") { + delete collection.layers[layer.id]; + } else if (typeof layer === "string") { + delete collection.layers[layer]; + } + + console.info(`[layers] Layer '${layer}' deleted`); + }, + }, + _logpath + ); + + if (parent) parent[key] = collection; + else layers.collections[key] = collection; + + console.info( + `[layers] Collection '${options.name}' at ${_logpath} registered` + ); + + // If always, we must create a layer to select + if (options.scope && options.scope.always) + collection + .registerLayer(options.scope.always.key, options.scope.always.options) + .activate(); + + return collection; }, }; diff --git a/js/util.d.ts b/js/util.d.ts new file mode 100644 index 0000000..e4cb07f --- /dev/null +++ b/js/util.d.ts @@ -0,0 +1,52 @@ +/** + * Generates a random string in the following format: + * + * xxxx-xxxx-xxxx-...-xxxx + * + * @param size number of character quartets to generate + * @return Generated ID + */ +declare function guid(size: number): string; + +/** + * Sets default values for options parameters + * + * @param options An object received as a parameter + * @param defaults An object with default values for each expected key + * @return The original options parameter + */ +declare function defaultOpt( + options: {[key: string]: any}, + defaults: {[key: string]: any} +): {[key: string]: any}; + +/** + * Sets default values for options parameters + * + * @param options An object received as a parameter + * @param defaults An object with default values for each expected key + * @return The original options parameter + */ +declare function makeReadOnly( + options: {[key: string]: any}, + defaults: {[key: string]: any} +): {[key: string]: any}; + +/** + * Makes an object read-only, throwing an exception when attempting to set + * + * @param obj Object to be proxied + * @param name Name of the object, for logging purposes + * @return The proxied object + */ +declare function makeReadOnly(obj: object, name?: string): object; + +/** + * Makes an object have each key be writeable only once, throwing an exception when + * attempting to set an existing parameter + * + * @param obj Object to be proxied + * @param name Name of the object, for logging purposes + * @return The proxied object + */ +declare function makeWriteOnce(obj: object, name?: string): object;