Skip to content

Commit 5f1609e

Browse files
authored
Add rectangle drawing tool (#53)
1 parent bf8382a commit 5f1609e

6 files changed

Lines changed: 113 additions & 28 deletions

File tree

celstomp/celstomp-app.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@
149149
const eraserSizeInput = $("eraserSize");
150150
const toolOpacityRange = $("toolOpacityRange");
151151
const toolAngleRange = $("toolAngleRange");
152+
const toolOpacityRow = toolOpacityRange?.closest(".sideRangeRow") || null;
153+
const toolAngleRow = toolAngleRange?.closest(".sideRangeRow") || null;
154+
const brushFoldSection = toolFoldBrushesBtn?.closest(".toolFold") || null;
152155
const eraserVal = $("eraserVal");
153156

154157

@@ -183,16 +186,25 @@
183186
function refreshToolSettingsUI() {
184187
const isBrush = tool === "brush";
185188
const isEraser = tool === "eraser";
186-
if (toolSettingsSection) toolSettingsSection.hidden = !(isBrush || isEraser);
187-
if (!isBrush && !isEraser) return;
189+
const isLine = tool === "line";
190+
const isRect = tool === "rect";
191+
const isShapeTool = isLine || isRect;
192+
const showsBrushSettings = isBrush || isShapeTool;
193+
if (toolSettingsSection) toolSettingsSection.hidden = !(showsBrushSettings || isEraser);
194+
if (brushFoldSection) brushFoldSection.hidden = isShapeTool;
195+
if (toolOpacityRow) toolOpacityRow.hidden = isShapeTool;
196+
if (toolAngleRow) toolAngleRow.hidden = isShapeTool;
197+
if (!showsBrushSettings && !isEraser) return;
188198
const s = isEraser ? eraserSettings : brushSettings;
189-
if (toolSettingsTitle) toolSettingsTitle.textContent = isEraser ? "Eraser" : "Brushes";
199+
if (toolSettingsTitle) toolSettingsTitle.textContent = isEraser ? "Eraser" : isShapeTool ? "Shape Tool" : "Brushes";
190200
safeSetValue(brushSizeInput, s.size);
191201
safeSetValue(brushSizeNumInput, s.size);
192202
safeSetValue(toolOpacityRange, Math.round(s.opacity * 100));
193203
safeSetValue(toolAngleRange, s.angle);
194-
const activeShape = document.querySelector('input[name="brushShape"][value="' + s.shape + '"]');
195-
if (activeShape) activeShape.checked = true;
204+
if (!isShapeTool) {
205+
const activeShape = document.querySelector('input[name="brushShape"][value="' + s.shape + '"]');
206+
if (activeShape) activeShape.checked = true;
207+
}
196208
}
197209
function setFoldExpanded(btn, body, open) {
198210
if (!btn || !body) return;
@@ -359,6 +371,7 @@
359371
bctx.strokeRect(0, 0, contentW, contentH);
360372
drawRectSelectionOverlay(fxctx);
361373
drawLineToolPreview(fxctx);
374+
drawRectToolPreview(fxctx);
362375
}
363376

364377
function onionCompositeOperation() {
@@ -408,6 +421,7 @@
408421
setTransform(fxctx);
409422
drawRectSelectionOverlay(fxctx);
410423
drawLineToolPreview(fxctx);
424+
drawRectToolPreview(fxctx);
411425
}
412426

413427
function wireBrushButtonRightClick() {

celstomp/js/input/pointer-events.js

Lines changed: 65 additions & 4 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 rectToolStart = null;
16+
let rectToolPreview = null;
1517
let lineToolStart = null;
1618
let lineToolPreview = null;
1719

@@ -268,6 +270,18 @@ function startStroke(e) {
268270
pickCanvasColorAtEvent(e);
269271
return;
270272
}
273+
if (tool === "rect") {
274+
isDrawing = true;
275+
const hex = colorToHex(currentColor);
276+
strokeHex = activeLayer === LAYER.FILL ? fillWhite : hex;
277+
activeSubColor[activeLayer] = strokeHex;
278+
ensureSublayer(activeLayer, strokeHex);
279+
renderLayerSwatches(activeLayer);
280+
beginGlobalHistoryStep(activeLayer, currentFrame, strokeHex);
281+
rectToolStart = { x, y };
282+
rectToolPreview = { x, y };
283+
return;
284+
}
271285
if (tool === "rect-select") {
272286
isDrawing = true;
273287
beginRectSelect(e);
@@ -347,6 +361,7 @@ function startStroke(e) {
347361
activeSubColor[activeLayer] = strokeHex;
348362
ensureSublayer(activeLayer, strokeHex);
349363
renderLayerSwatches(activeLayer);
364+
beginGlobalHistoryStep(activeLayer, currentFrame, strokeHex);
350365
lineToolStart = { x, y };
351366
lineToolPreview = { x, y };
352367
return;
@@ -409,6 +424,11 @@ function continueStroke(e) {
409424
x: x,
410425
y: y
411426
};
427+
if (tool === "rect") {
428+
rectToolPreview = { x, y };
429+
queueRenderAll();
430+
return;
431+
}
412432
if (tool === "rect-select") {
413433
updateRectSelect(e);
414434
lastPt = {
@@ -481,11 +501,34 @@ function continueStroke(e) {
481501
function endStroke() {
482502
if (!isDrawing) return;
483503
isDrawing = false;
484-
commitGlobalHistoryStep();
485504
const endKey = strokeHex;
486-
strokeHex = null;
487-
queueRenderAll();
488-
updateTimelineHasContent(currentFrame);
505+
const finishingRect = tool === "rect" && rectToolStart && rectToolPreview;
506+
const finishingLine = tool === "line" && lineToolStart && lineToolPreview;
507+
if (!finishingRect && !finishingLine) {
508+
commitGlobalHistoryStep();
509+
}
510+
if (tool === "rect" && rectToolStart && rectToolPreview) {
511+
const hex = strokeHex || activeSubColor?.[activeLayer] || colorToHex(currentColor);
512+
const off = getFrameCanvas(activeLayer, currentFrame, hex);
513+
const ctx = off.getContext("2d");
514+
ctx.strokeStyle = hex;
515+
ctx.lineWidth = Math.max(1, brushSize);
516+
ctx.lineCap = "round";
517+
ctx.beginPath();
518+
ctx.rect(rectToolStart.x, rectToolStart.y, rectToolPreview.x - rectToolStart.x, rectToolPreview.y - rectToolStart.y);
519+
ctx.stroke();
520+
markFrameHasContent(activeLayer, currentFrame, hex);
521+
markGlobalHistoryDirty();
522+
commitGlobalHistoryStep();
523+
rectToolStart = null;
524+
rectToolPreview = null;
525+
strokeHex = null;
526+
queueRenderAll();
527+
updateTimelineHasContent(currentFrame);
528+
lastPt = null;
529+
stabilizedPt = null;
530+
return;
531+
}
489532
if (tool === "rect-select") {
490533
endRectSelect();
491534
lastPt = null;
@@ -505,12 +548,18 @@ function endStroke() {
505548
ctx.lineTo(lineToolPreview.x, lineToolPreview.y);
506549
ctx.stroke();
507550
markFrameHasContent(activeLayer, currentFrame, hex);
551+
markGlobalHistoryDirty();
552+
commitGlobalHistoryStep();
508553
lineToolStart = null;
509554
lineToolPreview = null;
555+
strokeHex = null;
510556
queueRenderAll();
511557
updateTimelineHasContent(currentFrame);
512558
return;
513559
}
560+
strokeHex = null;
561+
queueRenderAll();
562+
updateTimelineHasContent(currentFrame);
514563
if (tool === "lasso-erase" && lassoActive) {
515564
lassoActive = false;
516565
applyLassoErase();
@@ -1538,3 +1587,15 @@ function fillFromLineart(F) {
15381587
updateTimelineHasContent(F);
15391588
return true;
15401589
}
1590+
1591+
function drawRectToolPreview(ctx) {
1592+
if (!rectToolStart || !rectToolPreview) return;
1593+
ctx.save();
1594+
ctx.strokeStyle = colorToHex(currentColor);
1595+
ctx.lineWidth = Math.max(1, brushSize);
1596+
ctx.globalAlpha = 0.5;
1597+
ctx.beginPath();
1598+
ctx.rect(rectToolStart.x, rectToolStart.y, rectToolPreview.x - rectToolStart.x, rectToolPreview.y - rectToolStart.y);
1599+
ctx.stroke();
1600+
ctx.restore();
1601+
}

celstomp/js/tools/brush-helper.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ function getBrushSizeForPreview(toolKind) {
315315
function updateBrushPreview() {
316316
if (!_brushPrevEl || !_brushPrevCanvas) return;
317317
const toolKind = getActiveToolKindForPreview();
318-
const showForTools = toolKind === "brush" || toolKind === "eraser" || toolKind === "line";
318+
const showForTools = toolKind === "brush" || toolKind === "eraser" || toolKind === "line" || toolKind === "rect";
319319
const isEraser = toolKind === "eraser";
320320
if (!showForTools) {
321321
_brushPrevEl.style.display = "none";
@@ -514,4 +514,4 @@ function openBrushCtxMenu(ev, anchorEl) {
514514
function closeBrushCtxMenu() {
515515
if (_brushCtxMenu) _brushCtxMenu.hidden = true;
516516
_brushCtxState = null;
517-
}
517+
}

celstomp/js/ui/interaction-shortcuts.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -642,12 +642,13 @@ function wireKeyboardShortcuts() {
642642
1: "brush",
643643
2: "eraser",
644644
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"
645+
4: "rect",
646+
5: "fill-brush",
647+
6: "fill-eraser",
648+
7: "lasso-fill",
649+
8: "lasso-erase",
650+
9: "rect-select",
651+
0: "eyedropper"
651652
};
652653
document.addEventListener("keydown", e => {
653654
if (e.defaultPrevented) return;
@@ -739,42 +740,49 @@ function onWindowKeyDown(e) {
739740
});
740741
}
741742
if (isDigit(4)) {
743+
e.preventDefault();
744+
pickTool({
745+
id: "tool-rect",
746+
value: "rect"
747+
});
748+
}
749+
if (isDigit(5)) {
742750
e.preventDefault();
743751
pickTool({
744752
id: "tool-fillbrush",
745753
value: "fill-brush"
746754
});
747755
}
748-
if (isDigit(5)) {
756+
if (isDigit(6)) {
749757
e.preventDefault();
750758
pickTool({
751759
id: "tool-filleraser",
752760
value: "fill-eraser"
753761
});
754762
}
755-
if (isDigit(6)) {
763+
if (isDigit(7)) {
756764
e.preventDefault();
757765
pickTool({
758766
id: "tool-lassoFill",
759767
value: "lasso-fill"
760768
});
761769
}
762-
if (isDigit(7)) {
770+
if (isDigit(8)) {
763771
e.preventDefault();
764772
pickTool({
765773
id: "tool-lassoErase",
766774
altIds: [ "tool-lassoerase", "tool-lasso-erase" ],
767775
value: "lasso-erase"
768776
});
769777
}
770-
if (isDigit(8)) {
778+
if (isDigit(9)) {
771779
e.preventDefault();
772780
pickTool({
773781
id: "tool-rectSelect",
774782
value: "rect-select"
775783
});
776784
}
777-
if (isDigit(9)) {
785+
if (isDigit(0)) {
778786
e.preventDefault();
779787
pickTool({
780788
id: "tool-eyedropper",

celstomp/js/ui/ui-components.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{ id: 'tool-brush', val: 'brush', label: 'Brush', checked: true },
66
{ id: 'tool-eraser', val: 'eraser', label: 'Eraser' },
77
{ 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>' },
8+
{ id: 'tool-rect', val: 'rect', label: 'Rect', icon: '<svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="5" width="18" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/></svg>' },
89
{ id: 'tool-fillbrush', val: 'fill-brush', label: 'Fill Brush' },
910
{ id: 'tool-filleraser', val: 'fill-eraser', label: 'Eraser Fill' },
1011
{ id: 'tool-lassoFill', val: 'lasso-fill', label: 'Lasso Fill' },

celstomp/parts/modals.js

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

0 commit comments

Comments
 (0)