d42837d597
Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
470 lines
13 KiB
JavaScript
470 lines
13 KiB
JavaScript
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.save();
|
|
ctx.strokeStyle = "black";
|
|
|
|
ctx.filter = "blur(" + state.brushBlur + "px)";
|
|
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.restore();
|
|
};
|
|
|
|
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",
|
|
}
|
|
);
|