diff --git a/celstomp/celstomp-app.js b/celstomp/celstomp-app.js index 074becb..e56123e 100644 --- a/celstomp/celstomp-app.js +++ b/celstomp/celstomp-app.js @@ -362,6 +362,8 @@ function renderBounds() { + fxctx.setTransform(1, 0, 0, 1, 0, 0); + fxctx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); setTransform(bctx); setTransform(fxctx); bctx.fillStyle = "#2a2f38"; @@ -902,7 +904,13 @@ safeSetChecked(onion, onionEnabled); safeSetChecked(psnap, playSnapped); toggle?.addEventListener("click", () => { - if (isPlaying) pausePlayback(); else startPlayback(); + if (isPlaying) { + pausePlayback(); + toggle.classList.remove("playing"); + } else { + startPlayback(); + toggle.classList.add("playing"); + } }); prevF?.addEventListener("click", () => gotoFrame(stepBySnap(-1))); nextF?.addEventListener("click", () => gotoFrame(stepBySnap(1))); @@ -919,6 +927,95 @@ safeSetValue(snapValue, v); updateHUD(); }); + const gridToggle = $("tlGridBtn"); + const gridSnapToggle = $("tlGridSnapBtn"); + const rulersToggle = $("tlRulersBtn"); + const guideSnapToggle = $("tlGuideSnapBtn"); + const addHGuideBtn = $("addHGuideBtn"); + const addVGuideBtn = $("addVGuideBtn"); + const clearGuidesBtn = $("clearGuidesBtn"); + const guideModeHint = $("guideModeHint"); + const gridSizeInput = $("tlGridSize"); + + let guidePlacementMode = null; + + if (gridToggle) { + gridToggle.addEventListener("click", e => { + gridEnabled = !gridEnabled; + gridToggle.classList.toggle("active", gridEnabled); + queueRenderAll(); + }); + } + if (gridSizeInput) { + gridSizeInput.addEventListener("change", e => { + const v = Math.max(8, Math.min(128, parseInt(e.target.value) || 32)); + gridSize = v; + e.target.value = v; + queueRenderAll(); + }); + } + if (gridSnapToggle) { + gridSnapToggle.addEventListener("click", e => { + gridSnap = !gridSnap; + gridSnapToggle.classList.toggle("active", gridSnap); + }); + } + if (rulersToggle) { + rulersToggle.addEventListener("click", e => { + rulersEnabled = !rulersEnabled; + rulersToggle.classList.toggle("active", rulersEnabled); + queueRenderAll(); + }); + } + if (guideSnapToggle) { + guideSnapToggle.addEventListener("click", e => { + guideSnap = !guideSnap; + guideSnapToggle.classList.toggle("active", guideSnap); + }); + } + function setGuidePlacementMode(mode) { + guidePlacementMode = mode; + if (addHGuideBtn) addHGuideBtn.classList.toggle("active", mode === "horizontal"); + if (addVGuideBtn) addVGuideBtn.classList.toggle("active", mode === "vertical"); + if (guideModeHint) { + guideModeHint.hidden = !mode; + guideModeHint.textContent = mode === "horizontal" ? "Click Canvas To Place H Guide" : mode === "vertical" ? "Click Canvas To Place V Guide" : ""; + } + if (!mode) { + document.body.classList.remove("guide-place-mode"); + document.body.classList.remove("guide-place-h"); + document.body.classList.remove("guide-place-v"); + } else { + document.body.classList.add("guide-place-mode"); + document.body.classList.toggle("guide-place-h", mode === "horizontal"); + document.body.classList.toggle("guide-place-v", mode === "vertical"); + } + } + if (addHGuideBtn) { + addHGuideBtn.addEventListener("click", e => { + if (guidePlacementMode === "horizontal") { + setGuidePlacementMode(null); + } else { + setGuidePlacementMode("horizontal"); + } + }); + } + if (addVGuideBtn) { + addVGuideBtn.addEventListener("click", e => { + if (guidePlacementMode === "vertical") { + setGuidePlacementMode(null); + } else { + setGuidePlacementMode("vertical"); + } + }); + } + if (clearGuidesBtn) { + clearGuidesBtn.addEventListener("click", () => { + guides = []; + queueRenderAll(); + }); + } + window.__celstompSetGuidePlacementMode = setGuidePlacementMode; function rebuildTimelineKeepFrame() { const cur = currentFrame; buildTimeline(); @@ -953,6 +1050,7 @@ const showLeft = $("showLeftEdge"); const showRight = $("showRightEdge"); const showTl = $("showTimelineEdge"); + const timelineEl = $("timeline"); const tLeft = $("toggleSidebarBtn"); const tRight = $("toggleRightbarBtn"); function applyLayoutChange() { @@ -974,8 +1072,28 @@ } function setTimelineOpen(open) { app.classList.toggle("tl-collapsed", !open); + document.body?.classList.toggle("tl-collapsed", !open); + if (timelineEl) { + timelineEl.hidden = !open; + timelineEl.style.display = open ? "" : "none"; + timelineEl.setAttribute("aria-hidden", open ? "false" : "true"); + } + if (showTl) { + showTl.style.display = open ? "none" : "block"; + } applyLayoutChange(); } + if (!document._celstompPanelToggleDelegated) { + document._celstompPanelToggleDelegated = true; + document.addEventListener("click", e => { + if (e.target.closest("#hideLeftPanelBtn")) setLeftOpen(false); + if (e.target.closest("#hideRightPanelBtn")) setRightOpen(false); + if (e.target.closest("#hideTimelineBtn")) setTimelineOpen(false); + if (e.target.closest("#showLeftEdge")) setLeftOpen(true); + if (e.target.closest("#showRightEdge")) setRightOpen(true); + if (e.target.closest("#showTimelineEdge")) setTimelineOpen(true); + }); + } setLeftOpen(true); setRightOpen(true); setTimelineOpen(true); diff --git a/celstomp/css/components/island.css b/celstomp/css/components/island.css index b764d09..4eb3d70 100644 --- a/celstomp/css/components/island.css +++ b/celstomp/css/components/island.css @@ -54,6 +54,50 @@ cursor: grabbing; } +.islandDock.drag-lock-candidate { + outline: 2px solid rgba(92, 179, 255, 0.6); + outline-offset: -2px; +} + +.islandDock.right-locked { + left: auto !important; + right: var(--dock-gap); + top: calc(var(--header-h) + var(--dock-gap)); + bottom: calc(var(--timeline-h) + var(--dock-gap-bottom)); + height: auto !important; + width: min(320px, 36vw); +} + +.islandDock.right-locked .islandHeader { + background: rgba(92, 179, 255, 0.12); + border-bottom-color: rgba(92, 179, 255, 0.35); +} + +.islandLockHint { + position: fixed; + top: calc(var(--header-h) + 14px); + right: 10px; + width: 180px; + height: calc(100vh - var(--header-h) - var(--timeline-h) - 24px); + border: 2px dashed rgba(92, 179, 255, 0.32); + border-radius: 10px; + background: rgba(92, 179, 255, 0.08); + color: rgba(180, 225, 255, 0.9); + display: none; + align-items: center; + justify-content: center; + text-align: center; + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + z-index: 39; + pointer-events: none; +} + +.islandLockHint.active { + display: flex; +} + .islandTitle{ font-size: 12px; letter-spacing: 0.08em; @@ -222,6 +266,30 @@ overflow-x: hidden; } +.islandSideBody { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.24) rgba(0,0,0,0.22); +} + +.islandSideBody::-webkit-scrollbar { + width: 10px; +} + +.islandSideBody::-webkit-scrollbar-track { + background: rgba(0,0,0,0.24); + border-radius: 999px; +} + +.islandSideBody::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.24); + border-radius: 999px; + border: 2px solid rgba(0,0,0,0.22); +} + +.islandSideBody::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.34); +} + .islandSideGrid{ display: flex; flex-direction: column; diff --git a/celstomp/css/components/layers.css b/celstomp/css/components/layers.css index 9396b0d..d659d64 100644 --- a/celstomp/css/components/layers.css +++ b/celstomp/css/components/layers.css @@ -104,6 +104,30 @@ body.swatch-reordering{ border-radius: 0px; } +#islandLayersSlot { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.24) rgba(0,0,0,0.22); +} + +#islandLayersSlot::-webkit-scrollbar { + width: 10px; +} + +#islandLayersSlot::-webkit-scrollbar-track { + background: rgba(0,0,0,0.24); + border-radius: 999px; +} + +#islandLayersSlot::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.24); + border-radius: 999px; + border: 2px solid rgba(0,0,0,0.22); +} + +#islandLayersSlot::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.34); +} + #islandLayersSlot #layerSeg{ display: flex !important; flex-direction: column !important; diff --git a/celstomp/css/components/overlays.css b/celstomp/css/components/overlays.css index ed86422..e644f68 100644 --- a/celstomp/css/components/overlays.css +++ b/celstomp/css/components/overlays.css @@ -120,6 +120,124 @@ opacity: .95; } +.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; +} + .infoList{ margin: 0; padding-left: 18px; diff --git a/celstomp/css/components/timeline.css b/celstomp/css/components/timeline.css index 9256498..7fa66dd 100644 --- a/celstomp/css/components/timeline.css +++ b/celstomp/css/components/timeline.css @@ -21,15 +21,16 @@ min-height:0; } -.app.tl-collapsed #timeline{ display:none; } +.app.tl-collapsed #timeline, +body.tl-collapsed #timeline{ display:none; } #timelineHeader{ display:flex; align-items:center; justify-content:space-between; - padding:8px 12px; + padding:6px 12px; color:#c7d0db; - gap:10px; + gap:8px; flex-wrap:wrap; position: relative; } @@ -37,7 +38,7 @@ #timelineHeader .right{ display:flex; align-items:center; - gap:8px; + gap:6px; flex-wrap:wrap; } #timelineHeader .center{ @@ -45,10 +46,159 @@ display:flex; align-items:center; justify-content:center; - gap:8px; + gap:6px; flex-wrap:wrap; } +#timelineHeader button { + appearance: none; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.06); + color: #c7d0db; + border-radius: 8px; + min-height: 32px; + padding: 0 10px; + font-size: 12px; +} + +#timelineHeader button:hover { + background: rgba(255,255,255,0.11); + border-color: rgba(255,255,255,0.2); +} + +#timelineHeader input[type="number"] { + appearance: textfield; + -moz-appearance: textfield; +} + +#timelineHeader input[type="number"]::-webkit-outer-spin-button, +#timelineHeader input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.tl-group { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: rgba(0,0,0,0.25); + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.06); +} + +.tl-group-tools { + gap: 2px; +} + +.tl-group-guide { + gap: 2px; +} + +.tl-guide-hint { + font-size: 10px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #ffb7a0; + background: rgba(255, 129, 92, 0.16); + border: 1px solid rgba(255, 129, 92, 0.42); + border-radius: 999px; + padding: 3px 8px; + white-space: nowrap; +} + +.tl-tool-btn { + width: 32px; + height: 32px; + padding: 6px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 6px; + color: #9aa5b5; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.tl-tool-btn:hover { + background: rgba(255,255,255,0.12); + color: #c5cdd8; + border-color: rgba(255,255,255,0.15); +} + +.tl-tool-btn.active { + background: rgba(64, 156, 255, 0.18); + border-color: rgba(64, 156, 255, 0.5); + color: #5cb3ff; +} + +.tl-guide-btn.active { + background: rgba(255, 129, 92, 0.2); + border-color: rgba(255, 129, 92, 0.55); + color: #ff9f7b; +} + +.tl-tool-btn svg { + width: 18px; + height: 18px; +} + +.tl-play-btn { + width: 32px; + height: 32px; + padding: 6px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 6px; + color: #9aa5b5; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.tl-play-btn:hover { + background: rgba(255,255,255,0.12); + color: #c5cdd8; + border-color: rgba(255,255,255,0.15); +} + +.tl-play-main { + width: 40px; + height: 40px; + background: rgba(64, 156, 255, 0.15); + border-color: rgba(64, 156, 255, 0.3); + color: #5cb3ff; +} + +.tl-play-main:hover { + background: rgba(64, 156, 255, 0.25); + border-color: rgba(64, 156, 255, 0.5); +} + +.tl-play-btn svg { + width: 100%; + height: 100%; +} + +.tl-num { + width: 45px; + padding: 4px 6px; + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 4px; + color: #c7d0db; + font-size: 12px; +} + +.tl-num:focus { + outline: none; + border-color: #409cff; +} + #timelineHeader #playBtn, #timelineHeader #pauseBtn, @@ -250,8 +400,130 @@ body.dragging-cel{ cursor: grabbing; user-select:none; } #timelineHeader input[type="number"], #timelineHeader .chip{ min-height:36px; } +.tl-group { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.tl-btn-group { + display: flex; + align-items: center; + gap: 2px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + padding: 2px; +} + +.tl-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: #c7d0db; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.tl-icon-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.tl-icon-btn:active { + background: rgba(255, 255, 255, 0.15); +} + +.tl-icon-btn.danger:hover { + background: rgba(255, 82, 82, 0.2); + color: #ff5252; +} + +.tl-icon-btn svg { + pointer-events: none; +} + +.tl-play-btn { + width: 40px; + height: 40px; + background: rgba(0, 229, 255, 0.15); + color: #9fd5ff; +} + +.tl-play-btn:hover { + background: rgba(0, 229, 255, 0.25); +} + +.tl-play-btn.playing .play-icon { display: none; } +.tl-play-btn.playing .pause-icon { display: block !important; } + +.tl-num-input { + width: 60px; + padding: 4px 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 6px; + background: rgba(0, 0, 0, 0.28); + color: #e2e8f0; + font-size: 12px; + -moz-appearance: textfield; +} + +.tl-num-input::-webkit-outer-spin-button, +.tl-num-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.tl-num-input-sm { + width: 48px; + padding: 2px 6px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 4px; + background: rgba(0, 0, 0, 0.28); + color: #e2e8f0; + font-size: 11px; + -moz-appearance: textfield; +} + +.tl-num-input-sm::-webkit-outer-spin-button, +.tl-num-input-sm::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.tl-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.2); + font-size: 12px; + color: #c7d0db; + cursor: pointer; +} + +.tl-chip input[type="checkbox"] { + margin: 0; +} + +.tl-opts-group { + background: transparent; +} + .tlMobArrow{ display:none; } +.tlMobArrow { + border: 1px solid rgba(255,255,255,0.14) !important; + background: rgba(0,0,0,0.28) !important; + color: #9fb6cd !important; +} + @media (max-width: 900px){ #timelineHeader .center, #timelineHeader .right{ gap: 8px; } diff --git a/celstomp/js/editor/export-helper.js b/celstomp/js/editor/export-helper.js index 1c7fd2f..4600051 100644 --- a/celstomp/js/editor/export-helper.js +++ b/celstomp/js/editor/export-helper.js @@ -8,7 +8,7 @@ const autosaveController = window.CelstompAutosave?.createController?.({ badgeEl: saveStateBadgeEl, buildSnapshot: async () => await buildProjectSnapshot(), pointerSelectors: [ "#drawCanvas", "#fillCurrent", "#fillAll", "#tlDupCel", "#toolSeg label", "#layerSeg .layerRow", "#timelineTable td" ], - valueSelectors: [ "#autofillToggle", "#brushSize", "#brushSizeRange", "#brushSizeNum", "#eraserSize", "#pressureSize", "#pressureOpacity", "#pressureTilt", "#tlSnap", "#tlSeconds", "#tlFps", "#tlOnion", "#tlTransparency", "#loopToggle", "#onionPrevColor", "#onionNextColor", "#onionAlpha", "#onionBlendMode" ], + valueSelectors: [ "#autofillToggle", "#brushSize", "#brushSizeRange", "#brushSizeNum", "#eraserSize", "#pressureSize", "#pressureOpacity", "#pressureTilt", "#tlSnap", "#tlSeconds", "#tlFps", "#tlOnion", "#tlTransparency", "#loopToggle", "#onionPrevColor", "#onionNextColor", "#onionAlpha", "#onionBlendMode", "#tlGridToggle", "#tlGridSnap", "#tlGridSize" ], onRestorePayload: (payload, source) => { const blob = new Blob([ JSON.stringify(payload.data) ], { type: "application/json" diff --git a/celstomp/js/ui/interaction-shortcuts.js b/celstomp/js/ui/interaction-shortcuts.js index 223d3ac..514d077 100644 --- a/celstomp/js/ui/interaction-shortcuts.js +++ b/celstomp/js/ui/interaction-shortcuts.js @@ -295,6 +295,8 @@ function _wireTimelineEnhancements() { const zoomIn = $("zoomTimelineIn"); const zoomOut = $("zoomTimelineOut"); + applyTimelineFrameWidth(readTimelineFrameWidth()); + if (insertBtn) { insertBtn.addEventListener("click", () => insertFrame(currentFrame)); } @@ -317,32 +319,59 @@ function _wireTimelineEnhancements() { } if (zoomIn) { zoomIn.addEventListener("click", () => { - timelineFrameWidth = Math.min(100, timelineFrameWidth + 5); - renderTimeline(); + applyTimelineFrameWidth(timelineFrameWidth + 5); }); } if (zoomOut) { zoomOut.addEventListener("click", () => { - timelineFrameWidth = Math.max(15, timelineFrameWidth - 5); - renderTimeline(); + applyTimelineFrameWidth(timelineFrameWidth - 5); }); } } + +function readTimelineFrameWidth() { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue("--frame-w"); + const parsed = parseFloat(cssValue); + if (Number.isFinite(parsed)) return parsed; + return timelineFrameWidth; +} + +function applyTimelineFrameWidth(value) { + const next = Math.max(15, Math.min(100, Math.round(Number(value) || 24))); + timelineFrameWidth = next; + document.documentElement.style.setProperty("--frame-w", `${next}px`); + if (typeof updatePlayheadMarker === "function") updatePlayheadMarker(); + if (typeof updateClipMarkers === "function") updateClipMarkers(); +} + +function refreshTimelineView() { + if (typeof buildTimeline === "function") { + buildTimeline(); + if (typeof updatePlayheadMarker === "function") updatePlayheadMarker(); + if (typeof updateClipMarkers === "function") updateClipMarkers(); + return; + } + if (typeof renderTimeline === "function") { + renderTimeline(); + } +} + function insertFrame(frameIndex) { beginGlobalHistoryStep(); for (let i = totalFrames - 1; i >= frameIndex; i--) { const src = getFrameCanvas(LAYER.LINE, i, null); if (src) { - const ctx = getFrameCanvas(LAYER.LINE, i + 1, null); - if (ctx) { - ctx.clearRect(0, 0, contentW, contentH); - ctx.drawImage(src, 0, 0); + const dst = getFrameCanvas(LAYER.LINE, i + 1, null); + const dctx = dst?.getContext?.("2d"); + if (dctx) { + dctx.clearRect(0, 0, contentW, contentH); + dctx.drawImage(src, 0, 0); } } } totalFrames++; markProjectDirty(); - renderTimeline(); + refreshTimelineView(); commitGlobalHistoryStep(); } function deleteFrame(frameIndex) { @@ -354,10 +383,11 @@ function deleteFrame(frameIndex) { for (let i = frameIndex; i < totalFrames - 1; i++) { const src = getFrameCanvas(LAYER.LINE, i + 1, null); if (src) { - const ctx = getFrameCanvas(LAYER.LINE, i, null); - if (ctx) { - ctx.clearRect(0, 0, contentW, contentH); - ctx.drawImage(src, 0, 0); + const dst = getFrameCanvas(LAYER.LINE, i, null); + const dctx = dst?.getContext?.("2d"); + if (dctx) { + dctx.clearRect(0, 0, contentW, contentH); + dctx.drawImage(src, 0, 0); } } } @@ -369,7 +399,7 @@ function deleteFrame(frameIndex) { totalFrames--; if (currentFrame >= totalFrames) currentFrame = totalFrames - 1; markProjectDirty(); - renderTimeline(); + refreshTimelineView(); commitGlobalHistoryStep(); } function _wireLayerQoL() { @@ -580,7 +610,6 @@ function _wireExtraKeyboardShortcuts() { }, true); } } - function flipSelection(horizontal) { if (!rectSelection.active) return; const c = getFrameCanvas(rectSelection.L, rectSelection.F, rectSelection.key); @@ -793,6 +822,11 @@ function onWindowKeyDown(e) { } } if (e.key === "Escape") { + if (document.body.classList.contains("guide-place-mode") && window.__celstompSetGuidePlacementMode) { + e.preventDefault(); + window.__celstompSetGuidePlacementMode(null); + return; + } if (tool === "lasso-fill" && lassoActive) { e.preventDefault(); cancelLasso(); @@ -853,6 +887,14 @@ function onWindowKeyDown(e) { } } } + const tag = e.target && e.target.tagName ? e.target.tagName.toUpperCase() : ""; + const typingNow = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || e.target && e.target.isContentEditable; + const textEditorOpen = document.body.classList.contains("canvasTextEntryMode"); + + if ((typingNow || textEditorOpen) && e.key === " ") { + return; + } + if (ctrl && e.key.toLowerCase() === "z" && !e.shiftKey) { e.preventDefault(); undo(); diff --git a/celstomp/js/ui/island-helper.js b/celstomp/js/ui/island-helper.js index ff6e029..2bacc9c 100644 --- a/celstomp/js/ui/island-helper.js +++ b/celstomp/js/ui/island-helper.js @@ -175,10 +175,50 @@ function wireFloatingIslandDrag() { if (!head) return; if (dock._dragWired) return; dock._dragWired = true; + const LOCK_KEY = "celstomp_island_right_locked"; + const LOCK_THRESHOLD = 170; + + let lockHint = $("islandLockHint"); + if (!lockHint) { + lockHint = document.createElement("div"); + lockHint.id = "islandLockHint"; + lockHint.className = "islandLockHint"; + lockHint.textContent = "Drop To Dock Right"; + document.body.appendChild(lockHint); + } + + function setRightLocked(v, opts = {}) { + const isLocked = !!v; + const persist = opts.persist !== false; + dock.classList.toggle("right-locked", isLocked); + if (isLocked) { + dock.style.left = ""; + dock.style.top = ""; + dock.style.right = "var(--dock-gap)"; + } else { + dock.style.right = ""; + } + if (persist) { + try { + localStorage.setItem(LOCK_KEY, isLocked ? "1" : "0"); + } catch {} + } + } + + try { + const saved = localStorage.getItem(LOCK_KEY); + if (saved === "1") { + setRightLocked(true, { + persist: false + }); + } + } catch {} + let dragging = false; let pid = null; let offX = 0; let offY = 0; + let lockCandidate = false; let cachedHeaderH = 48; let cachedVW = window.innerWidth; let cachedVH = window.innerHeight; @@ -202,6 +242,15 @@ function wireFloatingIslandDrag() { head.addEventListener("pointerdown", e => { if (e.pointerType === "mouse" && e.button !== 0) return; if (e.target.closest(".islandBtn, .islandBtns, .islandResizeHandle")) return; + + if (dock.classList.contains("right-locked")) { + const rLocked = dock.getBoundingClientRect(); + setRightLocked(false, { + persist: false + }); + dock.style.left = Math.round(rLocked.left) + "px"; + dock.style.top = Math.round(rLocked.top) + "px"; + } updateCache(); @@ -223,6 +272,9 @@ function wireFloatingIslandDrag() { const pos = clampPos(e.clientX - offX, e.clientY - offY); dock.style.left = pos.x + "px"; dock.style.top = pos.y + "px"; + lockCandidate = e.clientX >= window.innerWidth - LOCK_THRESHOLD; + dock.classList.toggle("drag-lock-candidate", lockCandidate); + lockHint.classList.toggle("active", lockCandidate); e.preventDefault(); }, { passive: false @@ -231,10 +283,18 @@ function wireFloatingIslandDrag() { if (!dragging || pid != null && e.pointerId !== pid) return; dragging = false; dock.classList.remove("dragging"); + dock.classList.remove("drag-lock-candidate"); + lockHint.classList.remove("active"); try { head.releasePointerCapture(pid); } catch {} pid = null; + if (lockCandidate) { + setRightLocked(true); + } else { + setRightLocked(false); + } + lockCandidate = false; }; window.addEventListener("pointerup", end, { @@ -405,4 +465,4 @@ function wireIslandResize() { }; handle.addEventListener("pointerup", end); handle.addEventListener("pointercancel", end); -} \ No newline at end of file +} diff --git a/celstomp/parts/timeline.js b/celstomp/parts/timeline.js index d98b5dc..12f4661 100644 --- a/celstomp/parts/timeline.js +++ b/celstomp/parts/timeline.js @@ -8,47 +8,77 @@ document.getElementById('part-timeline').innerHTML = ` aria-expanded="false">▴ -
+
Timeline 0s+0f Loop - - - - +
+ + +
+ +
-
- - - - - +
+
+ + + + + +
-
+
+
+ + +
- - +
+ + +
- - - +
+ + +
- - +
+ + +
- - - - - Zoom - - - +
@@ -61,5 +91,5 @@ document.getElementById('part-timeline').innerHTML = ` - + `; diff --git a/package-lock.json b/package-lock.json index a7086ee..e3f3a2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -915,6 +915,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" },