extract css
This commit is contained in:
323
index.html
323
index.html
@@ -3,229 +3,128 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Pipeline Test</title>
|
||||
<style>
|
||||
body {
|
||||
background: #222;
|
||||
color: #eee;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 80ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="./src/components.css">
|
||||
|
||||
.figure {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20em;
|
||||
}
|
||||
|
||||
.figure > canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./src/components.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Visualizer</h1>
|
||||
|
||||
<h2>Dynamic</h2>
|
||||
<h2>Dynamic Pulses</h2>
|
||||
<mc-diorama radius="4">
|
||||
<mc-timeline loop autoplay controls="false"></mc-timeline>
|
||||
<mc-world>
|
||||
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
|
||||
</mc-world>
|
||||
</mc-diorama>
|
||||
|
||||
<div class="figure" id="demo-pulses"></div>
|
||||
<h2>Static multi-view scene</h2>
|
||||
|
||||
<div class="figure" id="demo-piston"></div>
|
||||
<mc-frame id="nonsense">
|
||||
<mc-camera orbit></mc-camera>
|
||||
<mc-world>
|
||||
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
|
||||
</mc-world>
|
||||
</mc-frame>
|
||||
|
||||
<h2>Static Sub-Figures</h2>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr;">
|
||||
<div class="figure" id="demo-top-static"></div>
|
||||
<div class="figure" id="demo-iso"></div>
|
||||
<div class="grid-2col">
|
||||
<mc-diorama frameid="nonsense" theta="180" phi="90"></mc-diorama>
|
||||
<mc-diorama frameid="nonsense">
|
||||
<mc-camera orbit></mc-camera>
|
||||
</mc-diorama>
|
||||
<mc-diorama frameid="nonsense" theta="180" phi="0"></mc-diorama>
|
||||
<mc-diorama frameid="nonsense" theta="270" phi="0"></mc-diorama>
|
||||
</div>
|
||||
|
||||
<div id="atlas-container" style="position: absolute; right: 0; top: 0"></div>
|
||||
<h2>Dynamic Timeline Control</h2>
|
||||
<mc-diorama>
|
||||
<mc-camera orbit></mc-camera>
|
||||
<mc-timeline loop subticks></mc-timeline>
|
||||
<mc-world>
|
||||
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
|
||||
</mc-world>
|
||||
</mc-diorama>
|
||||
|
||||
<div id="atlas-container"></div>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import {Engine} from './src/engine.js';
|
||||
import {Diorama} from './src/diorama.js';
|
||||
import {World, WorldFrame} from './src/world.js';
|
||||
import {SpacetimeController} from "./src/controller.js";
|
||||
|
||||
async function initDocument() {
|
||||
try {
|
||||
const engine = new Engine();
|
||||
document.getElementById('atlas-container')?.appendChild(engine.atlas.canvas);
|
||||
|
||||
const w_static = new World(`
|
||||
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
|
||||
`)
|
||||
const f_static = new WorldFrame(w_static);
|
||||
const d_static_top = new Diorama('demo-top-static', f_static, () => engine.requestRender());
|
||||
d_static_top.radius = 5;
|
||||
d_static_top.theta = 180;
|
||||
d_static_top.phi = 90;
|
||||
d_static_top.centerView();
|
||||
d_static_top.saveState();
|
||||
engine.addDiorama(d_static_top)
|
||||
const d_static_iso = new Diorama('demo-iso', f_static, () => engine.requestRender());
|
||||
d_static_iso.radius = 5;
|
||||
d_static_iso.centerView();
|
||||
d_static_iso.saveState();
|
||||
engine.addDiorama(d_static_iso);
|
||||
const c_static_iso = new SpacetimeController(d_static_iso, {
|
||||
timeline: false,
|
||||
subticks: false,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
zoom: false,
|
||||
pan: false,
|
||||
});
|
||||
|
||||
const w_pulses = new World(`
|
||||
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
|
||||
`);
|
||||
const f_pulses = new WorldFrame(w_pulses);
|
||||
const d_pulses = new Diorama('demo-pulses', f_pulses, () => engine.requestRender());
|
||||
d_pulses.radius = 4;
|
||||
d_pulses.centerView()
|
||||
d_pulses.saveState()
|
||||
engine.addDiorama(d_pulses);
|
||||
const c_pulses = new SpacetimeController(d_pulses, {
|
||||
timeline: false,
|
||||
subticks: false,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
pan: false,
|
||||
zoom: false,
|
||||
orbit: false,
|
||||
})
|
||||
|
||||
const w_piston = new World(`
|
||||
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
|
||||
`);
|
||||
const f_piston_t = new WorldFrame(w_piston);
|
||||
const d_piston_t = new Diorama('demo-piston', f_piston_t, () => engine.requestRender());
|
||||
d_piston_t.radius = 4;
|
||||
d_piston_t.centerView();
|
||||
d_piston_t.saveState();
|
||||
engine.addDiorama(d_piston_t);
|
||||
const c_piston = new SpacetimeController(d_piston_t, {
|
||||
loop: true,
|
||||
})
|
||||
|
||||
// Fetch and build everything concurrently!
|
||||
await engine.updateAll();
|
||||
engine.requestRender();
|
||||
|
||||
|
||||
// const factor = 2.5;
|
||||
// const update_time = () => {
|
||||
// for (let i = 0; i < 20; i++) {
|
||||
// setTimeout(() => f_piston_t.seek(i), i * factor * 50)
|
||||
// }
|
||||
// };
|
||||
// setInterval(update_time, factor * 1000)
|
||||
// update_time();
|
||||
//
|
||||
// let i = 0;
|
||||
// let n = w_piston.eventCount;
|
||||
// const update_state = () => {
|
||||
// f_piston_s.seekIndex(i);
|
||||
// i = (i + 1) % n;
|
||||
// };
|
||||
// setInterval(update_state, 250);
|
||||
// update_state()
|
||||
//
|
||||
// const update_pulses = () => {
|
||||
// for (let i = 0; i < 8; i++) {
|
||||
// setTimeout(() => f_pulses.seek(i), i * factor * 50)
|
||||
// }
|
||||
// };
|
||||
// setInterval(update_pulses, factor * 8 * 50)
|
||||
// update_pulses();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Renderer Initialization Failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
initDocument();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
194
src/components.css
Normal file
194
src/components.css
Normal file
@@ -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;
|
||||
}
|
||||
133
src/components.js
Normal file
133
src/components.js
Normal file
@@ -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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user