remove busted timeline control
@@ -1,6 +1,5 @@
|
||||
#import "/lib.typ": diorama, example, note, todo
|
||||
|
||||
|
||||
= Tilesets <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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// cache.js
|
||||
|
||||
export class Cache {
|
||||
constructor(root, onResolve) {
|
||||
this.root = root;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// components.js
|
||||
import {Engine} from './engine.js';
|
||||
import {World, WorldFrame} from './world.js';
|
||||
import {Diorama} from './diorama.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 += `<span class="icon ico-rotate" title="Rotate view: left drag"></span>`;
|
||||
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 += `<span class="icon ico-pan" title="Move view: middle drag (or shift left drag)"></span>`;
|
||||
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 += `<span class="icon ico-zoom" title="Zoom view: right drag (or ctrl left drag)"></span>`;
|
||||
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 = `<span class="icon ico-reset" title="Reset camera"></span>`;
|
||||
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 = `
|
||||
<div class="btn" id="btn-play-sub" title="Animate block updates"><span class="icon ico-auto-events"></span></div>
|
||||
<div class="btn" id="btn-prev-sub" title="Previous block update"><span class="icon ico-next-event" style="transform: scaleX(-1);"></span></div>
|
||||
<div class="track-container" id="track-sub-container" title="Select block update (in current tick)">
|
||||
<div class="track-inner" id="track-sub-inner">
|
||||
<div class="track-fill" id="fill-sub"></div>
|
||||
<div id="markers-sub"></div>
|
||||
<div class="track-handle" id="handle-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn" id="btn-next-sub" title="Next block update"><span class="icon ico-next-event"></span></div>
|
||||
`;
|
||||
this.uiContainer.appendChild(this.subRow);
|
||||
}
|
||||
|
||||
this.tickRow = document.createElement('div');
|
||||
this.tickRow.className = 'scrubber-row';
|
||||
this.tickRow.innerHTML = `
|
||||
<div class="btn" id="btn-play" title="Animate gameticks" style="pointer-events: auto;"><span class="icon ico-play"></span></div>
|
||||
<div class="btn spacetime-hover-reveal" id="btn-loop" title="Repeat playback"><span class="icon ico-rotate"></span></div>
|
||||
<div class="btn spacetime-hover-reveal" id="btn-prev" title="Previous gametick"><span class="icon ico-next" style="transform: scaleX(-1);"></span></div>
|
||||
<div class="track-container spacetime-hover-reveal" id="track-tick-container" title="Select gametick">
|
||||
<div class="track-inner" id="track-tick-inner">
|
||||
<div class="track-fill" id="fill-tick"></div>
|
||||
<div id="markers-tick"></div>
|
||||
<div class="track-handle" id="handle-tick"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn spacetime-hover-reveal" id="btn-next" title="Next gametick"><span class="icon ico-next"></span></div>
|
||||
`;
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
|
||||
<path
|
||||
d="M500.7 138.7L512 149.4L512 96C512 78.3 526.3 64 544 64C561.7 64 576 78.3 576 96L576 224C576 241.7 561.7 256 544 256L416 256C398.3 256 384 241.7 384 224C384 206.3 398.3 192 416 192L463.9 192L456.3 184.8C456.1 184.6 455.9 184.4 455.7 184.2C380.7 109.2 259.2 109.2 184.2 184.2C109.2 259.2 109.2 380.7 184.2 455.7C259.2 530.7 380.7 530.7 455.7 455.7C463.9 447.5 471.2 438.8 477.6 429.6C487.7 415.1 507.7 411.6 522.2 421.7C536.7 431.8 540.2 451.8 530.1 466.3C521.6 478.5 511.9 490.1 501 501C401 601 238.9 601 139 501C39.1 401 39 239 139 139C238.9 39.1 400.7 39 500.7 138.7z"/>
|
||||
<path
|
||||
transform="scale(0.5) translate(320, 320)"
|
||||
d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
|
||||
|
||||
<rect x="0" y="0" width="640" height="640" fill="none" stroke="red"></rect>
|
||||
|
||||
<path transform="translate(120, 0)"
|
||||
d="M441.3 299.8C451.5 312.4 450.8 330.9 439.1 342.6L311.1 470.6C301.9 479.8 288.2 482.5 276.2 477.5C264.2 472.5 256.5 460.9 256.5 448L256.5 192C256.5 179.1 264.3 167.4 276.3 162.4C288.3 157.4 302 160.2 311.2 169.3L439.2 297.3L441.4 299.7z"/>
|
||||
|
||||
<path
|
||||
transform="scale(0.5) translate(120, 320)"
|
||||
d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M441.3 299.8C451.5 312.4 450.8 330.9 439.1 342.6L311.1 470.6C301.9 479.8 288.2 482.5 276.2 477.5C264.2 472.5 256.5 460.9 256.5 448L256.5 192C256.5 179.1 264.3 167.4 276.3 162.4C288.3 157.4 302 160.2 311.2 169.3L439.2 297.3L441.4 299.7z"/></svg>
|
||||
|
Before Width: | Height: | Size: 467 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>
|
||||
|
Before Width: | Height: | Size: 528 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
|
||||
<path
|
||||
d="M187.2 100.9C174.8 94.1 159.8 94.4 147.6 101.6C135.4 108.8 128 121.9 128 136L128 504C128 518.1 135.5 531.2 147.6 538.4C159.7 545.6 174.8 545.9 187.2 539.1L523.2 355.1C536 348.1 544 334.6 544 320C544 305.4 536 291.9 523.2 284.9L187.2 100.9z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 486 B |
@@ -1,17 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
|
||||
|
||||
<rect x="0" y="0" width="640" height="640" fill="none" stroke="red"></rect>
|
||||
|
||||
<path
|
||||
transform="translate(-120, 0)"
|
||||
d="M199.7 299.8C189.4 312.4 190.2 330.9 201.9 342.6L329.9 470.6C339.1 479.8 352.8 482.5 364.8 477.5C376.8 472.5 384.6 460.9 384.6 447.9L384.6 191.9C384.6 179 376.8 167.3 364.8 162.3C352.8 157.3 339.1 160.1 329.9 169.2L201.9 297.2L199.7 299.6z"/>
|
||||
|
||||
|
||||
<!-- <path transform="translate(120, 0)"-->
|
||||
<!-- d="M441.3 299.8C451.5 312.4 450.8 330.9 439.1 342.6L311.1 470.6C301.9 479.8 288.2 482.5 276.2 477.5C264.2 472.5 256.5 460.9 256.5 448L256.5 192C256.5 179.1 264.3 167.4 276.3 162.4C288.3 157.4 302 160.2 311.2 169.3L439.2 297.3L441.4 299.7z"/>-->
|
||||
|
||||
<path
|
||||
transform="scale(0.5) translate(520, 320)"
|
||||
d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M199.7 299.8C189.4 312.4 190.2 330.9 201.9 342.6L329.9 470.6C339.1 479.8 352.8 482.5 364.8 477.5C376.8 472.5 384.6 460.9 384.6 447.9L384.6 191.9C384.6 179 376.8 167.3 364.8 162.3C352.8 157.3 339.1 160.1 329.9 169.2L201.9 297.2L199.7 299.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 471 B |
@@ -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();
|
||||
|
||||