extract css

This commit is contained in:
David Allemang
2026-07-07 10:38:10 -04:00
parent 3360edae43
commit 668e1bcf69
7 changed files with 469 additions and 307 deletions

View File

@@ -3,229 +3,128 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Pipeline Test</title> <title>Pipeline Test</title>
<style>
body {
background: #222;
color: #eee;
font-family: sans-serif;
}
main { <link rel="stylesheet" href="style.css">
width: 80ch; <link rel="stylesheet" href="./src/components.css">
margin-inline: auto;
}
.figure { <script type="module" src="./src/components.js"></script>
position: relative;
width: 100%;
height: 20em;
}
.figure > canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head> </head>
<body> <body>
<main> <main>
<h1>Visualizer</h1> <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 class="grid-2col">
<div style="display: grid; grid-template-columns: 1fr 1fr;"> <mc-diorama frameid="nonsense" theta="180" phi="90"></mc-diorama>
<div class="figure" id="demo-top-static"></div> <mc-diorama frameid="nonsense">
<div class="figure" id="demo-iso"></div> <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>
<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> </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> </body>
</html> </html>

View File

@@ -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
View 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
View 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);

View File

@@ -1,37 +1,5 @@
// controller.js // 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 { export class SpacetimeController {
constructor(diorama, options = {}) { constructor(diorama, options = {}) {
this.diorama = diorama; this.diorama = diorama;
@@ -66,18 +34,8 @@ export class SpacetimeController {
this.lastFrameTime = performance.now(); this.lastFrameTime = performance.now();
this.playbackAccumulator = 0; this.playbackAccumulator = 0;
if (getComputedStyle(this.container).position === 'static') {
this.container.style.position = 'relative';
}
this.container.classList.add('spacetime-container'); 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.buildUI();
this.attachEvents(); this.attachEvents();
@@ -210,7 +168,10 @@ export class SpacetimeController {
} }
attachEvents() { 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 // Correctly set or remove the grab cursor
this.canvas.style.cursor = this.hasSpatial ? 'grab' : 'default'; this.canvas.style.cursor = this.hasSpatial ? 'grab' : 'default';
@@ -262,7 +223,7 @@ export class SpacetimeController {
}); });
this.canvas.addEventListener('click', () => { 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. // ensuring static scenes still act as tap-to-play toggles.
if (!this.hasDragged && this.opts.timeline) { if (!this.hasDragged && this.opts.timeline) {
this.playMode = this.playMode === 'paused' ? 'realtime' : 'paused'; this.playMode = this.playMode === 'paused' ? 'realtime' : 'paused';

View File

@@ -1,15 +1,15 @@
// diorama.js
import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js'; import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
export class Diorama { export class Diorama {
constructor(elementId, frame, requestRenderCallback) { constructor(element, frame, requestRenderCallback) {
this.element = document.getElementById(elementId); this.element = element;
this.element.style.position = 'relative';
this.canvas = document.createElement('canvas'); this.canvas = document.createElement('canvas');
this.element.appendChild(this.canvas); this.element.appendChild(this.canvas);
this.ctx2d = this.canvas.getContext('2d'); this.ctx2d = this.canvas.getContext('2d');
this.frame = frame; this.frame = frame;
this.dirty = true; // Start dirty to guarantee the first render this.dirty = true; // Start dirty to guarantee the first render
this.requestEngineRender = requestRenderCallback; this.requestEngineRender = requestRenderCallback;

22
style.css Normal file
View File

@@ -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;
}