player improvements
This commit is contained in:
@@ -173,7 +173,7 @@ mc-diorama {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
color: white;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
@@ -181,12 +181,8 @@ mc-diorama {
|
||||
|
||||
& .scrubber-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
& .btn {
|
||||
min-width: 4ch;
|
||||
}
|
||||
}
|
||||
|
||||
& .track-container {
|
||||
@@ -260,12 +256,7 @@ mc-diorama {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
& .visualizer-ui {
|
||||
background: rgba(20, 20, 20, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export class SpacetimeController {
|
||||
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');
|
||||
@@ -232,10 +233,10 @@ export class SpacetimeController {
|
||||
}
|
||||
|
||||
// --- TEMPORAL CONTROL ---
|
||||
|
||||
getMaxTime() {
|
||||
const evs = this.frame.world.events;
|
||||
return evs.length > 0 ? evs[evs.length - 1].time : 0;
|
||||
// Add +1 so the final tick plays out its full duration!
|
||||
return evs.length > 0 ? evs[evs.length - 1].time + 1 : 0;
|
||||
}
|
||||
|
||||
generateTickMarkers() {
|
||||
@@ -296,8 +297,15 @@ export class SpacetimeController {
|
||||
};
|
||||
|
||||
getEl('btn-play').onclick = () => {
|
||||
if (this.playMode === 'realtime') this.playMode = 'paused'; else {
|
||||
if (this.frame.currentTime >= this.getMaxTime() || this.frame.currentTime < 0) this.frame.seek(0);
|
||||
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();
|
||||
@@ -305,65 +313,85 @@ export class SpacetimeController {
|
||||
|
||||
getEl('btn-prev').onclick = () => {
|
||||
this.playMode = 'paused';
|
||||
let activeTime = -Infinity;
|
||||
for (const ev of this.frame.world.events) if (ev.time <= this.frame.currentTime) activeTime = ev.time;
|
||||
let prevTime = Math.floor(this.uiTickTime - 1);
|
||||
|
||||
let prevTime = -1;
|
||||
for (let i = this.frame.world.events.length - 1; i >= 0; i--) {
|
||||
if (this.frame.world.events[i].time < activeTime) {
|
||||
prevTime = this.frame.world.events[i].time;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 = this.getMaxTime() + 1;
|
||||
for (const ev of this.frame.world.events) {
|
||||
if (ev.time > this.frame.currentTime) {
|
||||
nextTime = ev.time;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
this.frame.seek(Math.round(pct * this.getMaxTime()));
|
||||
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 = () => {
|
||||
this.playMode = this.playMode === 'subtick' ? 'paused' : 'subtick';
|
||||
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;
|
||||
if (idx < this.frame.world.initialIndex) idx = this.isLooping ? this.frame.world.events.length : this.frame.world.initialIndex;
|
||||
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;
|
||||
if (idx > this.frame.world.events.length) idx = this.isLooping ? this.frame.world.initialIndex : this.frame.world.events.length;
|
||||
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;
|
||||
this.frame.seekIndex(this.currentTickStartIndex + Math.round(pct * this.currentTickEventCount));
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -399,8 +427,19 @@ export class SpacetimeController {
|
||||
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();
|
||||
const displayTime = Math.max(0, this.frame.currentTime);
|
||||
|
||||
// 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}%`;
|
||||
@@ -409,16 +448,38 @@ export class SpacetimeController {
|
||||
if (!this.opts.subticks) return;
|
||||
|
||||
const events = this.frame.world.events;
|
||||
let activeTickTime = -Infinity;
|
||||
for (const ev of events) {
|
||||
if (ev.time <= this.frame.currentTime) activeTickTime = ev.time; else break;
|
||||
if (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++;
|
||||
}
|
||||
}
|
||||
|
||||
let startIdx = 0, count = 0;
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
if (events[i].time === activeTickTime) {
|
||||
if (count === 0) 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,35 +516,40 @@ export class SpacetimeController {
|
||||
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) {
|
||||
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';
|
||||
}
|
||||
} else {
|
||||
this.frame.seek(nextTime);
|
||||
}
|
||||
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 > this.frame.world.events.length) {
|
||||
if (this.isLooping) this.frame.seekIndex(this.frame.world.initialIndex); else {
|
||||
this.frame.seekIndex(this.frame.world.events.length);
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user