const _color_brush_draw_callback = (evn, state) => { const ctx = state.drawLayer.ctx; ctx.strokeStyle = state.color; ctx.filter = "blur(" + state.brushBlur + "px) opacity(" + state.brushOpacity * 100 + "%)"; ctx.lineWidth = state.brushSize; ctx.beginPath(); ctx.moveTo( evn.px === undefined ? evn.x : evn.px, evn.py === undefined ? evn.y : evn.py ); ctx.lineTo(evn.x, evn.y); ctx.lineJoin = ctx.lineCap = "round"; ctx.stroke(); ctx.filter = null; }; const _color_brush_erase_callback = (evn, state, ctx) => { ctx.strokeStyle = "black"; ctx.lineWidth = state.brushSize; ctx.beginPath(); ctx.moveTo( evn.px === undefined ? evn.x : evn.px, evn.py === undefined ? evn.y : evn.py ); ctx.lineTo(evn.x, evn.y); ctx.lineJoin = ctx.lineCap = "round"; ctx.stroke(); }; const colorBrushTool = () => toolbar.registerTool( "./res/icons/brush.svg", "Color Brush", (state, opt) => { // Draw new cursor immediately uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.movecb({ ...mouse.coords.world.pos, evn: { clientX: mouse.coords.window.pos.x, clientY: mouse.coords.window.pos.y, }, }); // Layer for eyedropper magnifiying glass state.glassLayer = imageCollection.registerLayer(null, { bb: {x: 0, y: 0, w: 100, h: 100}, resolution: {w: 7, h: 7}, after: maskPaintLayer, }); state.glassLayer.hide(); state.glassLayer.canvas.style.imageRendering = "pixelated"; state.glassLayer.canvas.style.borderRadius = "50%"; state.drawLayer = imageCollection.registerLayer(null, { after: imgLayer, category: "display", ctxOptions: {willReadFrequently: true}, }); state.drawLayer.canvas.style.filter = "opacity(70%)"; state.eraseLayer = imageCollection.registerLayer(null, { after: imgLayer, category: "processing", ctxOptions: {willReadFrequently: true}, }); state.eraseLayer.hide(); state.eraseBackup = imageCollection.registerLayer(null, { after: imgLayer, category: "processing", }); state.eraseBackup.hide(); // Start Listeners mouse.listen.world.onmousemove.on(state.movecb); mouse.listen.world.onwheel.on(state.wheelcb); keyboard.listen.onkeydown.on(state.keydowncb); keyboard.listen.onkeyup.on(state.keyupcb); mouse.listen.world.btn.left.onclick.on(state.leftclickcb); mouse.listen.world.btn.left.onpaintstart.on(state.drawstartcb); mouse.listen.world.btn.left.onpaint.on(state.drawcb); mouse.listen.world.btn.left.onpaintend.on(state.drawendcb); mouse.listen.world.btn.right.onpaintstart.on(state.erasestartcb); mouse.listen.world.btn.right.onpaint.on(state.erasecb); mouse.listen.world.btn.right.onpaintend.on(state.eraseendcb); // Display Color setMask("none"); }, (state, opt) => { // Clear Listeners mouse.listen.world.onmousemove.clear(state.movecb); mouse.listen.world.onwheel.clear(state.wheelcb); keyboard.listen.onkeydown.clear(state.keydowncb); keyboard.listen.onkeyup.clear(state.keyupcb); mouse.listen.world.btn.left.onclick.clear(state.leftclickcb); mouse.listen.world.btn.left.onpaintstart.clear(state.drawstartcb); mouse.listen.world.btn.left.onpaint.clear(state.drawcb); mouse.listen.world.btn.left.onpaintend.clear(state.drawendcb); mouse.listen.world.btn.right.onpaintstart.clear(state.erasestartcb); mouse.listen.world.btn.right.onpaint.clear(state.erasecb); mouse.listen.world.btn.right.onpaintend.clear(state.eraseendcb); // Delete layer imageCollection.deleteLayer(state.drawLayer); imageCollection.deleteLayer(state.eraseBackup); imageCollection.deleteLayer(state.eraseLayer); imageCollection.deleteLayer(state.glassLayer); // Cancel any eyedropping state.drawing = false; state.disableDropper(); uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, { init: (state) => { state.config = { brushScrollSpeed: 1 / 5, minBrushSize: 2, maxBrushSize: 500, minBlur: 0, maxBlur: 30, }; state.color = "#FFFFFF"; state.brushSize = 32; state.brushBlur = 0; state.brushOpacity = 1; state.affectMask = true; state.block_res_change = true; state.setBrushSize = (size) => { state.brushSize = size; state.ctxmenu.brushSizeRange.value = size; state.ctxmenu.brushSizeText.value = size; }; state.eyedropper = false; state.enableDropper = () => { state.eyedropper = true; state.movecb(lastMouseMoveEvn); state.glassLayer.unhide(); }; state.disableDropper = () => { state.eyedropper = false; state.movecb(lastMouseMoveEvn); state.glassLayer.hide(); }; let lastMouseMoveEvn = {x: 0, y: 0}; state.movecb = (evn) => { lastMouseMoveEvn = evn; const vcp = {x: evn.evn.clientX, y: evn.evn.clientY}; // draw drawing cursor uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); uiCtx.beginPath(); uiCtx.arc( vcp.x, vcp.y, (state.eyedropper ? 50 : state.brushSize / 2) / viewport.zoom, 0, 2 * Math.PI, true ); uiCtx.strokeStyle = "black"; uiCtx.stroke(); // Draw eyedropper cursor and magnifiying glass if (state.eyedropper) { const bb = getBoundingBox(evn.x, evn.y, 7, 7, false); const canvas = uil.getVisible(bb, {includeBg: true}); state.glassLayer.ctx.clearRect(0, 0, 7, 7); state.glassLayer.ctx.drawImage(canvas, 0, 0); state.glassLayer.moveTo(evn.x - 50, evn.y - 50); } else { uiCtx.beginPath(); uiCtx.arc( vcp.x, vcp.y, state.brushSize / (2 * viewport.zoom), 0, 2 * Math.PI, true ); uiCtx.fillStyle = state.color + "50"; uiCtx.fill(); } }; state.wheelcb = (evn) => { state.brushSize = state.setBrushSize( state.brushSize - Math.floor(state.config.brushScrollSpeed * evn.delta) ); uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.movecb(evn); }; /** * These are basically for eyedropper purposes */ state.keydowncb = (evn) => { if (lastMouseMoveEvn.target === imageCollection.inputElement) switch (evn.code) { case "ShiftLeft": case "ShiftRight": state.enableDropper(); break; } }; state.keyupcb = (evn) => { switch (evn.code) { case "ShiftLeft": case "ShiftRight": if (!keyboard.isPressed(evn.code)) { state.disableDropper(); } break; } }; state.leftclickcb = (evn) => { if (evn.target === imageCollection.inputElement && state.eyedropper) { const bb = getBoundingBox(evn.x, evn.y, 1, 1, false); const visibleCanvas = uil.getVisible(bb); const dat = visibleCanvas .getContext("2d") .getImageData(0, 0, 1, 1).data; state.setColor( "#" + ((dat[0] << 16) | (dat[1] << 8) | dat[2]).toString(16) ); state.disableDropper(); } }; state.rightclickcb = (evn) => { if (evn.target === imageCollection.inputElement && state.eyedropper) { state.disableDropper(); } }; /** * Here we actually paint things */ state.drawstartcb = (evn) => { if (state.eyedropper) return; state.drawing = true; if (state.affectMask) _mask_brush_draw_callback(evn, state); _color_brush_draw_callback(evn, state); }; state.drawcb = (evn) => { if (state.eyedropper || !state.drawing) return; if (state.affectMask) _mask_brush_draw_callback(evn, state); _color_brush_draw_callback(evn, state); }; state.drawendcb = (evn) => { if (!state.drawing) return; state.drawing = false; const canvas = state.drawLayer.canvas; const ctx = state.drawLayer.ctx; const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; commands.runCommand( "drawImage", "Color Brush Draw", { image: cropped.canvas, ...bb, }, { extra: { log: `Color brush drawn at x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h}`, }, } ); ctx.clearRect(bb.x, bb.y, bb.w, bb.h); }; state.erasestartcb = (evn) => { if (state.eyedropper) return; state.erasing = true; if (state.affectMask) _mask_brush_erase_callback(evn, state); // Make a backup of the current image to apply erase later const bkpctx = state.eraseBackup.ctx; state.eraseBackup.clear(); bkpctx.drawImageRoot(uil.canvas, 0, 0); uil.ctx.globalCompositeOperation = "destination-out"; _color_brush_erase_callback(evn, state, uil.ctx); uil.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; state.erasecb = (evn) => { if (state.eyedropper || !state.erasing) return; if (state.affectMask) _mask_brush_erase_callback(evn, state); uil.ctx.globalCompositeOperation = "destination-out"; _color_brush_erase_callback(evn, state, uil.ctx); uil.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; state.eraseendcb = (evn) => { if (!state.erasing) return; state.erasing = false; const canvas = state.eraseLayer.canvas; const ctx = state.eraseLayer.ctx; const bkpcanvas = state.eraseBackup.canvas; const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; uil.ctx.filter = null; uil.layer.clear(); uil.ctx.drawImageRoot(bkpcanvas, 0, 0); commands.runCommand( "eraseImage", "Color Brush Erase", { mask: cropped.canvas, ...bb, }, { extra: { log: `Color brush erase at x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h}`, }, } ); ctx.clearRect(bb.x, bb.y, bb.w, bb.h); }; }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { state.ctxmenu = {}; // Affects Mask Checkbox const array = document.createElement("div"); const affectMaskCheckbox = _toolbar_input.checkbox( state, "affectMask", "Affect Mask", "icon-venetian-mask" ).checkbox; array.appendChild(affectMaskCheckbox); state.ctxmenu.affectMaskCheckbox = array; // Brush size slider const brushSizeSlider = _toolbar_input.slider( state, "brushSize", "Brush Size", { min: state.config.minBrushSize, max: state.config.maxBrushSize, step: 5, textStep: 1, } ); state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; // Brush opacity slider const brushOpacitySlider = _toolbar_input.slider( state, "brushOpacity", "Brush Opacity", { min: 0, max: 1, step: 0.05, textStep: 0.001, } ); state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider; // Brush blur slider const brushBlurSlider = _toolbar_input.slider( state, "brushBlur", "Brush Blur", { min: state.config.minBlur, max: state.config.maxBlur, step: 1, } ); state.ctxmenu.brushBlurSlider = brushBlurSlider.slider; // Brush color const brushColorPickerWrapper = document.createElement("div"); brushColorPickerWrapper.classList.add( "brush-color-picker", "wrapper" ); const brushColorPicker = document.createElement("input"); brushColorPicker.classList.add("brush-color-picker", "picker"); brushColorPicker.type = "color"; brushColorPicker.value = state.color; brushColorPicker.addEventListener("input", (evn) => { state.color = evn.target.value; }); state.setColor = (color) => { brushColorPicker.value = color; state.color = brushColorPicker.value; }; const brushColorEyeDropper = document.createElement("button"); brushColorEyeDropper.classList.add( "brush-color-picker", "eyedropper" ); brushColorEyeDropper.addEventListener("click", () => { if (state.eyedropper) state.disableDropper(); else state.enableDropper(); }); brushColorPickerWrapper.appendChild(brushColorPicker); brushColorPickerWrapper.appendChild(brushColorEyeDropper); state.ctxmenu.brushColorPicker = brushColorPickerWrapper; } menu.appendChild(state.ctxmenu.affectMaskCheckbox); menu.appendChild(state.ctxmenu.brushSizeSlider); menu.appendChild(state.ctxmenu.brushOpacitySlider); menu.appendChild(state.ctxmenu.brushBlurSlider); menu.appendChild(state.ctxmenu.brushColorPicker); }, shortcut: "C", } );