diff --git a/celstomp/celstomp-app.js b/celstomp/celstomp-app.js index 3d924b6..223da25 100644 --- a/celstomp/celstomp-app.js +++ b/celstomp/celstomp-app.js @@ -404,6 +404,9 @@ function clearFx() { fxctx.setTransform(1, 0, 0, 1, 0, 0); fxctx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); + setTransform(fxctx); + drawGrid(fxctx); + drawGuides(fxctx); } function wireBrushButtonRightClick() { @@ -924,6 +927,48 @@ playSnapped = !!e.target.checked; safeSetChecked(playSnappedChk, playSnapped); }); + + const gridBtn = $("toggleGridBtn"); + const gridSizeInp = $("gridSizeInput"); + const gridSnapBtn = $("toggleGridSnapBtn"); + const guidesBtn = $("toggleGuidesBtn"); + const guideSnapBtn = $("toggleGuideSnapBtn"); + const addGuideHBtn = $("addGuideHBtn"); + const addGuideVBtn = $("addGuideVBtn"); + const clearGuidesBtn = $("clearGuidesBtn"); + guideModeHint = $("guideModeHint"); + + gridBtn?.addEventListener("click", () => { + toggleGrid(); + gridBtn.classList.toggle("active", gridEnabled); + }); + gridSizeInp?.addEventListener("input", e => { + gridSize = Math.max(8, Math.min(128, parseInt(e.target.value, 10) || 32)); + queueRenderAll(); + }); + gridSnapBtn?.addEventListener("click", () => { + toggleGridSnap(); + gridSnapBtn.classList.toggle("active", gridSnap); + }); + guidesBtn?.addEventListener("click", () => { + toggleGuides(); + guidesBtn.classList.toggle("active", guidesEnabled); + }); + guideSnapBtn?.addEventListener("click", () => { + toggleGuideSnap(); + guideSnapBtn.classList.toggle("active", guideSnap); + }); + addGuideHBtn?.addEventListener("click", () => { + setGuideMode(guideMode === 'h' ? null : 'h'); + addGuideHBtn.classList.toggle("active", guideMode === 'h'); + addGuideVBtn?.classList.remove("active"); + }); + addGuideVBtn?.addEventListener("click", () => { + setGuideMode(guideMode === 'v' ? null : 'v'); + addGuideVBtn.classList.toggle("active", guideMode === 'v'); + addGuideHBtn?.classList.remove("active"); + }); + clearGuidesBtn?.addEventListener("click", clearGuides); } function wirePanelToggles() { diff --git a/celstomp/css/components/timeline.css b/celstomp/css/components/timeline.css index 9256498..cfec99a 100644 --- a/celstomp/css/components/timeline.css +++ b/celstomp/css/components/timeline.css @@ -401,3 +401,29 @@ body.dragging-cel{ cursor: grabbing; user-select:none; } padding: 6px 8px; } } + +.gridGuideCtrls { + display: flex; + align-items: center; + gap: 4px; +} + +.gridGuideCtrls button.active { + background: rgba(0, 229, 255, 0.25); + border-color: rgba(0, 229, 255, 0.6); +} + +.guideModeHint { + position: fixed; + bottom: calc(var(--timeline-h) + 8px); + left: 50%; + transform: translateX(-50%); + background: rgba(0, 229, 255, 0.18); + border: 1px solid rgba(0, 229, 255, 0.5); + color: #9fd5ff; + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; + z-index: 100; + pointer-events: none; +} diff --git a/celstomp/js/input/pointer-events.js b/celstomp/js/input/pointer-events.js index b0e5534..ef8b9ec 100644 --- a/celstomp/js/input/pointer-events.js +++ b/celstomp/js/input/pointer-events.js @@ -11,6 +11,16 @@ let usePressureTilt = false; let brushSize = 3; let autofill = false; +let gridEnabled = false; +let gridSize = 32; +let gridSnap = false; +let guidesEnabled = false; +let guideSnap = false; +let guideHLines = []; +let guideVLines = []; +let guideMode = null; +let guideModeHint = null; + let trailPoints = []; function pressure(e) { @@ -47,6 +57,14 @@ function handlePointerDown(e) { return; } } + if (guideMode && e.button === 0) { + const pos = getCanvasPointer(e); + const pt = screenToContent(pos.x, pos.y); + if (handleGuideClick(pt.x, pt.y)) { + e.preventDefault(); + return; + } + } try { drawCanvas.setPointerCapture(e.pointerId); } catch {} @@ -1487,3 +1505,104 @@ function fillFromLineart(F) { updateTimelineHasContent(F); return true; } + +function drawGrid(ctx) { + if (!gridEnabled) return; + ctx.save(); + ctx.strokeStyle = "rgba(255,255,255,0.12)"; + ctx.lineWidth = 1 / Math.max(getZoom(), 1); + ctx.beginPath(); + for (let x = 0; x <= contentW; x += gridSize) { + ctx.moveTo(x, 0); + ctx.lineTo(x, contentH); + } + for (let y = 0; y <= contentH; y += gridSize) { + ctx.moveTo(0, y); + ctx.lineTo(contentW, y); + } + ctx.stroke(); + ctx.restore(); +} + +function drawGuides(ctx) { + if (!guidesEnabled) return; + ctx.save(); + ctx.strokeStyle = "rgba(0,229,255,0.6)"; + ctx.lineWidth = 1 / Math.max(getZoom(), 1); + ctx.setLineDash([4 / Math.max(getZoom(), 1), 2 / Math.max(getZoom(), 1)]); + ctx.beginPath(); + for (const y of guideHLines) { + ctx.moveTo(0, y); + ctx.lineTo(contentW, y); + } + for (const x of guideVLines) { + ctx.moveTo(x, 0); + ctx.lineTo(x, contentH); + } + ctx.stroke(); + ctx.restore(); +} + +function snapToGrid(x, y) { + if (!gridEnabled || !gridSnap) return { x, y }; + return { + x: Math.round(x / gridSize) * gridSize, + y: Math.round(y / gridSize) * gridSize + }; +} + +function snapToGuides(x, y) { + if (!guidesEnabled || !guideSnap) return { x, y }; + const threshold = 8; + let sx = x, sy = y; + for (const gy of guideHLines) { + if (Math.abs(y - gy) < threshold) sy = gy; + } + for (const gx of guideVLines) { + if (Math.abs(x - gx) < threshold) sx = gx; + } + return { x: sx, y: sy }; +} + +function toggleGrid() { + gridEnabled = !gridEnabled; + queueRenderAll(); +} + +function toggleGridSnap() { + gridSnap = !gridSnap; +} + +function toggleGuides() { + guidesEnabled = !guidesEnabled; + queueRenderAll(); +} + +function toggleGuideSnap() { + guideSnap = !guideSnap; +} + +function clearGuides() { + guideHLines = []; + guideVLines = []; + queueRenderAll(); +} + +function setGuideMode(mode) { + guideMode = mode; + if (guideModeHint) { + guideModeHint.textContent = mode ? `Click to place ${mode === 'h' ? 'horizontal' : 'vertical'} guide` : ''; + guideModeHint.hidden = !mode; + } +} + +function handleGuideClick(x, y) { + if (!guideMode) return false; + if (guideMode === 'h') { + guideHLines.push(Math.round(y)); + } else if (guideMode === 'v') { + guideVLines.push(Math.round(x)); + } + queueRenderAll(); + return true; +} diff --git a/celstomp/js/ui/interaction-shortcuts.js b/celstomp/js/ui/interaction-shortcuts.js index 5359ed6..59ecc38 100644 --- a/celstomp/js/ui/interaction-shortcuts.js +++ b/celstomp/js/ui/interaction-shortcuts.js @@ -491,6 +491,14 @@ function _wireExtraKeyboardShortcuts() { return; } + if (k === "g") { + e.preventDefault(); + toggleGrid(); + const gridBtn = document.getElementById("toggleGridBtn"); + if (gridBtn) gridBtn.classList.toggle("active", gridEnabled); + return; + } + if (k === "[") { e.preventDefault(); brushSize = Math.max(1, brushSize - 5); diff --git a/celstomp/parts/timeline.js b/celstomp/parts/timeline.js index d98b5dc..dc30f65 100644 --- a/celstomp/parts/timeline.js +++ b/celstomp/parts/timeline.js @@ -48,6 +48,17 @@ document.getElementById('part-timeline').innerHTML = ` Zoom +
+ + + + + + + + +
+ @@ -60,6 +71,6 @@ document.getElementById('part-timeline').innerHTML = ` - + `;