From 20b8ac6e3bc96fde62eedfe8c9533ab79c6e1bf3 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 7 Jul 2026 12:03:23 -0400 Subject: [PATCH] unified styling --- index.html | 34 +++++++- src/components.css | 183 ++++++++++++++++++++++++++++------------ src/controller.js | 113 +++++++++++++------------ src/ico-auto-events.svg | 8 ++ src/ico-next-event.svg | 12 +++ src/ico-next.svg | 1 + src/ico-pan.svg | 1 + src/ico-pause.svg | 1 + src/ico-play.svg | 5 ++ src/ico-prev-event.svg | 17 ++++ src/ico-prev.svg | 1 + src/ico-reset.svg | 1 + src/ico-rotate.svg | 1 + src/ico-zoom.svg | 1 + 14 files changed, 271 insertions(+), 108 deletions(-) create mode 100644 src/ico-auto-events.svg create mode 100644 src/ico-next-event.svg create mode 100644 src/ico-next.svg create mode 100644 src/ico-pan.svg create mode 100644 src/ico-pause.svg create mode 100644 src/ico-play.svg create mode 100644 src/ico-prev-event.svg create mode 100644 src/ico-prev.svg create mode 100644 src/ico-reset.svg create mode 100644 src/ico-rotate.svg create mode 100644 src/ico-zoom.svg diff --git a/index.html b/index.html index 79832c50..fbb69b39 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,15 @@

Visualizer

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nibh mi, aliquam ut urna sit amet, mollis sodales + augue. Duis faucibus nibh eu purus egestas lobortis. In ut vehicula eros. Orci varius natoque penatibus et + magnis dis parturient montes, nascetur ridiculus mus. In hac habitasse platea dictumst. Quisque ultricies mauris + sed tortor cursus, eu eleifend neque vulputate. Suspendisse vitae libero eu tellus porta vestibulum. Quisque + tortor turpis, tincidunt vel enim et, tempor facilisis sem. Sed condimentum elit odio, non finibus sapien + faucibus in. Duis a tempus ex. Mauris sodales nisi ut mi lacinia auctor. Nunc interdum cursus sem luctus + faucibus. Quisque mattis iaculis finibus. Aliquam commodo leo a risus porttitor, vel vulputate nisi + vehicula.

+

Dynamic Pulses

@@ -40,10 +49,17 @@

Static multi-view scene

+

Phasellus tortor turpis, commodo ut tortor scelerisque, fermentum tempus nisi. Pellentesque consectetur commodo + rhoncus. Sed ullamcorper turpis dapibus, luctus nibh eu, dapibus risus. Fusce finibus ante eleifend, venenatis + sem eu, tempor elit. Duis eget lacus justo. Ut efficitur lacus a massa sagittis fringilla. Proin non dictum + eros. Etiam porttitor libero a facilisis vulputate. Duis facilisis eget arcu quis ultricies. Etiam dui erat, + luctus ut tristique vitae, fermentum quis magna.

+ p -1 0 0 redstone_wire east=side west=none north=none south=none power=0 + p -1 -1 0 smooth_stone_slab type=top p 0 0 0 repeater facing=east delay=1 locked=false powered=false p 2 0 0 oak_trapdoor facing=east half=bottom open=true p 0 -1 0 lodestone @@ -54,12 +70,18 @@
- +
+

Sed urna ante, aliquet et ex nec, posuere laoreet sem. Aenean non sem a massa vehicula faucibus. Donec varius + mattis elit ut efficitur. Aenean quis mattis urna. Praesent quis lacus ac ligula sollicitudin mattis. Donec mi + enim, suscipit eget urna a, consectetur suscipit sapien. Etiam lacinia, arcu vel fermentum vulputate, erat nibh + efficitur eros, euismod varius ex eros in urna. Nulla magna ex, congue ac finibus sit amet, tristique nec + ante.

+

Dynamic Timeline Control

@@ -124,6 +146,16 @@
+

Suspendisse potenti. Nulla facilisi. Mauris non ullamcorper justo. Ut euismod lacus eget orci elementum, eu + cursus nisl pellentesque. Vestibulum ligula magna, venenatis eget ligula eu, gravida semper tellus. Praesent + quis enim eu metus hendrerit feugiat. Etiam lorem tortor, condimentum sed condimentum sit amet, placerat + vulputate ipsum. Duis varius lobortis elit, et pharetra velit feugiat sed. Class aptent taciti sociosqu ad + litora torquent per conubia nostra, per inceptos himenaeos. Cras venenatis ligula sed elit aliquam vestibulum. + Duis fringilla, mauris at lacinia posuere, mauris urna auctor lacus, vitae faucibus velit ante sit amet massa. + Ut id massa eget dui vestibulum pretium. Nunc tempus odio quis nisi ultrices, id volutpat dui tristique. + Suspendisse lacus metus, pulvinar ac urna vitae, pellentesque volutpat eros. In cursus ipsum in enim ornare, sed + tempus mi tincidunt.

+
diff --git a/src/components.css b/src/components.css index 5b3ca9c9..c6cbe13f 100644 --- a/src/components.css +++ b/src/components.css @@ -18,19 +18,125 @@ mc-diorama { .spacetime-overlay { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + 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; - padding: 12px; 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); @@ -42,21 +148,19 @@ mc-diorama { font-weight: bold; font-family: inherit; backdrop-filter: blur(4px); - transition: background 0.1s, border-color 0.1s; - pointer-events: auto; /* Always clickable when visible */ + pointer-events: auto; &:hover { background: rgba(255, 255, 255, 0.2); } } - & .reset-wrapper { - display: flex; - justify-content: flex-end; - width: 100%; + & .top-row .btn { + background: rgba(0, 0, 0, 0.6); + visibility: hidden; - & .btn { - visibility: hidden; + &:hover { + background: rgba(0, 0, 0, 0.8); } } @@ -65,8 +169,8 @@ mc-diorama { & .visualizer-ui { pointer-events: none; background: transparent; - padding: 12px; - border-radius: 8px; + margin: -5px; + padding: 5px; display: flex; flex-direction: column; gap: 12px; @@ -88,7 +192,7 @@ mc-diorama { & .track-container { flex-grow: 1; height: 24px; - padding: 0 10px; + padding: 0 8px; /* Safe zone so the rounded handle doesn't bleed out */ cursor: pointer; display: flex; align-items: center; @@ -96,27 +200,16 @@ mc-diorama { & .track-inner { position: relative; width: 100%; - height: 100%; + height: 8px; /* Defines the actual track geometry */ + background: rgba(255, 255, 255, 0.2); pointer-events: none; - & .track-bg { - position: absolute; - left: -10px; - right: -10px; - top: 8px; - height: 8px; - background: rgba(255, 255, 255, 0.2); - border-radius: 4px; - } - & .track-fill { position: absolute; - left: -10px; - top: 8px; - height: 8px; + left: 0; + top: 0; + bottom: 0; /* Snaps tightly to inner geometry */ background: #f00; - width: 10px; - border-radius: 4px; } & .track-handle { @@ -125,7 +218,7 @@ mc-diorama { height: 16px; background: #fff; border-radius: 50%; - top: 12px; + top: 50%; transform: translate(-50%, -50%); z-index: 3; box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); @@ -133,8 +226,8 @@ mc-diorama { & .marker { position: absolute; - top: 8px; - height: 8px; + top: 0; + bottom: 0; /* Matches track height */ transform: translateX(-50%); pointer-events: none; z-index: 1; @@ -153,22 +246,6 @@ mc-diorama { } } } - - /* Spatial Tool Hints */ - - & .spacetime-indicator { - position: absolute; - left: 33px; - top: 29px; - display: flex; - gap: 12px; - color: rgba(255, 255, 255, 0.4); - font-family: monospace; - font-size: 14px; - pointer-events: none; - opacity: 1; - transition: opacity 0.2s ease; - } } /* --- Hover Visibility Logic --- */ @@ -184,10 +261,6 @@ mc-diorama { pointer-events: auto; } - & .spacetime-indicator { - opacity: 0; - } - & .visualizer-ui { background: rgba(20, 20, 20, 0.85); backdrop-filter: blur(4px); diff --git a/src/controller.js b/src/controller.js index 1f759d9b..773a755b 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,8 +15,7 @@ export class SpacetimeController { subticks: true, autoplay: false, loop: false, - speed: 0.5, - ...options + speed: 0.5, ...options }; this.hasSpatial = this.opts.orbit || this.opts.pan || this.opts.zoom; @@ -50,77 +49,79 @@ export class SpacetimeController { this.overlay = document.createElement('div'); this.overlay.className = 'spacetime-overlay'; - // --- Feature Indicator (Bottom Left) --- - const activeFeatures = []; - // 't' is removed; the permanent Play button is now the timeline indicator! - if (this.opts.pan) activeFeatures.push('p'); - if (this.opts.zoom) activeFeatures.push('z'); - if (this.opts.orbit) activeFeatures.push('o'); + // --- Top Row Wrapper --- + const topRow = document.createElement('div'); + topRow.className = 'top-row'; + // --- Feature Indicator (Top Left) --- this.indicator = document.createElement('div'); this.indicator.className = 'spacetime-indicator'; - this.indicator.innerHTML = activeFeatures.map(f => `${f}`).join(''); - this.overlay.appendChild(this.indicator); - // --- Top: Reset Button --- + // Inject individual SVG icons with structural tooltips + if (this.opts.orbit) { + this.indicator.innerHTML += ``; + } + if (this.opts.pan) { + this.indicator.innerHTML += ``; + } + if (this.opts.zoom) { + this.indicator.innerHTML += ``; + } + topRow.appendChild(this.indicator); + + // --- Reset Button (Top Right) --- if (this.hasSpatial) { - const topWrapper = document.createElement('div'); - topWrapper.className = 'reset-wrapper'; this.resetBtn = document.createElement('div'); - this.resetBtn.className = 'btn'; - this.resetBtn.textContent = 'Reset View'; + this.resetBtn.className = 'btn spacetime-hover-reveal'; + this.resetBtn.innerHTML = ``; this.resetBtn.addEventListener('click', () => { this.diorama.loadState(); this.resetBtn.style.visibility = 'hidden'; }); - topWrapper.appendChild(this.resetBtn); - this.overlay.appendChild(topWrapper); + topRow.appendChild(this.resetBtn); } + this.overlay.appendChild(topRow); + // --- Bottom: Timeline Controls --- if (this.opts.timeline) { this.uiContainer = document.createElement('div'); - // Removed spacetime-hover-reveal from the wrapper this.uiContainer.className = 'visualizer-ui'; if (this.opts.subticks) { this.subRow = document.createElement('div'); - // The entire subtick row hides on idle this.subRow.className = 'scrubber-row spacetime-hover-reveal'; this.subRow.innerHTML = ` -
S>
-
<
-
+
+
+
-
-
>
+
`; this.uiContainer.appendChild(this.subRow); } this.tickRow = document.createElement('div'); this.tickRow.className = 'scrubber-row'; - // Only the play button remains fully visible and clickable this.tickRow.innerHTML = ` -
R>
-
Loop
-
<<
-
+
+
+
+
-
-
>>
+
`; this.uiContainer.appendChild(this.tickRow); this.overlay.appendChild(this.uiContainer); @@ -195,13 +196,10 @@ export class SpacetimeController { this.dragButton = e.button; if (this.dragButton === 0) { - if (e.ctrlKey || e.metaKey) this.dragButton = 2; - else if (e.shiftKey) this.dragButton = 1; + if (e.ctrlKey || e.metaKey) this.dragButton = 2; else if (e.shiftKey) this.dragButton = 1; } - if (this.dragButton === 1) this.canvas.style.cursor = 'move'; - else if (this.dragButton === 2) this.canvas.style.cursor = 'ns-resize'; - else this.canvas.style.cursor = 'grabbing'; + if (this.dragButton === 1) this.canvas.style.cursor = 'move'; else if (this.dragButton === 2) this.canvas.style.cursor = 'ns-resize'; else this.canvas.style.cursor = 'grabbing'; }); window.addEventListener('mouseup', () => { @@ -218,9 +216,7 @@ export class SpacetimeController { if (Math.hypot(dx, dy) > 3) { this.lastMouse = {x: e.clientX, y: e.clientY}; - if (this.dragButton === 0) this.orbit(dx, dy); - else if (this.dragButton === 1) this.pan(dx, dy); - else if (this.dragButton === 2) this.zoomLinear(dy); + if (this.dragButton === 0) this.orbit(dx, dy); else if (this.dragButton === 1) this.pan(dx, dy); else if (this.dragButton === 2) this.zoomLinear(dy); notifyCameraChange(); } }); @@ -300,8 +296,7 @@ export class SpacetimeController { }; getEl('btn-play').onclick = () => { - if (this.playMode === 'realtime') this.playMode = 'paused'; - else { + if (this.playMode === 'realtime') this.playMode = 'paused'; else { if (this.frame.currentTime >= this.getMaxTime() || this.frame.currentTime < 0) this.frame.seek(0); this.playMode = 'realtime'; } @@ -378,20 +373,37 @@ export class SpacetimeController { if (!this.opts.timeline) return; const getEl = id => this.uiContainer.querySelector('#' + id); - getEl('btn-play').textContent = this.playMode === 'realtime' ? '||' : 'R>'; + 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) { - getEl('btn-play-sub').textContent = this.playMode === 'subtick' ? '||' : 'S>'; + 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'; } const maxTime = this.getMaxTime(); const displayTime = Math.max(0, this.frame.currentTime); const tickPct = maxTime > 0 ? (displayTime / maxTime) * 100 : 0; - getEl('fill-tick').style.width = `calc(${tickPct}% + 10px)`; + + getEl('fill-tick').style.width = `${tickPct}%`; getEl('handle-tick').style.left = `${tickPct}%`; if (!this.opts.subticks) return; @@ -399,8 +411,7 @@ export class SpacetimeController { const events = this.frame.world.events; let activeTickTime = -Infinity; for (const ev of events) { - if (ev.time <= this.frame.currentTime) activeTickTime = ev.time; - else break; + if (ev.time <= this.frame.currentTime) activeTickTime = ev.time; else break; } let startIdx = 0, count = 0; @@ -424,19 +435,18 @@ export class SpacetimeController { 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'); + 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 = `calc(${subPct}% + 10px)`; + getEl('fill-sub').style.width = `${subPct}%`; getEl('handle-sub').style.left = `${subPct}%`; getEl('handle-sub').style.display = 'block'; } else { - getEl('fill-sub').style.width = `0px`; + getEl('fill-sub').style.width = `0%`; getEl('handle-sub').style.display = 'none'; } } @@ -468,8 +478,7 @@ export class SpacetimeController { const nextIdx = this.frame.currentIndex + steps; if (nextIdx > this.frame.world.events.length) { - if (this.isLooping) this.frame.seekIndex(this.frame.world.initialIndex); - else { + if (this.isLooping) this.frame.seekIndex(this.frame.world.initialIndex); else { this.frame.seekIndex(this.frame.world.events.length); this.playMode = 'paused'; } diff --git a/src/ico-auto-events.svg b/src/ico-auto-events.svg new file mode 100644 index 00000000..b290c85e --- /dev/null +++ b/src/ico-auto-events.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/ico-next-event.svg b/src/ico-next-event.svg new file mode 100644 index 00000000..d49ea75f --- /dev/null +++ b/src/ico-next-event.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ico-next.svg b/src/ico-next.svg new file mode 100644 index 00000000..563a4aa8 --- /dev/null +++ b/src/ico-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-pan.svg b/src/ico-pan.svg new file mode 100644 index 00000000..1a5739be --- /dev/null +++ b/src/ico-pan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-pause.svg b/src/ico-pause.svg new file mode 100644 index 00000000..240fd3b9 --- /dev/null +++ b/src/ico-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-play.svg b/src/ico-play.svg new file mode 100644 index 00000000..cfc5adca --- /dev/null +++ b/src/ico-play.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/ico-prev-event.svg b/src/ico-prev-event.svg new file mode 100644 index 00000000..8ba0c3b3 --- /dev/null +++ b/src/ico-prev-event.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ico-prev.svg b/src/ico-prev.svg new file mode 100644 index 00000000..80097fef --- /dev/null +++ b/src/ico-prev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-reset.svg b/src/ico-reset.svg new file mode 100644 index 00000000..6e9b6023 --- /dev/null +++ b/src/ico-reset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-rotate.svg b/src/ico-rotate.svg new file mode 100644 index 00000000..beed861d --- /dev/null +++ b/src/ico-rotate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ico-zoom.svg b/src/ico-zoom.svg new file mode 100644 index 00000000..7c411729 --- /dev/null +++ b/src/ico-zoom.svg @@ -0,0 +1 @@ + \ No newline at end of file