diff --git a/celstomp/celstomp-app.js b/celstomp/celstomp-app.js index a30930d..61175e2 100644 --- a/celstomp/celstomp-app.js +++ b/celstomp/celstomp-app.js @@ -149,6 +149,9 @@ const eraserSizeInput = $("eraserSize"); const toolOpacityRange = $("toolOpacityRange"); const toolAngleRange = $("toolAngleRange"); + const toolOpacityRow = toolOpacityRange?.closest(".sideRangeRow") || null; + const toolAngleRow = toolAngleRange?.closest(".sideRangeRow") || null; + const brushFoldSection = toolFoldBrushesBtn?.closest(".toolFold") || null; const eraserVal = $("eraserVal"); @@ -183,16 +186,25 @@ function refreshToolSettingsUI() { const isBrush = tool === "brush"; const isEraser = tool === "eraser"; - if (toolSettingsSection) toolSettingsSection.hidden = !(isBrush || isEraser); - if (!isBrush && !isEraser) return; + const isLine = tool === "line"; + const isRect = tool === "rect"; + const isShapeTool = isLine || isRect; + const showsBrushSettings = isBrush || isShapeTool; + if (toolSettingsSection) toolSettingsSection.hidden = !(showsBrushSettings || isEraser); + if (brushFoldSection) brushFoldSection.hidden = isShapeTool; + if (toolOpacityRow) toolOpacityRow.hidden = isShapeTool; + if (toolAngleRow) toolAngleRow.hidden = isShapeTool; + if (!showsBrushSettings && !isEraser) return; const s = isEraser ? eraserSettings : brushSettings; - if (toolSettingsTitle) toolSettingsTitle.textContent = isEraser ? "Eraser" : "Brushes"; + if (toolSettingsTitle) toolSettingsTitle.textContent = isEraser ? "Eraser" : isShapeTool ? "Shape Tool" : "Brushes"; safeSetValue(brushSizeInput, s.size); safeSetValue(brushSizeNumInput, s.size); safeSetValue(toolOpacityRange, Math.round(s.opacity * 100)); safeSetValue(toolAngleRange, s.angle); - const activeShape = document.querySelector('input[name="brushShape"][value="' + s.shape + '"]'); - if (activeShape) activeShape.checked = true; + if (!isShapeTool) { + const activeShape = document.querySelector('input[name="brushShape"][value="' + s.shape + '"]'); + if (activeShape) activeShape.checked = true; + } } function setFoldExpanded(btn, body, open) { if (!btn || !body) return; @@ -359,6 +371,7 @@ bctx.strokeRect(0, 0, contentW, contentH); drawRectSelectionOverlay(fxctx); drawLineToolPreview(fxctx); + drawRectToolPreview(fxctx); } function onionCompositeOperation() { @@ -408,6 +421,7 @@ setTransform(fxctx); drawRectSelectionOverlay(fxctx); drawLineToolPreview(fxctx); + drawRectToolPreview(fxctx); } function wireBrushButtonRightClick() { diff --git a/celstomp/js/input/pointer-events.js b/celstomp/js/input/pointer-events.js index cd58e81..80f685f 100644 --- a/celstomp/js/input/pointer-events.js +++ b/celstomp/js/input/pointer-events.js @@ -12,6 +12,8 @@ let brushSize = 3; let autofill = false; let trailPoints = []; +let rectToolStart = null; +let rectToolPreview = null; let lineToolStart = null; let lineToolPreview = null; @@ -268,6 +270,18 @@ function startStroke(e) { pickCanvasColorAtEvent(e); return; } + if (tool === "rect") { + isDrawing = true; + const hex = colorToHex(currentColor); + strokeHex = activeLayer === LAYER.FILL ? fillWhite : hex; + activeSubColor[activeLayer] = strokeHex; + ensureSublayer(activeLayer, strokeHex); + renderLayerSwatches(activeLayer); + beginGlobalHistoryStep(activeLayer, currentFrame, strokeHex); + rectToolStart = { x, y }; + rectToolPreview = { x, y }; + return; + } if (tool === "rect-select") { isDrawing = true; beginRectSelect(e); @@ -347,6 +361,7 @@ function startStroke(e) { activeSubColor[activeLayer] = strokeHex; ensureSublayer(activeLayer, strokeHex); renderLayerSwatches(activeLayer); + beginGlobalHistoryStep(activeLayer, currentFrame, strokeHex); lineToolStart = { x, y }; lineToolPreview = { x, y }; return; @@ -409,6 +424,11 @@ function continueStroke(e) { x: x, y: y }; + if (tool === "rect") { + rectToolPreview = { x, y }; + queueRenderAll(); + return; + } if (tool === "rect-select") { updateRectSelect(e); lastPt = { @@ -481,11 +501,34 @@ function continueStroke(e) { function endStroke() { if (!isDrawing) return; isDrawing = false; - commitGlobalHistoryStep(); const endKey = strokeHex; - strokeHex = null; - queueRenderAll(); - updateTimelineHasContent(currentFrame); + const finishingRect = tool === "rect" && rectToolStart && rectToolPreview; + const finishingLine = tool === "line" && lineToolStart && lineToolPreview; + if (!finishingRect && !finishingLine) { + commitGlobalHistoryStep(); + } + if (tool === "rect" && rectToolStart && rectToolPreview) { + const hex = strokeHex || activeSubColor?.[activeLayer] || colorToHex(currentColor); + const off = getFrameCanvas(activeLayer, currentFrame, hex); + const ctx = off.getContext("2d"); + ctx.strokeStyle = hex; + ctx.lineWidth = Math.max(1, brushSize); + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.rect(rectToolStart.x, rectToolStart.y, rectToolPreview.x - rectToolStart.x, rectToolPreview.y - rectToolStart.y); + ctx.stroke(); + markFrameHasContent(activeLayer, currentFrame, hex); + markGlobalHistoryDirty(); + commitGlobalHistoryStep(); + rectToolStart = null; + rectToolPreview = null; + strokeHex = null; + queueRenderAll(); + updateTimelineHasContent(currentFrame); + lastPt = null; + stabilizedPt = null; + return; + } if (tool === "rect-select") { endRectSelect(); lastPt = null; @@ -505,12 +548,18 @@ function endStroke() { ctx.lineTo(lineToolPreview.x, lineToolPreview.y); ctx.stroke(); markFrameHasContent(activeLayer, currentFrame, hex); + markGlobalHistoryDirty(); + commitGlobalHistoryStep(); lineToolStart = null; lineToolPreview = null; + strokeHex = null; queueRenderAll(); updateTimelineHasContent(currentFrame); return; } + strokeHex = null; + queueRenderAll(); + updateTimelineHasContent(currentFrame); if (tool === "lasso-erase" && lassoActive) { lassoActive = false; applyLassoErase(); @@ -1538,3 +1587,15 @@ function fillFromLineart(F) { updateTimelineHasContent(F); return true; } + +function drawRectToolPreview(ctx) { + if (!rectToolStart || !rectToolPreview) return; + ctx.save(); + ctx.strokeStyle = colorToHex(currentColor); + ctx.lineWidth = Math.max(1, brushSize); + ctx.globalAlpha = 0.5; + ctx.beginPath(); + ctx.rect(rectToolStart.x, rectToolStart.y, rectToolPreview.x - rectToolStart.x, rectToolPreview.y - rectToolStart.y); + ctx.stroke(); + ctx.restore(); +} diff --git a/celstomp/js/tools/brush-helper.js b/celstomp/js/tools/brush-helper.js index 7682367..c3ddddd 100644 --- a/celstomp/js/tools/brush-helper.js +++ b/celstomp/js/tools/brush-helper.js @@ -315,7 +315,7 @@ function getBrushSizeForPreview(toolKind) { function updateBrushPreview() { if (!_brushPrevEl || !_brushPrevCanvas) return; const toolKind = getActiveToolKindForPreview(); - const showForTools = toolKind === "brush" || toolKind === "eraser" || toolKind === "line"; + const showForTools = toolKind === "brush" || toolKind === "eraser" || toolKind === "line" || toolKind === "rect"; const isEraser = toolKind === "eraser"; if (!showForTools) { _brushPrevEl.style.display = "none"; @@ -514,4 +514,4 @@ function openBrushCtxMenu(ev, anchorEl) { function closeBrushCtxMenu() { if (_brushCtxMenu) _brushCtxMenu.hidden = true; _brushCtxState = null; -} \ No newline at end of file +} diff --git a/celstomp/js/ui/interaction-shortcuts.js b/celstomp/js/ui/interaction-shortcuts.js index 6473446..223d3ac 100644 --- a/celstomp/js/ui/interaction-shortcuts.js +++ b/celstomp/js/ui/interaction-shortcuts.js @@ -642,12 +642,13 @@ function wireKeyboardShortcuts() { 1: "brush", 2: "eraser", 3: "line", - 4: "fill-brush", - 5: "fill-eraser", - 6: "lasso-fill", - 7: "lasso-erase", - 8: "rect-select", - 9: "eyedropper" + 4: "rect", + 5: "fill-brush", + 6: "fill-eraser", + 7: "lasso-fill", + 8: "lasso-erase", + 9: "rect-select", + 0: "eyedropper" }; document.addEventListener("keydown", e => { if (e.defaultPrevented) return; @@ -739,27 +740,34 @@ function onWindowKeyDown(e) { }); } if (isDigit(4)) { + e.preventDefault(); + pickTool({ + id: "tool-rect", + value: "rect" + }); + } + if (isDigit(5)) { e.preventDefault(); pickTool({ id: "tool-fillbrush", value: "fill-brush" }); } - if (isDigit(5)) { + if (isDigit(6)) { e.preventDefault(); pickTool({ id: "tool-filleraser", value: "fill-eraser" }); } - if (isDigit(6)) { + if (isDigit(7)) { e.preventDefault(); pickTool({ id: "tool-lassoFill", value: "lasso-fill" }); } - if (isDigit(7)) { + if (isDigit(8)) { e.preventDefault(); pickTool({ id: "tool-lassoErase", @@ -767,14 +775,14 @@ function onWindowKeyDown(e) { value: "lasso-erase" }); } - if (isDigit(8)) { + if (isDigit(9)) { e.preventDefault(); pickTool({ id: "tool-rectSelect", value: "rect-select" }); } - if (isDigit(9)) { + if (isDigit(0)) { e.preventDefault(); pickTool({ id: "tool-eyedropper", diff --git a/celstomp/js/ui/ui-components.js b/celstomp/js/ui/ui-components.js index fc949ee..86106d4 100644 --- a/celstomp/js/ui/ui-components.js +++ b/celstomp/js/ui/ui-components.js @@ -5,6 +5,7 @@ { id: 'tool-brush', val: 'brush', label: 'Brush', checked: true }, { id: 'tool-eraser', val: 'eraser', label: 'Eraser' }, { id: 'tool-line', val: 'line', label: 'Line', icon: '' }, + { id: 'tool-rect', val: 'rect', label: 'Rect', icon: '' }, { id: 'tool-fillbrush', val: 'fill-brush', label: 'Fill Brush' }, { id: 'tool-filleraser', val: 'fill-eraser', label: 'Eraser Fill' }, { id: 'tool-lassoFill', val: 'lasso-fill', label: 'Lasso Fill' }, diff --git a/celstomp/parts/modals.js b/celstomp/parts/modals.js index 74870de..fa98cc2 100644 --- a/celstomp/parts/modals.js +++ b/celstomp/parts/modals.js @@ -53,12 +53,13 @@ document.getElementById('part-modals').innerHTML = `