250c833895
Also updates a lot of other things (brush size now independent from scale factor, split some files, and a lot other things; removed erase safeguard as now erase is supported by undo/redo; tried adding github prettier autoformatting to pull requests; may have some other things as well Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com> Former-commit-id: 0ba21f23c69f9dca2c3189a838b945900b01f81d
300 lines
7.9 KiB
JavaScript
300 lines
7.9 KiB
JavaScript
/**
|
|
* Toolbar
|
|
*/
|
|
|
|
const toolbar = {
|
|
_toolbar: document.getElementById("ui-toolbar"),
|
|
|
|
tools: [],
|
|
|
|
_makeToolbarEntry: (tool) => {
|
|
const toolTitle = document.createElement("img");
|
|
toolTitle.classList.add("tool-icon");
|
|
toolTitle.src = tool.icon;
|
|
|
|
const toolEl = document.createElement("div");
|
|
toolEl.id = `tool-${tool.id}`;
|
|
toolEl.classList.add("tool");
|
|
toolEl.title = tool.name;
|
|
if (tool.options.shortcut) toolEl.title += ` (${tool.options.shortcut})`;
|
|
toolEl.onclick = () => tool.enable();
|
|
|
|
toolEl.appendChild(toolTitle);
|
|
|
|
return toolEl;
|
|
},
|
|
|
|
registerTool(
|
|
icon,
|
|
toolname,
|
|
enable,
|
|
disable,
|
|
options = {
|
|
/**
|
|
* Runs on tool creation. It receives the tool state.
|
|
*
|
|
* Can be used to setup callback functions, for example.
|
|
*/
|
|
init: null,
|
|
/**
|
|
* Function to populate the state menu.
|
|
*
|
|
* It receives a div element (that is the menu) and the current tool state.
|
|
*/
|
|
populateContextMenu: null,
|
|
/**
|
|
* Help description of the tool; for now used for nothing
|
|
*/
|
|
description: "",
|
|
/**
|
|
* Shortcut; Text describing this tool's shortcut access
|
|
*/
|
|
shortcut: "",
|
|
}
|
|
) {
|
|
// Set some defaults
|
|
if (!options.init)
|
|
options.init = (state) => console.debug(`Initialized tool '${toolname}'`);
|
|
|
|
if (!options.populateContextMenu)
|
|
options.populateContextMenu = (menu, state) => {
|
|
const span = document.createElement("span");
|
|
span.textContent = "Tool has no context menu";
|
|
menu.appendChild(span);
|
|
return;
|
|
};
|
|
|
|
// Create tool
|
|
const id = guid();
|
|
|
|
const contextMenuEl = document.getElementById("tool-context");
|
|
|
|
const tool = {
|
|
id,
|
|
icon,
|
|
name: toolname,
|
|
enabled: false,
|
|
_element: null,
|
|
state: {},
|
|
options,
|
|
enable: (opt = null) => {
|
|
this.tools.filter((t) => t.enabled).forEach((t) => t.disable());
|
|
|
|
while (contextMenuEl.lastChild) {
|
|
contextMenuEl.removeChild(contextMenuEl.lastChild);
|
|
}
|
|
options.populateContextMenu(contextMenuEl, tool.state);
|
|
|
|
tool._element && tool._element.classList.add("using");
|
|
tool.enabled = true;
|
|
enable(tool.state, opt);
|
|
},
|
|
disable: (opt = null) => {
|
|
tool._element && tool._element.classList.remove("using");
|
|
disable(tool.state, opt);
|
|
tool.enabled = false;
|
|
},
|
|
};
|
|
|
|
// Initalize
|
|
options.init && options.init(tool.state);
|
|
|
|
this.tools.push(tool);
|
|
|
|
// Add tool to toolbar
|
|
tool._element = this._makeToolbarEntry(tool);
|
|
this._toolbar.appendChild(tool._element);
|
|
|
|
return tool;
|
|
},
|
|
|
|
addSeparator() {
|
|
const separator = document.createElement("div");
|
|
separator.classList.add("separator");
|
|
this._toolbar.appendChild(separator);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Dream and img2img tools
|
|
*/
|
|
const _reticle_draw = (evn, snapToGrid = true) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
const bb = getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
basePixelCount * scaleFactor,
|
|
basePixelCount * scaleFactor,
|
|
snapToGrid && basePixelCount
|
|
);
|
|
|
|
// draw targeting square reticle thingy cursor
|
|
ovCtx.strokeStyle = "#FFF";
|
|
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
|
|
}
|
|
};
|
|
|
|
const tools = {};
|
|
|
|
/**
|
|
* Dream tool
|
|
*/
|
|
tools.dream = toolbar.registerTool(
|
|
"res/icons/image-plus.svg",
|
|
"Dream",
|
|
(state, opt) => {
|
|
// Draw new cursor immediately
|
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
_reticle_draw({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
|
|
|
// Start Listeners
|
|
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
|
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
|
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
|
},
|
|
(state, opt) => {
|
|
// Clear Listeners
|
|
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
|
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
|
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
|
},
|
|
{
|
|
init: (state) => {
|
|
state.snapToGrid = true;
|
|
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
|
|
state.dreamcb = (evn) => {
|
|
dream_generate_callback(evn, state);
|
|
};
|
|
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
|
},
|
|
populateContextMenu: (menu, state) => {
|
|
if (!state.ctxmenu) {
|
|
state.ctxmenu = {};
|
|
// Snap To Grid Checkbox
|
|
const snapToGridCheckbox = document.createElement("input");
|
|
snapToGridCheckbox.type = "checkbox";
|
|
snapToGridCheckbox.checked = state.snapToGrid;
|
|
snapToGridCheckbox.onchange = () =>
|
|
(state.snapToGrid = snapToGridCheckbox.checked);
|
|
state.ctxmenu.snapToGridCheckbox = snapToGridCheckbox;
|
|
|
|
const snapToGridLabel = document.createElement("label");
|
|
snapToGridLabel.appendChild(snapToGridCheckbox);
|
|
snapToGridLabel.appendChild(new Text("Snap to Grid"));
|
|
state.ctxmenu.snapToGridLabel = snapToGridLabel;
|
|
}
|
|
|
|
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
|
},
|
|
shortcut: "D",
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Mask Editing tools
|
|
*/
|
|
toolbar.addSeparator();
|
|
|
|
/**
|
|
* Mask Brush tool
|
|
*/
|
|
tools.maskbrush = toolbar.registerTool(
|
|
"res/icons/paintbrush.svg",
|
|
"Mask Brush",
|
|
(state, opt) => {
|
|
// Draw new cursor immediately
|
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
state.movecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
|
|
|
// Start Listeners
|
|
mouse.listen.canvas.onmousemove.on(state.movecb);
|
|
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
|
mouse.listen.canvas.left.onpaint.on(state.drawcb);
|
|
mouse.listen.canvas.right.onpaint.on(state.erasecb);
|
|
},
|
|
(state, opt) => {
|
|
// Clear Listeners
|
|
mouse.listen.canvas.onmousemove.clear(state.movecb);
|
|
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
|
mouse.listen.canvas.left.onpaint.clear(state.drawcb);
|
|
mouse.listen.canvas.right.onpaint.clear(state.erasecb);
|
|
},
|
|
{
|
|
init: (state) => {
|
|
state.config = {
|
|
brushScrollSpeed: 1 / 4,
|
|
minBrushSize: 10,
|
|
maxBrushSize: 500,
|
|
};
|
|
|
|
state.brushSize = 64;
|
|
state.setBrushSize = (size) => {
|
|
state.brushSize = size;
|
|
state.ctxmenu.brushSizeRange.value = size;
|
|
state.ctxmenu.brushSizeText.value = size;
|
|
};
|
|
|
|
state.movecb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
// draw big translucent red blob cursor
|
|
ovCtx.beginPath();
|
|
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
|
ovCtx.fillStyle = "#FF6A6A50";
|
|
ovCtx.fill();
|
|
}
|
|
};
|
|
|
|
state.wheelcb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
state.setBrushSize(
|
|
Math.max(
|
|
state.config.minBrushSize,
|
|
Math.min(
|
|
state.config.maxBrushSize,
|
|
state.brushSize -
|
|
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
|
)
|
|
)
|
|
);
|
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
state.movecb(evn);
|
|
}
|
|
};
|
|
|
|
state.drawcb = (evn) => mask_brush_draw_callback(evn, state);
|
|
state.erasecb = (evn) => mask_brush_erase_callback(evn, state);
|
|
},
|
|
populateContextMenu: (menu, state) => {
|
|
if (!state.ctxmenu) {
|
|
state.ctxmenu = {};
|
|
// Brush Size slider
|
|
const brushSizeRange = document.createElement("input");
|
|
brushSizeRange.type = "range";
|
|
brushSizeRange.value = state.brushSize;
|
|
brushSizeRange.max = state.config.maxBrushSize;
|
|
brushSizeRange.step = 8;
|
|
brushSizeRange.min = state.config.minBrushSize;
|
|
brushSizeRange.oninput = () =>
|
|
(state.brushSize = parseInt(brushSizeRange.value));
|
|
state.ctxmenu.brushSizeRange = brushSizeRange;
|
|
const brushSizeText = document.createElement("input");
|
|
brushSizeText.type = "number";
|
|
brushSizeText.value = state.brushSize;
|
|
brushSizeText.oninput = () =>
|
|
(state.brushSize = parseInt(brushSizeText.value));
|
|
state.ctxmenu.brushSizeText = brushSizeText;
|
|
|
|
const brushSizeLabel = document.createElement("label");
|
|
brushSizeLabel.appendChild(new Text("Brush Size"));
|
|
brushSizeLabel.appendChild(brushSizeText);
|
|
brushSizeLabel.appendChild(brushSizeRange);
|
|
state.ctxmenu.brushSizeLabel = brushSizeLabel;
|
|
}
|
|
|
|
menu.appendChild(state.ctxmenu.brushSizeLabel);
|
|
},
|
|
shortcut: "M",
|
|
}
|
|
);
|
|
|
|
toolbar.tools[0].enable();
|