diff --git a/index.html b/index.html index 1027f91c..79832c50 100644 --- a/index.html +++ b/index.html @@ -3,229 +3,128 @@ Pipeline Test - +

Visualizer

-

Dynamic

+

Dynamic Pulses

+ + + + p 0 0 0 observer facing=south powered=false + p 0 0 1 observer facing=east powered=false + p 1 0 1 observer facing=north powered=false + p 1 0 0 observer facing=west powered=false + t 0 + p 0 0 1 powered=false + p 0 0 0 powered=true + t 2 + p 0 0 0 powered=false + p 1 0 0 powered=true + t 4 + p 1 0 0 powered=false + p 1 0 1 powered=true + t 6 + p 1 0 1 powered=false + p 0 0 1 powered=true + t 8 + p 0 0 0 + + -
+

Static multi-view scene

-
+ + + + p -1 0 0 redstone_wire east=side west=none north=none south=none power=0 + 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 + p 0 -2 0 piston facing=north extended=false + + -

Static Sub-Figures

-
-
-
+
+ + + + + +
-
+

Dynamic Timeline Control

+ + + + + p 0 -1 0 smooth_stone_slab type=top + p 1 -1 0 smooth_stone_slab type=top + p 0 0 0 redstone_wire power=0 east=side west=none north=none south=none + p 1 0 0 repeater facing=west powered=false locked=false delay=2 + p 2 0 0 sticky_piston facing=east extended=false + p 2 -1 0 piston facing=east extended=false + p 3 0 0 slime_block + p 4 0 0 slime_block + t 2 + p 0 0 0 power=15 + t 6 + p 1 0 0 powered=true + l begin-extend + t 7 + p 2 0 0 extended=true + p 4.0 0 0 air & + p 4.5 0 0 slime_block + p 3.0 0 0 air & + p 3.5 0 0 slime_block + p 2.5 0 0 piston_head facing=east short=true type=sticky + p 2 -1 0 extended=true + p 2.5 -1 0 piston_head facing=east short=true type=normal + t 8 + p 4.5 0 0 air & + p 5.0 0 0 slime_block + p 3.5 0 0 air & + p 4.0 0 0 slime_block + p 2.5 0 0 air & + p 3.0 0 0 piston_head facing=east short=false type=sticky + p 2.5 -1 0 air & + p 3.0 -1 0 piston_head facing=east short=false type=normal + l end-extend + t 12 + p 0 0 0 power=0 + t 16 + p 1 0 0 powered=false + l begin-retract + t 17 + p 3 0 0 air & + p 2.5 0 0 piston_head facing=east short=true type=sticky + p 4.0 0 0 air & + p 3.5 0 0 slime_block + p 5.0 0 0 air & + p 4.5 0 0 slime_block + p 3.0 -1 0 air & + p 2.5 -1 0 piston_head facing=east short=true type=normal + t 18 + p 2.5 0 0 air + p 3.5 0 0 air & + p 3.0 0 0 slime_block + p 4.5 0 0 air & + p 4.0 0 0 slime_block + p 2 0 0 extended=false + p 2.5 -1 0 air + p 2 -1 0 extended=false + l end-retract + + + +
- - - + \ No newline at end of file diff --git a/src/autoplayer.js b/src/autoplayer.js deleted file mode 100644 index d147947f..00000000 --- a/src/autoplayer.js +++ /dev/null @@ -1,47 +0,0 @@ -// autoplayer.js - -export class AutoPlayer { - constructor(diorama, speed = 1.0) { - this.frame = diorama.frame; - this.speed = speed; - - this.lastFrameTime = performance.now(); - this.playheadTime = 0; - this.isRunning = true; - - this._loop = this.loop.bind(this); - requestAnimationFrame(this._loop); - } - - getMaxTime() { - const evs = this.frame.world.events; - return evs.length > 0 ? evs[evs.length - 1].time : 0; - } - - loop(time) { - if (!this.isRunning) return; - - const dt = (time - this.lastFrameTime) / 1000; - this.lastFrameTime = time; - const maxTime = this.getMaxTime(); - - // Only animate if there are actually forward events in the timeline - if (maxTime > 0) { - this.playheadTime += dt * 20 * this.speed; - - // Loop wrap-around - if (this.playheadTime >= maxTime) { - this.playheadTime = this.playheadTime % maxTime; - } - - this.frame.seek(this.playheadTime); - } - - requestAnimationFrame(this._loop); - } - - // Call this if you ever need to destroy the diorama and stop the memory leak - stop() { - this.isRunning = false; - } -} \ No newline at end of file diff --git a/src/components.css b/src/components.css new file mode 100644 index 00000000..276a1c42 --- /dev/null +++ b/src/components.css @@ -0,0 +1,194 @@ +/* --- Component structural styles --- */ +mc-world, mc-timeline, mc-camera, mc-frame { + display: none; +} + +mc-diorama { + display: block; + position: relative; + width: 100%; + height: 20em; +} + +mc-diorama canvas { + display: block; + width: 100%; + height: 100%; +} + +/* --- UI Controls styles --- */ +.spacetime-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + display: flex; + flex-direction: column; + justify-content: space-between; + box-sizing: border-box; + padding: 12px; + z-index: 10; +} + +.reset-wrapper { + display: flex; + justify-content: flex-end; + width: 100%; +} + +.reset-btn { + pointer-events: auto; + padding: 6px 12px; + background: rgba(0, 0, 0, 0.6); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + cursor: pointer; + display: none; + font-family: sans-serif; + border-radius: 4px; + font-weight: bold; + backdrop-filter: blur(4px); +} + +.reset-btn:hover { + background: rgba(0, 0, 0, 0.8); +} + +.visualizer-ui { + pointer-events: auto; + font-family: sans-serif; + background: rgba(20, 20, 20, 0.85); + padding: 12px; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 12px; + color: white; + user-select: none; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.scrubber-row { + display: flex; + gap: 12px; + align-items: center; +} + +.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; + min-width: 4ch; + text-align: center; + font-weight: bold; + transition: background 0.1s; +} + +.btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.track-container { + flex-grow: 1; + height: 24px; + padding: 0 10px; + cursor: pointer; + display: flex; + align-items: center; +} + +.track-inner { + position: relative; + width: 100%; + height: 100%; + 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; + background: #f00; + width: 10px; + border-radius: 4px; +} + +.track-handle { + position: absolute; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + top: 12px; + transform: translate(-50%, -50%); + z-index: 3; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +} + +.marker { + position: absolute; + top: 8px; + height: 8px; + 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; +} + +/* --- Hover Visibility Logic --- */ +.spacetime-hover-reveal { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.spacetime-container:hover .spacetime-hover-reveal { + opacity: 1; + pointer-events: auto; +} + +.spacetime-indicator { + position: absolute; + left: 33px; + bottom: 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; +} + +.spacetime-container:hover .spacetime-indicator { + opacity: 0; +} \ No newline at end of file diff --git a/src/components.js b/src/components.js new file mode 100644 index 00000000..428b4532 --- /dev/null +++ b/src/components.js @@ -0,0 +1,133 @@ +// components.js +import {Engine} from './engine.js'; +import {World, WorldFrame} from './world.js'; +import {Diorama} from './diorama.js'; +import {SpacetimeController} from './controller.js'; + +// Global Engine Singleton +let engineInstance = null; + +function getEngine() { + if (!engineInstance) { + engineInstance = new Engine(); + document.body.appendChild(engineInstance.canvas) + } + return engineInstance; +} + +export class MCWorldElement extends HTMLElement { + connectedCallback() { + if (this.world) return; + // Read text content directly, allowing inline scripting + const script = this.textContent || ''; + this.world = new World(script); + } +} + +export class MCFrameElement extends HTMLElement { + connectedCallback() { + // Use a microtask to ensure children are parsed + setTimeout(() => this.init(), 0); + } + + init() { + if (this.frame) return; + + let world; + const worldId = this.getAttribute('worldid'); + + if (worldId) { + world = document.getElementById(worldId)?.world; + } else { + const worldEl = this.querySelector('mc-world'); + world = worldEl?.world; + } + + if (world) { + this.frame = new WorldFrame(world); + } + } +} + +export class MCDioramaElement extends HTMLElement { + connectedCallback() { + setTimeout(() => this.init(), 0); + } + + init() { + const engine = getEngine(); + let frame; + + // 1. Resolve the Frame context + const frameId = this.getAttribute('frameid'); + const worldId = this.getAttribute('worldid'); + + if (frameId) { + frame = document.getElementById(frameId)?.frame; + } else if (worldId) { + const world = document.getElementById(worldId)?.world; + if (world) frame = new WorldFrame(world); + } else { + const frameEl = this.querySelector('mc-frame'); + if (frameEl) { + frame = frameEl.frame; + } else { + const worldEl = this.querySelector('mc-world'); + if (worldEl) frame = new WorldFrame(worldEl.world); + } + } + + if (!frame) { + console.error("mc-diorama requires a resolved frame or world."); + return; + } + + // 2. Build the Diorama + const diorama = new Diorama(this, frame, () => engine.requestRender()); + + // Process spatial attributes + if (this.hasAttribute('radius')) diorama.radius = parseFloat(this.getAttribute('radius')); + if (this.hasAttribute('theta')) diorama.theta = parseFloat(this.getAttribute('theta')); + if (this.hasAttribute('phi')) diorama.phi = parseFloat(this.getAttribute('phi')); + + diorama.centerView(); + diorama.saveState(); // Lock this in for the Reset button + + // 3. Resolve the Controllers + const timelineEl = this.querySelector('mc-timeline'); + const cameraEl = this.querySelector('mc-camera'); + + // Default to fully static + const opts = { + timeline: !!timelineEl, + orbit: !!cameraEl, + pan: !!cameraEl, + zoom: !!cameraEl, + autoplay: false, + loop: false, + subticks: false, + }; + + if (timelineEl) { + if (timelineEl.hasAttribute('autoplay')) opts.autoplay = timelineEl.getAttribute('autoplay') !== 'false'; + if (timelineEl.hasAttribute('loop')) opts.loop = timelineEl.getAttribute('loop') !== 'false'; + if (timelineEl.hasAttribute('subticks')) opts.subticks = timelineEl.getAttribute('subticks') !== 'false'; + if (timelineEl.hasAttribute('controls')) opts.timeline = timelineEl.getAttribute('controls') !== 'false'; + if (timelineEl.hasAttribute('speed')) opts.speed = parseFloat(timelineEl.getAttribute('speed')); + } + + if (cameraEl) { + if (cameraEl.hasAttribute('orbit')) opts.orbit = cameraEl.getAttribute('orbit') !== 'false'; + if (cameraEl.hasAttribute('pan')) opts.pan = cameraEl.getAttribute('pan') !== 'false'; + if (cameraEl.hasAttribute('zoom')) opts.zoom = cameraEl.getAttribute('zoom') !== 'false'; + } + + this.controller = new SpacetimeController(diorama, opts); + engine.addDiorama(diorama); + } +} + +// Register Elements +customElements.define('mc-world', MCWorldElement); +customElements.define('mc-frame', MCFrameElement); +customElements.define('mc-diorama', MCDioramaElement); \ No newline at end of file diff --git a/src/controller.js b/src/controller.js index 2eda33f0..f35c883b 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,37 +1,5 @@ // controller.js -const UI_CSS = ` - .spacetime-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; box-sizing: border-box; padding: 12px; z-index: 10; } - - .reset-wrapper { display: flex; justify-content: flex-end; width: 100%; } - .reset-btn { pointer-events: auto; padding: 6px 12px; background: rgba(0,0,0,0.6); color: white; border: 1px solid rgba(255,255,255,0.3); cursor: pointer; display: none; font-family: sans-serif; border-radius: 4px; font-weight: bold; backdrop-filter: blur(4px); } - .reset-btn:hover { background: rgba(0,0,0,0.8); } - - .visualizer-ui { pointer-events: auto; font-family: sans-serif; background: rgba(20,20,20,0.85); padding: 12px; border-radius: 8px; display: flex; flex-direction: column; gap: 12px; color: white; user-select: none; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.5); } - .scrubber-row { display: flex; gap: 12px; align-items: center; } - - .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; min-width: 4ch; text-align: center; font-weight: bold; transition: background 0.1s; } - .btn:hover { background: rgba(255,255,255,0.2); } - - .track-container { flex-grow: 1; height: 24px; padding: 0 10px; cursor: pointer; display: flex; align-items: center; } - .track-inner { position: relative; width: 100%; height: 100%; 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; background: #f00; width: 10px; border-radius: 4px; } - .track-handle { position: absolute; width: 16px; height: 16px; background: #fff; border-radius: 50%; top: 12px; transform: translate(-50%, -50%); z-index: 3; box-shadow: 0 0 4px rgba(0,0,0,0.5); } - - .marker { position: absolute; top: 8px; height: 8px; 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; } - - /* --- Hover Visibility Logic --- */ - .spacetime-hover-reveal { opacity: 0; pointer-events: none; transition: opacity 0.2s ease; } - .spacetime-container:hover .spacetime-hover-reveal { opacity: 1; pointer-events: auto; } - - .spacetime-indicator { position: absolute; left: 33px; bottom: 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; } - .spacetime-container:hover .spacetime-indicator { opacity: 0; } -`; - export class SpacetimeController { constructor(diorama, options = {}) { this.diorama = diorama; @@ -66,18 +34,8 @@ export class SpacetimeController { this.lastFrameTime = performance.now(); this.playbackAccumulator = 0; - if (getComputedStyle(this.container).position === 'static') { - this.container.style.position = 'relative'; - } this.container.classList.add('spacetime-container'); - if (!document.getElementById('spacetime-ui-styles')) { - const style = document.createElement('style'); - style.id = 'spacetime-ui-styles'; - style.textContent = UI_CSS; - document.head.appendChild(style); - } - this.buildUI(); this.attachEvents(); @@ -210,7 +168,10 @@ export class SpacetimeController { } attachEvents() { - this.canvas.addEventListener('contextmenu', e => e.preventDefault()); + this.canvas.addEventListener('contextmenu', e => { + if (!this.hasSpatial) return; + e.preventDefault() + }); // Correctly set or remove the grab cursor this.canvas.style.cursor = this.hasSpatial ? 'grab' : 'default'; @@ -262,7 +223,7 @@ export class SpacetimeController { }); this.canvas.addEventListener('click', () => { - // hasDragged will always be false if hasSpatial is false, + // 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'; diff --git a/src/diorama.js b/src/diorama.js index a8d57460..9bcd62ac 100644 --- a/src/diorama.js +++ b/src/diorama.js @@ -1,15 +1,15 @@ +// diorama.js import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js'; export class Diorama { - constructor(elementId, frame, requestRenderCallback) { - this.element = document.getElementById(elementId); - this.element.style.position = 'relative'; + constructor(element, frame, requestRenderCallback) { + this.element = element; this.canvas = document.createElement('canvas'); this.element.appendChild(this.canvas); this.ctx2d = this.canvas.getContext('2d'); this.frame = frame; - + this.dirty = true; // Start dirty to guarantee the first render this.requestEngineRender = requestRenderCallback; diff --git a/style.css b/style.css new file mode 100644 index 00000000..5ef590f4 --- /dev/null +++ b/style.css @@ -0,0 +1,22 @@ +body { + background: #222; + color: #eee; + font-family: sans-serif; +} + +main { + width: 80ch; + margin-inline: auto; +} + +.grid-2col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +#atlas-container { + position: absolute; + right: 0; + top: 0; +} \ No newline at end of file