diff --git a/celstomp/css/components/overlays.css b/celstomp/css/components/overlays.css index ed86422..5986b80 100644 --- a/celstomp/css/components/overlays.css +++ b/celstomp/css/components/overlays.css @@ -127,6 +127,124 @@ line-height: 1.35; } +.canvasTextEntry { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.32); + z-index: 9400; +} + +.canvasTextEntry.open { + display: flex; +} + +.canvasTextEntryCard { + width: min(420px, calc(100vw - 24px)); + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(16, 20, 28, 0.96); + border-radius: 10px; + box-shadow: 0 16px 42px rgba(0, 0, 0, 0.45); + padding: 12px; + display: grid; + gap: 10px; +} + +.canvasTextEntryLabel { + font-size: 12px; + color: #c7d0db; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.canvasTextEntryInput { + width: 100%; + min-height: 36px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(0, 0, 0, 0.28); + color: #e2e8f0; + border-radius: 8px; + padding: 8px 10px; +} + +.canvasTextEntryInput:focus { + outline: none; + border-color: rgba(92, 179, 255, 0.8); + box-shadow: 0 0 0 2px rgba(92, 179, 255, 0.2); +} + +.canvasTextEntryOptions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.canvasTextEntryOpt { + display: grid; + gap: 4px; +} + +.canvasTextEntryOpt > span { + font-size: 11px; + color: #b7c3d2; + letter-spacing: 0.03em; +} + +.canvasTextEntrySelect, +.canvasTextEntryNum { + width: 100%; + min-height: 32px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(0, 0, 0, 0.28); + color: #e2e8f0; + border-radius: 8px; + padding: 6px 8px; +} + +.canvasTextEntrySelect:focus, +.canvasTextEntryNum:focus { + outline: none; + border-color: rgba(92, 179, 255, 0.8); + box-shadow: 0 0 0 2px rgba(92, 179, 255, 0.2); +} + +.canvasTextEntryOptCheck { + align-items: center; + grid-template-columns: auto 1fr; + gap: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + background: rgba(255, 255, 255, 0.04); + padding: 6px 8px; +} + +.canvasTextEntryOptCheck input { + margin: 0; +} + +.canvasTextEntryActions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.canvasTextEntryBtn { + min-height: 34px; + padding: 0 12px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.06); + color: #d0d8e2; +} + +.canvasTextEntryBtnPrimary { + border-color: rgba(92, 179, 255, 0.6); + background: rgba(92, 179, 255, 0.18); + color: #9fd5ff; +} + /* Modal Card */ .modalBackdrop{ position: fixed; diff --git a/celstomp/css/components/tools.css b/celstomp/css/components/tools.css index f8c57f3..11dd82b 100644 --- a/celstomp/css/components/tools.css +++ b/celstomp/css/components/tools.css @@ -126,6 +126,22 @@ cursor: pointer; } +#islandToolsSlot #toolSeg > label svg { + width: 20px; + height: 20px; + color: rgba(255,255,255,0.85); + position: relative; + z-index: 1; +} + +#islandToolsSlot #toolSeg > label:has(svg) { + background-image: none; +} + +#islandToolsSlot #toolSeg > label:has(svg)::before { + display: none; +} + #islandToolsSlot #toolSeg > label::before{ content: ""; width: 20px; diff --git a/celstomp/js/input/pointer-events.js b/celstomp/js/input/pointer-events.js index b0e5534..a68f399 100644 --- a/celstomp/js/input/pointer-events.js +++ b/celstomp/js/input/pointer-events.js @@ -11,6 +11,10 @@ let usePressureTilt = false; let brushSize = 3; let autofill = false; +let textEntryActive = false; +let textEntryX = 0; +let textEntryY = 0; + let trailPoints = []; function pressure(e) { @@ -266,6 +270,10 @@ function startStroke(e) { pickCanvasColorAtEvent(e); return; } + if (tool === "text") { + openTextEntryAt(x, y); + return; + } if (tool === "rect-select") { isDrawing = true; beginRectSelect(e); @@ -1487,3 +1495,97 @@ function fillFromLineart(F) { updateTimelineHasContent(F); return true; } + +function openTextEntryAt(cx, cy) { + const dialog = document.getElementById("canvasTextEntry"); + const input = document.getElementById("canvasTextEntryInput"); + if (!dialog) return; + textEntryX = Math.round(cx); + textEntryY = Math.round(cy); + textEntryActive = true; + input.value = ""; + dialog.hidden = false; + dialog.classList.add("open"); + setTimeout(() => input.focus(), 50); +} + +function closeTextEntry() { + const dialog = document.getElementById("canvasTextEntry"); + if (!dialog) return; + textEntryActive = false; + dialog.hidden = true; + dialog.classList.remove("open"); +} + +function applyTextEntry() { + const input = document.getElementById("canvasTextEntryInput"); + const sizeInput = document.getElementById("canvasTextEntrySize"); + const fontSelect = document.getElementById("canvasTextEntryFont"); + const boldCheck = document.getElementById("canvasTextEntryBold"); + const italicCheck = document.getElementById("canvasTextEntryItalic"); + if (!input) return; + const text = input.value; + if (!text) { + closeTextEntry(); + return; + } + const fontSize = Math.max(8, Math.min(200, parseInt(sizeInput?.value || "32", 10) || 32)); + const fontFamily = fontSelect?.value || "Arial"; + const bold = boldCheck?.checked ? "bold " : ""; + const italic = italicCheck?.checked ? "italic " : ""; + const fontStyle = `${italic}${bold}${fontSize}px ${fontFamily}`; + if (activeLayer === PAPER_LAYER) { + closeTextEntry(); + return; + } + const hex = colorToHex(currentColor); + const key = activeLayer === LAYER.FILL ? fillWhite : hex; + beginGlobalHistoryStep(activeLayer, currentFrame, key); + pushUndo(activeLayer, currentFrame, key); + const canvas = getFrameCanvas(activeLayer, currentFrame, key); + if (!canvas) { + closeTextEntry(); + return; + } + const ctx = canvas.getContext("2d"); + ctx.font = fontStyle; + ctx.fillStyle = hex; + ctx.textBaseline = "top"; + const lines = text.split("\n"); + let lineY = textEntryY; + for (const line of lines) { + ctx.fillText(line, textEntryX, lineY); + lineY += fontSize * 1.2; + } + canvas._hasContent = true; + markGlobalHistoryDirty(); + commitGlobalHistoryStep(); + queueRenderAll(); + updateTimelineHasContent(currentFrame); + closeTextEntry(); +} + +function initTextEntry() { + const dialog = document.getElementById("canvasTextEntry"); + const cancelBtn = document.getElementById("canvasTextEntryCancel"); + const applyBtn = document.getElementById("canvasTextEntryApply"); + const input = document.getElementById("canvasTextEntryInput"); + if (!dialog) return; + cancelBtn?.addEventListener("click", closeTextEntry); + applyBtn?.addEventListener("click", applyTextEntry); + input?.addEventListener("keydown", e => { + if (e.key === "Escape") { + e.preventDefault(); + closeTextEntry(); + } + if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + applyTextEntry(); + } + }); + dialog.addEventListener("click", e => { + if (e.target === dialog) closeTextEntry(); + }); +} + +document.addEventListener("DOMContentLoaded", initTextEntry); diff --git a/celstomp/js/ui/interaction-shortcuts.js b/celstomp/js/ui/interaction-shortcuts.js index 5359ed6..347c191 100644 --- a/celstomp/js/ui/interaction-shortcuts.js +++ b/celstomp/js/ui/interaction-shortcuts.js @@ -605,7 +605,9 @@ function wireKeyboardShortcuts() { 5: "lasso-fill", 6: "lasso-erase", 7: "rect-select", - 8: "eyedropper" + 8: "eyedropper", + 9: "text", + t: "text" }; document.addEventListener("keydown", e => { if (e.defaultPrevented) return; diff --git a/celstomp/js/ui/ui-components.js b/celstomp/js/ui/ui-components.js index 3614e29..eb9ca8f 100644 --- a/celstomp/js/ui/ui-components.js +++ b/celstomp/js/ui/ui-components.js @@ -4,6 +4,7 @@ const tools = [ { id: 'tool-brush', val: 'brush', label: 'Brush', checked: true }, { id: 'tool-eraser', val: 'eraser', label: 'Eraser' }, + { id: 'tool-text', val: 'text', label: 'Text', 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' }, @@ -27,7 +28,12 @@ const lbl = document.createElement('label'); lbl.htmlFor = t.id; lbl.dataset.tool = t.val; - lbl.textContent = t.label; + lbl.setAttribute('aria-label', t.label); + if (t.icon) { + lbl.innerHTML = t.icon; + } else { + lbl.textContent = t.label; + } if (t.val === 'brush') lbl.id = 'toolBrushLabel'; if (t.val === 'eraser') lbl.id = 'toolEraserLabel'; diff --git a/celstomp/parts/modals.js b/celstomp/parts/modals.js index 5351362..7385434 100644 --- a/celstomp/parts/modals.js +++ b/celstomp/parts/modals.js @@ -58,6 +58,8 @@ document.getElementById('part-modals').innerHTML = `