f8687e9bac
performance optimization for firefox seems to have broken chrome after today's changes. Seems like we will have to accept the slower method of calculating coordinates. At least it is more flexible. Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
321 lines
8.1 KiB
JavaScript
321 lines
8.1 KiB
JavaScript
/**
|
|
* This is a manager for the many canvas and content layers that compose the application
|
|
*
|
|
* It manages canvases and their locations and sizes according to current viewport views
|
|
*/
|
|
const layers = {
|
|
_collections: [],
|
|
collections: makeWriteOnce({}, "layers.collections"),
|
|
|
|
listen: {
|
|
oncollectioncreate: new Observer(),
|
|
oncollectiondelete: new Observer(),
|
|
|
|
onlayercreate: new Observer(),
|
|
onlayerdelete: new Observer(),
|
|
},
|
|
|
|
// Registers a new collection
|
|
// Layer collections are a group of layers (canvases) that are rendered in tandem. (same width, height, position, transform, etc)
|
|
registerCollection: (key, size, options = {}) => {
|
|
defaultOpt(options, {
|
|
// Display name for the collection
|
|
name: key,
|
|
|
|
// Initial layer
|
|
initLayer: {
|
|
key: "default",
|
|
options: {},
|
|
},
|
|
|
|
// Input multiplier (Size of the input element div)
|
|
inputSizeMultiplier: 3,
|
|
|
|
// Target
|
|
targetElement: document.getElementById("layer-render"),
|
|
|
|
// Resolution of the image
|
|
resolution: size,
|
|
});
|
|
|
|
if (options.inputSizeMultiplier % 2 === 0) options.inputSizeMultiplier++;
|
|
|
|
// Path used for logging purposes
|
|
const _logpath = "layers.collections." + key;
|
|
|
|
// Collection ID
|
|
const id = guid();
|
|
|
|
// Collection element
|
|
const element = document.createElement("div");
|
|
element.id = `collection-${id}`;
|
|
element.style.width = `${size.w}px`;
|
|
element.style.height = `${size.h}px`;
|
|
element.classList.add("collection");
|
|
|
|
// Input element (overlay element for input handling)
|
|
const inputel = document.createElement("div");
|
|
inputel.id = `collection-input-${id}`;
|
|
inputel.addEventListener("mouseover", (evn) => {
|
|
document.activeElement.blur();
|
|
});
|
|
inputel.classList.add("collection-input-overlay");
|
|
element.appendChild(inputel);
|
|
|
|
options.targetElement.appendChild(element);
|
|
|
|
const collection = makeWriteOnce(
|
|
{
|
|
id,
|
|
|
|
_logpath,
|
|
|
|
_layers: [],
|
|
layers: {},
|
|
|
|
name: options.name,
|
|
element,
|
|
inputElement: inputel,
|
|
_inputOffset: null,
|
|
get inputOffset() {
|
|
return this._inputOffset;
|
|
},
|
|
|
|
_resizeInputDiv() {
|
|
// Set offset
|
|
this._inputOffset = {
|
|
x: -Math.floor(options.inputSizeMultiplier / 2) * size.w,
|
|
y: -Math.floor(options.inputSizeMultiplier / 2) * size.h,
|
|
};
|
|
|
|
// Resize the input element
|
|
this.inputElement.style.left = `${this.inputOffset.x}px`;
|
|
this.inputElement.style.top = `${this.inputOffset.y}px`;
|
|
this.inputElement.style.width = `${
|
|
size.w * options.inputSizeMultiplier
|
|
}px`;
|
|
this.inputElement.style.height = `${
|
|
size.h * options.inputSizeMultiplier
|
|
}px`;
|
|
},
|
|
|
|
size,
|
|
resolution: options.resolution,
|
|
|
|
/**
|
|
* Registers a new layer
|
|
*
|
|
* @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer.
|
|
* @param {object} options
|
|
* @param {string} options.name
|
|
* @param {?BoundingBox} options.bb
|
|
* @param {{w: number, h: number}} options.resolution
|
|
* @param {?string} options.group
|
|
* @param {object} options.after
|
|
* @param {object} options.ctxOptions
|
|
* @returns
|
|
*/
|
|
registerLayer: (key = null, options = {}) => {
|
|
// Make ID
|
|
const id = guid();
|
|
|
|
defaultOpt(options, {
|
|
// Display name for the layer
|
|
name: key || `Temporary ${id}`,
|
|
|
|
// Bounding box for layer
|
|
bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h},
|
|
|
|
// Resolution for layer
|
|
resolution: null,
|
|
|
|
// Group for the layer ("group/subgroup/subsubgroup")
|
|
group: null,
|
|
|
|
// If set, will insert the layer after the given one
|
|
after: null,
|
|
|
|
// Context creation options
|
|
ctxOptions: {},
|
|
});
|
|
|
|
// Calculate resolution
|
|
if (!options.resolution)
|
|
options.resolution = {
|
|
w: (collection.resolution.w / collection.size.w) * options.bb.w,
|
|
h: (collection.resolution.h / collection.size.h) * options.bb.h,
|
|
};
|
|
|
|
// This layer's canvas
|
|
// This is where black magic will take place in the future
|
|
/**
|
|
* @todo Use the canvas black arts to auto-scale canvas
|
|
*/
|
|
const canvas = document.createElement("canvas");
|
|
canvas.id = `layer-${id}`;
|
|
|
|
canvas.style.left = `${options.bb.x}px`;
|
|
canvas.style.top = `${options.bb.y}px`;
|
|
canvas.style.width = `${options.bb.w}px`;
|
|
canvas.style.height = `${options.bb.h}px`;
|
|
canvas.width = options.resolution.w;
|
|
canvas.height = options.resolution.h;
|
|
|
|
if (!options.after) collection.element.appendChild(canvas);
|
|
else {
|
|
options.after.canvas.after(canvas);
|
|
}
|
|
|
|
const ctx = canvas.getContext("2d", options.ctxOptions);
|
|
|
|
// Path used for logging purposes
|
|
const _layerlogpath = key
|
|
? _logpath + ".layers." + key
|
|
: _logpath + ".layers[" + id + "]";
|
|
const layer = makeWriteOnce(
|
|
{
|
|
_logpath: _layerlogpath,
|
|
_collection: collection,
|
|
|
|
id,
|
|
key,
|
|
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;
|
|
},
|
|
}
|
|
),
|
|
|
|
/** Our canvas */
|
|
canvas,
|
|
ctx,
|
|
|
|
/**
|
|
* Moves this layer to another level (after given layer)
|
|
*
|
|
* @param {Layer} layer Will move layer to after this one
|
|
*/
|
|
moveAfter(layer) {
|
|
layer.canvas.after(this.canvas);
|
|
},
|
|
|
|
/**
|
|
* Moves this layer to another level (before given layer)
|
|
*
|
|
* @param {Layer} layer Will move layer to before this one
|
|
*/
|
|
moveBefore(layer) {
|
|
layer.canvas.before(this.canvas);
|
|
},
|
|
|
|
/**
|
|
* Moves this layer to another location
|
|
*
|
|
* @param {number} x X coordinate of the top left of the canvas
|
|
* @param {number} y Y coordinate of the top left of the canvas
|
|
*/
|
|
moveTo(x, y) {
|
|
canvas.style.left = `${x}px`;
|
|
canvas.style.top = `${y}px`;
|
|
},
|
|
|
|
/**
|
|
* Resizes layer in place
|
|
*
|
|
* @param {number} w New width
|
|
* @param {number} h New height
|
|
*/
|
|
resize(w, h) {
|
|
canvas.width = Math.round(
|
|
options.resolution.w * (w / options.bb.w)
|
|
);
|
|
canvas.height = Math.round(
|
|
options.resolution.h * (h / options.bb.h)
|
|
);
|
|
canvas.style.width = `${w}px`;
|
|
canvas.style.height = `${h}px`;
|
|
},
|
|
|
|
// Hides this layer (don't draw)
|
|
hide() {
|
|
this.canvas.style.display = "none";
|
|
},
|
|
// Hides this layer (don't draw)
|
|
unhide() {
|
|
this.canvas.style.display = "block";
|
|
},
|
|
},
|
|
_layerlogpath
|
|
);
|
|
|
|
// Add to indexers
|
|
if (!options.after) collection._layers.push(layer);
|
|
else {
|
|
const index = collection._layers.findIndex(
|
|
(l) => l === options.after
|
|
);
|
|
collection._layers.splice(index, 0, layer);
|
|
}
|
|
if (key) collection.layers[key] = layer;
|
|
|
|
if (key === null)
|
|
console.debug(
|
|
`[layers] Anonymous layer '${layer.name}' registered`
|
|
);
|
|
else
|
|
console.info(
|
|
`[layers] Layer '${layer.name}' at ${layer._logpath} registered`
|
|
);
|
|
|
|
layers.listen.onlayercreate.emit({
|
|
layer,
|
|
});
|
|
return layer;
|
|
},
|
|
|
|
// Deletes a layer
|
|
deleteLayer: (layer) => {
|
|
const lobj = collection._layers.splice(
|
|
collection._layers.findIndex(
|
|
(l) => l.id === layer || l.id === layer.id
|
|
),
|
|
1
|
|
)[0];
|
|
if (!lobj) return;
|
|
|
|
layers.listen.onlayerdelete.emit({
|
|
layer: lobj,
|
|
});
|
|
if (lobj.key) delete collection.layers[lobj.key];
|
|
|
|
collection.element.removeChild(lobj.canvas);
|
|
|
|
if (lobj.key) console.info(`[layers] Layer '${lobj.key}' deleted`);
|
|
else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`);
|
|
},
|
|
},
|
|
_logpath,
|
|
["_inputOffset"]
|
|
);
|
|
|
|
collection._resizeInputDiv();
|
|
|
|
layers._collections.push(collection);
|
|
layers.collections[key] = collection;
|
|
|
|
console.info(
|
|
`[layers] Collection '${options.name}' at ${_logpath} registered`
|
|
);
|
|
|
|
return collection;
|
|
},
|
|
};
|