/** * 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();