Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions celstomp/css/components/overlays.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions celstomp/css/components/tools.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
102 changes: 102 additions & 0 deletions celstomp/js/input/pointer-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
4 changes: 3 additions & 1 deletion celstomp/js/ui/interaction-shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion celstomp/js/ui/ui-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<svg viewBox="0 0 24 24" width="18" height="18"><path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>' },
{ 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' },
Expand All @@ -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';
Expand Down
37 changes: 37 additions & 0 deletions celstomp/parts/modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ document.getElementById('part-modals').innerHTML = `
<div class="shortcutRow"><kbd>6</kbd><span>Lasso Erase</span></div>
<div class="shortcutRow"><kbd>7</kbd><span>Rect Select</span></div>
<div class="shortcutRow"><kbd>8</kbd><span>Eyedropper</span></div>
<div class="shortcutRow"><kbd>9</kbd><span>Text</span></div>
<div class="shortcutRow"><kbd>T</kbd><span>Text</span></div>
</div>
<div class="shortcutSection">
<h4>Navigation</h4>
Expand Down Expand Up @@ -225,4 +227,39 @@ document.getElementById('part-modals').innerHTML = `
<button id="autosaveIntervalConfirmBtn" type="button">Apply</button>
</div>
</div>

<div id="canvasTextEntry" class="canvasTextEntry" role="dialog" aria-modal="true" aria-labelledby="canvasTextEntryLabel" hidden>
<div class="canvasTextEntryCard">
<div id="canvasTextEntryLabel" class="canvasTextEntryLabel">Add Text</div>
<textarea id="canvasTextEntryInput" class="canvasTextEntryInput" rows="3" placeholder="Enter text..."></textarea>
<div class="canvasTextEntryOptions">
<div class="canvasTextEntryOpt">
<span>Font Size</span>
<input id="canvasTextEntrySize" class="canvasTextEntryNum" type="number" min="8" max="200" value="32" />
</div>
<div class="canvasTextEntryOpt">
<span>Font</span>
<select id="canvasTextEntryFont" class="canvasTextEntrySelect">
<option value="Arial">Arial</option>
<option value="Georgia">Georgia</option>
<option value="Courier New">Courier New</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<label class="canvasTextEntryOpt canvasTextEntryOptCheck">
<input id="canvasTextEntryBold" type="checkbox" />
<span>Bold</span>
</label>
<label class="canvasTextEntryOpt canvasTextEntryOptCheck">
<input id="canvasTextEntryItalic" type="checkbox" />
<span>Italic</span>
</label>
</div>
<div class="canvasTextEntryActions">
<button id="canvasTextEntryCancel" class="canvasTextEntryBtn" type="button">Cancel</button>
<button id="canvasTextEntryApply" class="canvasTextEntryBtn canvasTextEntryBtnPrimary" type="button">Add Text</button>
</div>
</div>
</div>
`;