diff --git a/content/tilesets.typ b/content/tilesets.typ index 99400c92..9685c1a0 100644 --- a/content/tilesets.typ +++ b/content/tilesets.typ @@ -1,6 +1,5 @@ #import "/lib.typ": diorama, example, note, todo - = Tilesets The protocols as described are very sensitive to the exact timing that the EID counter is @@ -14,12 +13,15 @@ sequence* is a chain of redstone components that all update in the scheduled tic set* is a system of constructing tile sequences that enforces a particular global ordering. #show raw.where(lang: "mc-diorama"): it => diorama( + orbit: true, + pan: true, + zoom: true, autoplay: true, - loop: true, theta: 160, phi: 20, it.text, ) + ```mc-diorama p 0 -1 0 smooth_stone_slab type=top p 1 -1 0 smooth_stone_slab type=top diff --git a/src/cache.js b/src/cache.js index bc1d4e26..41ec910c 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,5 +1,3 @@ -// cache.js - export class Cache { constructor(root, onResolve) { this.root = root; diff --git a/src/components.css b/src/components.css index 28093cb5..2790d272 100644 --- a/src/components.css +++ b/src/components.css @@ -1,4 +1,3 @@ -/* --- Component Structural Styles --- */ mc-world, mc-timeline, mc-camera, mc-frame { display: none; } @@ -9,256 +8,88 @@ mc-diorama { width: 100%; height: 20em; - & canvas { + canvas { display: block; width: 100%; height: 100%; } -} -.spacetime-overlay { - position: absolute; - inset: 0; /* Replaces top, left, width, height */ - padding: 5px; - pointer-events: none; - display: flex; - flex-direction: column; - justify-content: space-between; - box-sizing: border-box; - z-index: 10; - font-family: sans-serif; - /* Core icon engine rules */ - - & .icon { - display: inline-block; - width: 1.2em; - height: 1.2em; - background-color: currentColor; /* Inherits text color (white, or hover states) */ - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - -webkit-mask-size: contain; - - /* Map classes to your local SVG paths */ - - &.ico-play { - mask-image: url('ico-play.svg'); - -webkit-mask-image: url('ico-play.svg'); - } - - &.ico-pause { - mask-image: url('ico-pause.svg'); - -webkit-mask-image: url('ico-pause.svg'); - } - - &.ico-auto-events { - mask-image: url('ico-auto-events.svg'); - -webkit-mask-image: url('ico-auto-events.svg'); - } - - &.ico-prev { - mask-image: url('ico-prev.svg'); - -webkit-mask-image: url('ico-prev.svg'); - } - - &.ico-next { - mask-image: url('ico-next.svg'); - -webkit-mask-image: url('ico-next.svg'); - } - - &.ico-prev-event { - mask-image: url('ico-prev-event.svg'); - -webkit-mask-image: url('ico-prev-event.svg'); - } - - &.ico-next-event { - mask-image: url('ico-next-event.svg'); - -webkit-mask-image: url('ico-next-event.svg'); - } - - &.ico-reset { - mask-image: url('ico-reset.svg'); - -webkit-mask-image: url('ico-reset.svg'); - } - - /* Spatial indicators (mapped for future integration if desired) */ - - &.ico-rotate { - mask-image: url('ico-rotate.svg'); - -webkit-mask-image: url('ico-rotate.svg'); - } - - &.ico-pan { - mask-image: url('ico-pan.svg'); - -webkit-mask-image: url('ico-pan.svg'); - } - - &.ico-zoom { - mask-image: url('ico-zoom.svg'); - -webkit-mask-image: url('ico-zoom.svg'); - } - } - - /* Define the safe zones for the top cluster */ - - & .top-row { - display: flex; - justify-content: space-between; - align-items: flex-start; - pointer-events: none; - } - - /* Spatial Tool Hints */ - - & .spacetime-indicator { - display: flex; - gap: 4px; - /* Mimic the .btn box model so baselines perfectly align with the Play button */ - padding: 4px 8px; - border: 1px solid transparent; - color: rgba(255, 255, 255, 0.4); - font-family: monospace; - font-size: 14px; - opacity: 1; - transition: opacity 0.2s ease; - pointer-events: auto; - } - - /* Unified Button Design Language */ - - & .btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; /* Spacing between icon and text for the reset button */ - - } - - & .btn { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: white; - padding: 4px 8px; - border-radius: 4px; - cursor: pointer; - text-align: center; - font-weight: bold; - font-family: inherit; - backdrop-filter: blur(4px); - pointer-events: auto; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } - } - - & .top-row .btn { - background: rgba(0, 0, 0, 0.6); - visibility: hidden; - - &:hover { - background: rgba(0, 0, 0, 0.8); - } - } - - /* Bottom: Timeline Panel Context */ - - & .visualizer-ui { - pointer-events: none; - background: transparent; - margin: -5px; + .overlay { + position: absolute; + inset: 0; padding: 5px; + pointer-events: none; display: flex; flex-direction: column; - gap: 4px; - color: white; - user-select: none; - border: 1px solid transparent; - transition: all 0.2s ease; + justify-content: space-between; + box-sizing: border-box; + z-index: 10; + font-family: sans-serif; - & .scrubber-row { - display: flex; - gap: 4px; - align-items: center; - } + .icon { + display: inline-block; + width: 1.2em; + height: 1.2em; + background-color: currentColor; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: contain; - & .track-container { - flex-grow: 1; - height: 24px; - padding: 0 8px; /* Safe zone so the rounded handle doesn't bleed out */ - cursor: pointer; - display: flex; - align-items: center; - touch-action: none; + &.ico-reset { + mask-image: url('ico-reset.svg'); + -webkit-mask-image: url('ico-reset.svg'); + } - & .track-inner { - position: relative; - width: 100%; - height: 8px; /* Defines the actual track geometry */ - background: rgba(255, 255, 255, 0.2); - pointer-events: none; + &.ico-rotate { + mask-image: url('ico-rotate.svg'); + -webkit-mask-image: url('ico-rotate.svg'); + } - & .track-fill { - position: absolute; - left: 0; - top: 0; - bottom: 0; /* Snaps tightly to inner geometry */ - background: #f00; - } + &.ico-pan { + mask-image: url('ico-pan.svg'); + -webkit-mask-image: url('ico-pan.svg'); + } - & .track-handle { - position: absolute; - width: 16px; - height: 16px; - background: #fff; - border-radius: 50%; - top: 50%; - transform: translate(-50%, -50%); - z-index: 3; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); - } - - & .marker { - position: absolute; - top: 0; - bottom: 0; /* Matches track height */ - transform: translateX(-50%); - pointer-events: none; - z-index: 1; - - &.marker-event { - width: 2px; - background: rgba(255, 255, 255, 0.5); - } - - &.marker-label { - width: 4px; - background: gold; - z-index: 2; - } - } + &.ico-zoom { + mask-image: url('ico-zoom.svg'); + -webkit-mask-image: url('ico-zoom.svg'); } } - } -} -/* --- Hover Visibility Logic --- */ -.spacetime-hover-reveal { - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease; -} + .top-row { + display: flex; + justify-content: flex-start; + align-items: flex-start; + gap: 4px; -.spacetime-container:hover { - & .spacetime-hover-reveal { - opacity: 1; - pointer-events: auto; + padding: 4px 8px; + + color: rgba(255, 255, 255, 0.4); + + transition: opacity 0.2s ease; + pointer-events: auto; + } + + .ico-reset { + margin-inline-start: auto; + cursor: pointer; + } } - & .visualizer-ui { - pointer-events: auto; + .spacetime-hover-reveal { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + } + + &:hover { + .spacetime-hover-reveal { + opacity: 1; + pointer-events: auto; + } } } \ No newline at end of file diff --git a/src/components.js b/src/components.js index 9224053f..1b94526b 100644 --- a/src/components.js +++ b/src/components.js @@ -1,4 +1,3 @@ -// components.js import {Engine} from './engine.js'; import {World, WorldFrame} from './world.js'; import {Diorama} from './diorama.js'; diff --git a/src/controller.js b/src/controller.js index d18945f3..b15e5554 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,5 +1,3 @@ -// controller.js - export class SpacetimeController { constructor(diorama, options = {}) { this.diorama = diorama; @@ -7,136 +5,68 @@ export class SpacetimeController { this.container = diorama.element; this.canvas = diorama.canvas; - this.opts = { - orbit: true, - pan: true, - zoom: true, - timeline: true, - subticks: true, - autoplay: false, - loop: false, - speed: 1.0, ...options - }; + this.opts = {orbit: true, pan: true, zoom: true, speed: 1.0, ...options}; this.hasSpatial = this.opts.orbit || this.opts.pan || this.opts.zoom; - // Camera State this.isDragging = false; - this.hasDragged = false; this.dragButton = 0; this.lastMouse = {x: 0, y: 0}; - // Timeline State - this.playMode = this.opts.autoplay ? 'realtime' : 'paused'; - this.isLooping = this.opts.loop; this.speed = this.opts.speed; this.lastFrameTime = performance.now(); - this.uiTickTime = 0; - this.playbackAccumulator = 0; - - this.container.classList.add('spacetime-container'); this.buildUI(); this.attachEvents(); - if (this.opts.timeline || this.opts.autoplay) { - this.frame.subscribe(() => this.syncUI()); - this._loop = this.loop.bind(this); - requestAnimationFrame(this._loop); - } + requestAnimationFrame(this.loop.bind(this)); } buildUI() { - this.overlay = document.createElement('div'); - this.overlay.className = 'spacetime-overlay'; + const overlay = document.createElement('div'); + overlay.className = 'overlay'; + this.container.appendChild(overlay); - // --- Top Row Wrapper --- - const topRow = document.createElement('div'); - topRow.className = 'top-row'; + const header = document.createElement('div'); + header.className = 'top-row'; + overlay.appendChild(header); - // --- Feature Indicator (Top Left) --- - this.indicator = document.createElement('div'); - this.indicator.className = 'spacetime-indicator'; - - // Inject individual SVG icons with structural tooltips if (this.opts.orbit) { - this.indicator.innerHTML += ``; + let elt = document.createElement('span'); + elt.className = 'icon ico-rotate'; + elt.title = 'Rotate view: left drag'; + header.appendChild(elt); } + if (this.opts.pan) { - this.indicator.innerHTML += ``; + let elt = document.createElement('span'); + elt.className = 'icon ico-pan'; + elt.title = 'Move view: middle drag (or shift left drag)'; + header.appendChild(elt); } + if (this.opts.zoom) { - this.indicator.innerHTML += ``; + let elt = document.createElement('span'); + elt.className = 'icon ico-zoom'; + elt.title = 'Zoom view: right drag (or ctrl left drag)'; + header.appendChild(elt); } - topRow.appendChild(this.indicator); - // --- Reset Button (Top Right) --- if (this.hasSpatial) { - this.resetBtn = document.createElement('div'); - this.resetBtn.className = 'btn spacetime-hover-reveal'; - this.resetBtn.innerHTML = ``; + let elt = document.createElement('span'); + elt.className = 'spacetime-hover-reveal icon ico-reset'; + elt.title = 'Reset camera'; + header.appendChild(elt); - this.resetBtn.addEventListener('click', () => { + elt.addEventListener('click', () => { this.diorama.loadState(); this.resetBtn.style.visibility = 'hidden'; + this.resetBtn = elt; }); - - topRow.appendChild(this.resetBtn); + this.resetBtn = elt; } - - this.overlay.appendChild(topRow); - - // --- Bottom: Timeline Controls --- - if (this.opts.timeline) { - this.uiContainer = document.createElement('div'); - this.uiContainer.className = 'visualizer-ui'; - - if (this.opts.subticks) { - this.subRow = document.createElement('div'); - this.subRow.className = 'scrubber-row spacetime-hover-reveal'; - this.subRow.innerHTML = ` -
-
-
-
-
-
-
-
-
-
- `; - this.uiContainer.appendChild(this.subRow); - } - - this.tickRow = document.createElement('div'); - this.tickRow.className = 'scrubber-row'; - this.tickRow.innerHTML = ` -
-
-
-
-
-
-
-
-
-
-
- `; - this.uiContainer.appendChild(this.tickRow); - this.overlay.appendChild(this.uiContainer); - - this.bindTimelineEvents(); - this.generateTickMarkers(); - this.syncUI(); - } - - this.container.appendChild(this.overlay); } - // --- SPATIAL CONTROL --- - orbit(dx, dy) { if (!this.opts.orbit) return; this.diorama.theta -= dx * 0.4; @@ -160,15 +90,9 @@ export class SpacetimeController { this.diorama.target[2] += (-rightZ * dx + upZ * dy) * panSpeed; } - zoomRatio(ratio) { - if (!this.opts.zoom) return; - this.diorama.radius *= ratio; - this.diorama.radius = Math.max(1, Math.min(50, this.diorama.radius)); - } - zoomLinear(delta) { if (!this.opts.zoom) return; - this.diorama.radius += delta * 0.05; + this.diorama.radius += delta * 0.0125; this.diorama.radius = Math.max(1, Math.min(50, this.diorama.radius)); } @@ -178,21 +102,17 @@ export class SpacetimeController { e.preventDefault() }); - // Correctly set or remove the grab cursor this.canvas.style.cursor = this.hasSpatial ? 'grab' : 'default'; const notifyCameraChange = () => { - this.hasDragged = true; if (this.resetBtn && this.diorama.checkpoint) this.resetBtn.style.visibility = 'visible'; this.diorama.requestRender(); }; this.canvas.addEventListener('mousedown', (e) => { - // Short-circuit drag logic entirely if no spatial controls are enabled if (!this.hasSpatial) return; this.isDragging = true; - this.hasDragged = false; this.lastMouse = {x: e.clientX, y: e.clientY}; this.dragButton = e.button; @@ -222,17 +142,6 @@ export class SpacetimeController { } }); - this.canvas.addEventListener('click', () => { - // hasDragged will always be false if hasSpatial is false, - // ensuring static scenes still act as tap-to-play toggles. - if (!this.hasDragged && this.opts.timeline) { - this.playMode = this.playMode === 'paused' ? 'realtime' : 'paused'; - this.syncUI(); - } - }); - - // --- MOBILE TOUCH LOGIC ("Wait and See" Scroll vs Camera) --- - this.touchEngaged = false; this.touchTimer = null; this.touchStartPos = new Map(); @@ -240,7 +149,6 @@ export class SpacetimeController { this.canvas.addEventListener('touchstart', (e) => { if (!this.hasSpatial) return; - // DO NOT call e.preventDefault() here, or the page will never scroll! for (let i = 0; i < e.changedTouches.length; i++) { const t = e.changedTouches[i]; @@ -248,22 +156,20 @@ export class SpacetimeController { this.lastTouchPos.set(t.identifier, {x: t.clientX, y: t.clientY}); } - // A new finger restarts the timeout clearTimeout(this.touchTimer); this.touchEngaged = false; this.touchTimer = setTimeout(() => { this.touchEngaged = true; - // Once engaged, disable click propagation so it doesn't toggle play/pause - this.hasDragged = true; }, 200); }, {passive: false}); this.canvas.addEventListener('touchmove', (e) => { - if (!this.hasSpatial) return; + if (!this.hasSpatial) { + return; // Let the browser scroll the page + } if (!this.touchEngaged) { - // Check if they are swiping quickly before the timer completes. let maxDist = 0; for (let i = 0; i < e.touches.length; i++) { const t = e.touches[i]; @@ -272,14 +178,12 @@ export class SpacetimeController { maxDist = Math.max(maxDist, Math.hypot(t.clientX - start.x, t.clientY - start.y)); } } - // If they drag more than 10px before the 200ms timer, assume page scroll intent if (maxDist > 10) clearTimeout(this.touchTimer); return; // Let the browser scroll the page } - // --- CAMERA IS ENGAGED --- - e.preventDefault(); // Stop the page from scrolling - this.hasDragged = true; + // Fall through. Stop the page from scrolling and handle orbit/zoom. + e.preventDefault(); if (e.touches.length === 1) { // 1 Finger: Orbit @@ -333,338 +237,26 @@ export class SpacetimeController { this.canvas.addEventListener('touchcancel', handleTouchEnd); } - // --- TEMPORAL CONTROL --- getMaxTime() { const evs = this.frame.world.events; - // Add +1 so the final tick plays out its full duration! return evs.length > 0 ? evs[evs.length - 1].time + 1 : 0; } - generateTickMarkers() { - const markerContainer = this.uiContainer.querySelector('#markers-tick'); - const maxTime = this.getMaxTime(); - if (maxTime <= 0) return; - - const labeledTimes = new Set(); - for (const idx of this.frame.world.labels.values()) { - if (idx < this.frame.world.events.length) { - labeledTimes.add(this.frame.world.events[idx].time); - } - } - - for (const t of labeledTimes) { - if (t < 0) continue; - const pct = (t / maxTime) * 100; - const marker = document.createElement('div'); - marker.className = 'marker marker-label'; - marker.style.left = `${pct}%`; - markerContainer.appendChild(marker); - } - } - - setupDraggableTrack(containerId, innerId, onDrag) { - const wrapper = this.uiContainer.querySelector('#' + containerId); - const inner = this.uiContainer.querySelector('#' + innerId); - let isTrackDragging = false; - - const update = (e) => { - const rect = inner.getBoundingClientRect(); - let pct = (e.clientX - rect.left) / rect.width; - pct = Math.max(0, Math.min(1, pct)); - onDrag(pct); - }; - - wrapper.addEventListener('pointerdown', (e) => { - wrapper.setPointerCapture(e.pointerId); - isTrackDragging = true; - this.playMode = 'paused'; - update(e); - }); - - wrapper.addEventListener('pointermove', (e) => { - if (isTrackDragging) update(e); - }); - - const stopDrag = (e) => { - if (isTrackDragging) { - isTrackDragging = false; - wrapper.releasePointerCapture(e.pointerId); - } - }; - - wrapper.addEventListener('pointerup', stopDrag); - wrapper.addEventListener('pointercancel', stopDrag); - } - - bindTimelineEvents() { - const getEl = id => this.uiContainer.querySelector('#' + id); - - getEl('btn-loop').onclick = () => { - this.isLooping = !this.isLooping; - this.syncUI(); - }; - - getEl('btn-play').onclick = () => { - if (this.playMode === 'realtime') this.playMode = 'paused'; - else { - // Check if time is exhausted OR if all events have been processed - const isAtEnd = this.frame.currentTime >= this.getMaxTime() || this.frame.currentIndex >= this.frame.world.events.length; - - if (isAtEnd || this.frame.currentTime < 0) { - this.frame.seek(0); - this.uiTickTime = 0; // Explicitly reset UI sync - } - this.playMode = 'realtime'; - } - this.syncUI(); - }; - - getEl('btn-prev').onclick = () => { - this.playMode = 'paused'; - let prevTime = Math.floor(this.uiTickTime - 1); - - if (prevTime < 0) prevTime = this.isLooping ? this.getMaxTime() : 0; - - this.frame.seek(prevTime); - this.uiTickTime = prevTime; // Explicit jump - this.syncUI(); - }; - - getEl('btn-next').onclick = () => { - this.playMode = 'paused'; - let nextTime = Math.floor(this.uiTickTime + 1); - - if (nextTime > this.getMaxTime()) nextTime = this.isLooping ? 0 : this.getMaxTime(); - - this.frame.seek(nextTime); - this.uiTickTime = nextTime; // Explicit jump - this.syncUI(); - }; - - this.setupDraggableTrack('track-tick-container', 'track-tick-inner', (pct) => { - const targetTime = Math.round(pct * this.getMaxTime()); - this.frame.seek(targetTime); - this.uiTickTime = targetTime; // Explicit jump. Do not snap to event index! - this.syncUI(); - }); - - if (this.opts.subticks) { - getEl('btn-play-sub').onclick = () => { - if (this.playMode === 'subtick') this.playMode = 'paused'; - else { - // Check if all events are processed - if (this.frame.currentIndex >= this.frame.world.events.length) { - this.frame.seekIndex(this.frame.world.initialIndex || 0); - const events = this.frame.world.events; - this.uiTickTime = events.length > 0 ? events[0].time : 0; - } - this.playMode = 'subtick'; - } - this.syncUI(); - }; - - getEl('btn-prev-sub').onclick = () => { - this.playMode = 'paused'; - let idx = this.frame.currentIndex - 1; - const events = this.frame.world.events; - const initialIndex = this.frame.world.initialIndex || 0; - - if (idx < initialIndex) idx = this.isLooping ? events.length : initialIndex; - this.frame.seekIndex(idx); - - // Allow the UI to fall backwards naturally - if (idx < events.length && idx >= 0) this.uiTickTime = events[idx].time; - this.syncUI(); - }; - - getEl('btn-next-sub').onclick = () => { - this.playMode = 'paused'; - let idx = this.frame.currentIndex + 1; - const events = this.frame.world.events; - const initialIndex = this.frame.world.initialIndex || 0; - - if (idx > events.length) idx = this.isLooping ? initialIndex : events.length; - this.frame.seekIndex(idx); - - // Force UI to advance if the button naturally crossed the threshold - if (idx < events.length && idx >= 0) { - this.uiTickTime = events[idx].time; - } else if (events.length > 0) { - this.uiTickTime = events[events.length - 1].time; - } - this.syncUI(); - }; - - this.setupDraggableTrack('track-sub-container', 'track-sub-inner', (pct) => { - if (this.currentTickEventCount === 0) return; - let targetIdx = this.currentTickStartIndex + Math.round(pct * this.currentTickEventCount); - this.frame.seekIndex(targetIdx); - // Intentionally omit updating uiTickTime to let scrubbing "stick" to the current tick. - this.syncUI(); - }); - } - } - - syncUI() { - if (!this.opts.timeline) return; - const getEl = id => this.uiContainer.querySelector('#' + id); - - const playButton = getEl('btn-play'); - const playIcon = playButton.querySelector('.icon'); - if (this.playMode === 'realtime') { - playIcon.className = 'icon ico-pause'; - playButton.setAttribute('title', 'Pause gametick animation'); // Dynamic Tooltip - } else { - playIcon.className = 'icon ico-play'; - playButton.setAttribute('title', 'Animate gameticks'); // Dynamic Tooltip - } - - getEl('btn-loop').style.background = this.isLooping ? 'rgba(102, 204, 102, 0.4)' : 'rgba(255, 255, 255, 0.1)'; - getEl('btn-loop').style.borderColor = this.isLooping ? '#6c6' : 'rgba(255, 255, 255, 0.2)'; - - if (this.opts.subticks) { - const subPlayButton = getEl('btn-play-sub'); - const subPlayIcon = subPlayButton.querySelector('.icon'); - if (this.playMode === 'subtick') { - subPlayIcon.className = 'icon ico-pause'; - subPlayButton.setAttribute('title', 'Pause block update animation'); // Dynamic Tooltip - } else { - subPlayIcon.className = 'icon ico-auto-events'; - subPlayButton.setAttribute('title', 'Animate block updates'); // Dynamic Tooltip - } - this.subRow.style.display = this.playMode === 'realtime' ? 'none' : 'flex'; - } - - // Force uiTickTime to perfectly match realtime flow - if (this.playMode === 'realtime') { - this.uiTickTime = Math.floor(this.frame.currentTime); - } - - const maxTime = this.getMaxTime(); - - // Fix: Decouple the UI tick track from realtime when scrubbed/paused - let displayTime = Math.max(0, this.frame.currentTime); - if (this.playMode !== 'realtime') { - displayTime = Math.max(0, this.uiTickTime); - } - - const tickPct = maxTime > 0 ? (displayTime / maxTime) * 100 : 0; - - getEl('fill-tick').style.width = `${tickPct}%`; - getEl('handle-tick').style.left = `${tickPct}%`; - - if (!this.opts.subticks) return; - - const events = this.frame.world.events; - if (events.length === 0) return; - - // --- Verify uiTickTime is valid for the current index --- - let startIdx = -1, count = 0; - for (let i = 0; i < events.length; i++) { - if (events[i].time === this.uiTickTime) { - if (startIdx === -1) startIdx = i; - count++; - } - } - - // Fix: Only auto-correct if we are NOT in realtime AND the tick isn't empty. - // This stops empty ticks from auto-snapping to filled ticks. - if (this.playMode !== 'realtime' && startIdx !== -1) { - // Because scrubbing to 100% means currentIndex == startIdx + count, - // this > condition strictly permits the inclusive UI overlap! - if (this.frame.currentIndex < startIdx || this.frame.currentIndex > startIdx + count) { - if (this.frame.currentIndex < events.length) { - this.uiTickTime = events[this.frame.currentIndex].time; - } else { - this.uiTickTime = events[events.length - 1].time; - } - - // Recalculate bounds for the newly corrected tick - startIdx = -1; - count = 0; - for (let i = 0; i < events.length; i++) { - if (events[i].time === this.uiTickTime) { - if (startIdx === -1) startIdx = i; - count++; - } - } - } - } - - this.currentTickStartIndex = startIdx; - this.currentTickEventCount = count; - - const markerContainer = getEl('markers-sub'); - markerContainer.innerHTML = ''; - - if (count > 0) { - const labeledIndices = new Set(this.frame.world.labels.values()); - for (let i = 0; i <= count; i++) { - const markerPct = (i / count) * 100; - const marker = document.createElement('div'); - marker.className = 'marker'; - marker.style.left = `${markerPct}%`; - if (labeledIndices.has(startIdx + i)) marker.classList.add('marker-label'); else marker.classList.add('marker-event'); - markerContainer.appendChild(marker); - } - - let localCursor = Math.max(0, Math.min(count, this.frame.currentIndex - startIdx)); - const subPct = (localCursor / count) * 100; - - getEl('fill-sub').style.width = `${subPct}%`; - getEl('handle-sub').style.left = `${subPct}%`; - getEl('handle-sub').style.display = 'block'; - } else { - getEl('fill-sub').style.width = `0%`; - getEl('handle-sub').style.display = 'none'; - } - } - loop(time) { const dt = (time - this.lastFrameTime) / 1000; this.lastFrameTime = time; const maxTime = this.getMaxTime(); - if (this.playMode === 'realtime') { - let nextTime = this.frame.currentTime + dt * 20 * this.speed; - if (nextTime >= maxTime) { - if (this.isLooping && maxTime > 0) { - this.frame.seek(nextTime % maxTime); - } else { - this.frame.seek(maxTime); - this.uiTickTime = maxTime; // Snap UI to the very end - this.playMode = 'paused'; - } + let nextTime = this.frame.currentTime + dt * 20 * this.speed; + if (nextTime >= maxTime) { + if (maxTime > 0) { + this.frame.seek(nextTime % maxTime); } else { - this.frame.seek(nextTime); + this.frame.seek(maxTime); } - if (this.opts.timeline) this.syncUI(); - } else if (this.playMode === 'subtick') { - this.playbackAccumulator += dt * 4 * this.speed; - if (this.playbackAccumulator >= 1.0) { - const steps = Math.floor(this.playbackAccumulator); - this.playbackAccumulator -= steps; - const nextIdx = this.frame.currentIndex + steps; - const events = this.frame.world.events; - const initialIndex = this.frame.world.initialIndex || 0; - - if (nextIdx > events.length) { - if (this.isLooping) { - this.frame.seekIndex(initialIndex); - if (events.length > 0) this.uiTickTime = events[0].time; - } else { - this.frame.seekIndex(events.length); - this.playMode = 'paused'; - } - } else { - this.frame.seekIndex(nextIdx); - if (nextIdx < events.length) this.uiTickTime = events[nextIdx].time; - } - } - if (this.opts.timeline) this.syncUI(); } else { - this.playbackAccumulator = 0; + this.frame.seek(nextTime); } - requestAnimationFrame(this._loop); + requestAnimationFrame(this.loop.bind(this)); } } \ No newline at end of file diff --git a/src/ico-auto-events.svg b/src/ico-auto-events.svg deleted file mode 100644 index b290c85e..00000000 --- a/src/ico-auto-events.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/ico-next-event.svg b/src/ico-next-event.svg deleted file mode 100644 index d49ea75f..00000000 --- a/src/ico-next-event.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/ico-next.svg b/src/ico-next.svg deleted file mode 100644 index 563a4aa8..00000000 --- a/src/ico-next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/ico-pause.svg b/src/ico-pause.svg deleted file mode 100644 index 240fd3b9..00000000 --- a/src/ico-pause.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/ico-play.svg b/src/ico-play.svg deleted file mode 100644 index cfc5adca..00000000 --- a/src/ico-play.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/src/ico-prev-event.svg b/src/ico-prev-event.svg deleted file mode 100644 index 8ba0c3b3..00000000 --- a/src/ico-prev-event.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/ico-prev.svg b/src/ico-prev.svg deleted file mode 100644 index 80097fef..00000000 --- a/src/ico-prev.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/world.js b/src/world.js index a30fc82c..3136f1bd 100644 --- a/src/world.js +++ b/src/world.js @@ -1,5 +1,3 @@ -// world.js - export class Block { constructor(id, pos, state) { this.id = id; @@ -10,7 +8,6 @@ export class Block { export class World { constructor(script = '') { - // Pure Data State this.events = []; this.labels = new Map(); this.initialState = new Map();