Skip to content

Commit bf8382a

Browse files
authored
feat: add line drawing tool (#52)
1 parent 5c32f94 commit bf8382a

7 files changed

Lines changed: 106 additions & 20 deletions

File tree

celstomp/celstomp-app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@
358358
bctx.fillRect(0, 0, contentW, contentH);
359359
bctx.strokeRect(0, 0, contentW, contentH);
360360
drawRectSelectionOverlay(fxctx);
361+
drawLineToolPreview(fxctx);
361362
}
362363

363364
function onionCompositeOperation() {
@@ -404,6 +405,9 @@
404405
function clearFx() {
405406
fxctx.setTransform(1, 0, 0, 1, 0, 0);
406407
fxctx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);
408+
setTransform(fxctx);
409+
drawRectSelectionOverlay(fxctx);
410+
drawLineToolPreview(fxctx);
407411
}
408412

409413
function wireBrushButtonRightClick() {

celstomp/css/components/tools.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@
126126
cursor: pointer;
127127
}
128128

129+
#islandToolsSlot #toolSeg > label svg {
130+
width: 20px;
131+
height: 20px;
132+
color: rgba(255,255,255,0.85);
133+
position: relative;
134+
z-index: 1;
135+
}
136+
137+
#islandToolsSlot #toolSeg > label:has(svg) {
138+
background-image: none;
139+
}
140+
141+
#islandToolsSlot #toolSeg > label:has(svg)::before {
142+
display: none;
143+
}
144+
129145
#islandToolsSlot #toolSeg > label::before{
130146
content: "";
131147
width: 20px;

celstomp/js/input/pointer-events.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ let brushSize = 3;
1212
let autofill = false;
1313

1414
let trailPoints = [];
15+
let lineToolStart = null;
16+
let lineToolPreview = null;
1517

1618
function pressure(e) {
1719
const pid = Number.isFinite(e?.pointerId) ? e.pointerId : -1;
@@ -338,6 +340,17 @@ function startStroke(e) {
338340
startPan(e);
339341
return;
340342
}
343+
if (tool === "line") {
344+
isDrawing = true;
345+
const hex = colorToHex(currentColor);
346+
strokeHex = activeLayer === LAYER.FILL ? fillWhite : hex;
347+
activeSubColor[activeLayer] = strokeHex;
348+
ensureSublayer(activeLayer, strokeHex);
349+
renderLayerSwatches(activeLayer);
350+
lineToolStart = { x, y };
351+
lineToolPreview = { x, y };
352+
return;
353+
}
341354
if (activeLayer === PAPER_LAYER) {
342355
return;
343356
}
@@ -404,6 +417,11 @@ function continueStroke(e) {
404417
};
405418
return;
406419
}
420+
if (tool === "line") {
421+
lineToolPreview = { x, y };
422+
queueRenderAll();
423+
return;
424+
}
407425
if (tool === "fill-eraser" || tool === "fill-brush") {
408426
fxTransform();
409427
fxStamp1px(lastPt.x, lastPt.y, x, y);
@@ -474,6 +492,25 @@ function endStroke() {
474492
stabilizedPt = null;
475493
return;
476494
}
495+
if (tool === "line" && lineToolStart && lineToolPreview) {
496+
const hex = strokeHex || activeSubColor?.[activeLayer] || colorToHex(currentColor);
497+
const off = getFrameCanvas(activeLayer, currentFrame, hex);
498+
const ctx = off.getContext("2d");
499+
ctx.lineCap = "round";
500+
ctx.lineJoin = "round";
501+
ctx.lineWidth = brushSize;
502+
ctx.strokeStyle = hex;
503+
ctx.beginPath();
504+
ctx.moveTo(lineToolStart.x, lineToolStart.y);
505+
ctx.lineTo(lineToolPreview.x, lineToolPreview.y);
506+
ctx.stroke();
507+
markFrameHasContent(activeLayer, currentFrame, hex);
508+
lineToolStart = null;
509+
lineToolPreview = null;
510+
queueRenderAll();
511+
updateTimelineHasContent(currentFrame);
512+
return;
513+
}
477514
if (tool === "lasso-erase" && lassoActive) {
478515
lassoActive = false;
479516
applyLassoErase();
@@ -843,6 +880,20 @@ function drawRectSelectionOverlay(ctx) {
843880
ctx.strokeRect(rectSelection.x, rectSelection.y, rectSelection.w, rectSelection.h);
844881
ctx.restore();
845882
}
883+
function drawLineToolPreview(ctx) {
884+
if (!lineToolStart || !lineToolPreview) return;
885+
ctx.save();
886+
ctx.lineCap = "round";
887+
ctx.lineJoin = "round";
888+
ctx.lineWidth = brushSize;
889+
ctx.strokeStyle = currentColor;
890+
ctx.globalAlpha = 0.5;
891+
ctx.beginPath();
892+
ctx.moveTo(lineToolStart.x, lineToolStart.y);
893+
ctx.lineTo(lineToolPreview.x, lineToolPreview.y);
894+
ctx.stroke();
895+
ctx.restore();
896+
}
846897
function beginRectSelect(e) {
847898
if (activeLayer === PAPER_LAYER) return;
848899
const pos = getCanvasPointer(e);

celstomp/js/tools/brush-helper.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ function getBrushSizeForPreview(toolKind) {
315315
function updateBrushPreview() {
316316
if (!_brushPrevEl || !_brushPrevCanvas) return;
317317
const toolKind = getActiveToolKindForPreview();
318-
const isBrush = toolKind === "brush";
318+
const showForTools = toolKind === "brush" || toolKind === "eraser" || toolKind === "line";
319319
const isEraser = toolKind === "eraser";
320-
if (!isBrush && !isEraser) {
320+
if (!showForTools) {
321321
_brushPrevEl.style.display = "none";
322322
return;
323323
}

celstomp/js/ui/interaction-shortcuts.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -641,12 +641,13 @@ function wireKeyboardShortcuts() {
641641
const toolByKey = {
642642
1: "brush",
643643
2: "eraser",
644-
3: "fill-brush",
645-
4: "fill-eraser",
646-
5: "lasso-fill",
647-
6: "lasso-erase",
648-
7: "rect-select",
649-
8: "eyedropper"
644+
3: "line",
645+
4: "fill-brush",
646+
5: "fill-eraser",
647+
6: "lasso-fill",
648+
7: "lasso-erase",
649+
8: "rect-select",
650+
9: "eyedropper"
650651
};
651652
document.addEventListener("keydown", e => {
652653
if (e.defaultPrevented) return;
@@ -731,42 +732,49 @@ function onWindowKeyDown(e) {
731732
});
732733
}
733734
if (isDigit(3)) {
735+
e.preventDefault();
736+
pickTool({
737+
id: "tool-line",
738+
value: "line"
739+
});
740+
}
741+
if (isDigit(4)) {
734742
e.preventDefault();
735743
pickTool({
736744
id: "tool-fillbrush",
737745
value: "fill-brush"
738746
});
739747
}
740-
if (isDigit(4)) {
748+
if (isDigit(5)) {
741749
e.preventDefault();
742750
pickTool({
743751
id: "tool-filleraser",
744752
value: "fill-eraser"
745753
});
746754
}
747-
if (isDigit(5)) {
755+
if (isDigit(6)) {
748756
e.preventDefault();
749757
pickTool({
750758
id: "tool-lassoFill",
751759
value: "lasso-fill"
752760
});
753761
}
754-
if (isDigit(6)) {
762+
if (isDigit(7)) {
755763
e.preventDefault();
756764
pickTool({
757765
id: "tool-lassoErase",
758766
altIds: [ "tool-lassoerase", "tool-lasso-erase" ],
759767
value: "lasso-erase"
760768
});
761769
}
762-
if (isDigit(7)) {
770+
if (isDigit(8)) {
763771
e.preventDefault();
764772
pickTool({
765773
id: "tool-rectSelect",
766774
value: "rect-select"
767775
});
768776
}
769-
if (isDigit(8)) {
777+
if (isDigit(9)) {
770778
e.preventDefault();
771779
pickTool({
772780
id: "tool-eyedropper",

celstomp/js/ui/ui-components.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const tools = [
55
{ id: 'tool-brush', val: 'brush', label: 'Brush', checked: true },
66
{ id: 'tool-eraser', val: 'eraser', label: 'Eraser' },
7+
{ id: 'tool-line', val: 'line', label: 'Line', icon: '<svg viewBox="0 0 24 24" width="18" height="18"><line x1="4" y1="20" x2="20" y2="4" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>' },
78
{ id: 'tool-fillbrush', val: 'fill-brush', label: 'Fill Brush' },
89
{ id: 'tool-filleraser', val: 'fill-eraser', label: 'Eraser Fill' },
910
{ id: 'tool-lassoFill', val: 'lasso-fill', label: 'Lasso Fill' },
@@ -27,7 +28,12 @@
2728
const lbl = document.createElement('label');
2829
lbl.htmlFor = t.id;
2930
lbl.dataset.tool = t.val;
30-
lbl.textContent = t.label;
31+
lbl.setAttribute('aria-label', t.label);
32+
if (t.icon) {
33+
lbl.innerHTML = t.icon;
34+
} else {
35+
lbl.textContent = t.label;
36+
}
3137

3238
if (t.val === 'brush') lbl.id = 'toolBrushLabel';
3339
if (t.val === 'eraser') lbl.id = 'toolEraserLabel';

celstomp/parts/modals.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ document.getElementById('part-modals').innerHTML = `
5252
<h4>Tools</h4>
5353
<div class="shortcutRow"><kbd>1</kbd><span>Brush</span></div>
5454
<div class="shortcutRow"><kbd>2</kbd><span>Eraser</span></div>
55-
<div class="shortcutRow"><kbd>3</kbd><span>Fill Brush</span></div>
56-
<div class="shortcutRow"><kbd>4</kbd><span>Fill Eraser</span></div>
57-
<div class="shortcutRow"><kbd>5</kbd><span>Lasso Fill</span></div>
58-
<div class="shortcutRow"><kbd>6</kbd><span>Lasso Erase</span></div>
59-
<div class="shortcutRow"><kbd>7</kbd><span>Rect Select</span></div>
60-
<div class="shortcutRow"><kbd>8</kbd><span>Eyedropper</span></div>
55+
<div class="shortcutRow"><kbd>3</kbd><span>Line</span></div>
56+
<div class="shortcutRow"><kbd>4</kbd><span>Fill Brush</span></div>
57+
<div class="shortcutRow"><kbd>5</kbd><span>Fill Eraser</span></div>
58+
<div class="shortcutRow"><kbd>6</kbd><span>Lasso Fill</span></div>
59+
<div class="shortcutRow"><kbd>7</kbd><span>Lasso Erase</span></div>
60+
<div class="shortcutRow"><kbd>8</kbd><span>Rect Select</span></div>
61+
<div class="shortcutRow"><kbd>9</kbd><span>Eyedropper</span></div>
6162
</div>
6263
<div class="shortcutSection">
6364
<h4>Navigation</h4>

0 commit comments

Comments
 (0)