player improvements
This commit is contained in:
@@ -173,7 +173,7 @@ mc-diorama {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
color: white;
|
color: white;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -181,12 +181,8 @@ mc-diorama {
|
|||||||
|
|
||||||
& .scrubber-row {
|
& .scrubber-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& .btn {
|
|
||||||
min-width: 4ch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .track-container {
|
& .track-container {
|
||||||
@@ -260,12 +256,7 @@ mc-diorama {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .visualizer-ui {
|
& .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;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ export class SpacetimeController {
|
|||||||
this.isLooping = this.opts.loop;
|
this.isLooping = this.opts.loop;
|
||||||
this.speed = this.opts.speed;
|
this.speed = this.opts.speed;
|
||||||
this.lastFrameTime = performance.now();
|
this.lastFrameTime = performance.now();
|
||||||
|
this.uiTickTime = 0;
|
||||||
this.playbackAccumulator = 0;
|
this.playbackAccumulator = 0;
|
||||||
|
|
||||||
this.container.classList.add('spacetime-container');
|
this.container.classList.add('spacetime-container');
|
||||||
@@ -232,10 +233,10 @@ export class SpacetimeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- TEMPORAL CONTROL ---
|
// --- TEMPORAL CONTROL ---
|
||||||
|
|
||||||
getMaxTime() {
|
getMaxTime() {
|
||||||
const evs = this.frame.world.events;
|
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() {
|
generateTickMarkers() {
|
||||||
@@ -296,8 +297,15 @@ export class SpacetimeController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getEl('btn-play').onclick = () => {
|
getEl('btn-play').onclick = () => {
|
||||||
if (this.playMode === 'realtime') this.playMode = 'paused'; else {
|
if (this.playMode === 'realtime') this.playMode = 'paused';
|
||||||
if (this.frame.currentTime >= this.getMaxTime() || this.frame.currentTime < 0) this.frame.seek(0);
|
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.playMode = 'realtime';
|
||||||
}
|
}
|
||||||
this.syncUI();
|
this.syncUI();
|
||||||
@@ -305,65 +313,85 @@ export class SpacetimeController {
|
|||||||
|
|
||||||
getEl('btn-prev').onclick = () => {
|
getEl('btn-prev').onclick = () => {
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
let activeTime = -Infinity;
|
let prevTime = Math.floor(this.uiTickTime - 1);
|
||||||
for (const ev of this.frame.world.events) if (ev.time <= this.frame.currentTime) activeTime = ev.time;
|
|
||||||
|
|
||||||
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;
|
if (prevTime < 0) prevTime = this.isLooping ? this.getMaxTime() : 0;
|
||||||
|
|
||||||
this.frame.seek(prevTime);
|
this.frame.seek(prevTime);
|
||||||
|
this.uiTickTime = prevTime; // Explicit jump
|
||||||
this.syncUI();
|
this.syncUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
getEl('btn-next').onclick = () => {
|
getEl('btn-next').onclick = () => {
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
let nextTime = this.getMaxTime() + 1;
|
let nextTime = Math.floor(this.uiTickTime + 1);
|
||||||
for (const ev of this.frame.world.events) {
|
|
||||||
if (ev.time > this.frame.currentTime) {
|
|
||||||
nextTime = ev.time;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextTime > this.getMaxTime()) nextTime = this.isLooping ? 0 : this.getMaxTime();
|
if (nextTime > this.getMaxTime()) nextTime = this.isLooping ? 0 : this.getMaxTime();
|
||||||
|
|
||||||
this.frame.seek(nextTime);
|
this.frame.seek(nextTime);
|
||||||
|
this.uiTickTime = nextTime; // Explicit jump
|
||||||
this.syncUI();
|
this.syncUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setupDraggableTrack('track-tick-container', 'track-tick-inner', (pct) => {
|
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();
|
this.syncUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.opts.subticks) {
|
if (this.opts.subticks) {
|
||||||
getEl('btn-play-sub').onclick = () => {
|
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();
|
this.syncUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
getEl('btn-prev-sub').onclick = () => {
|
getEl('btn-prev-sub').onclick = () => {
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
let idx = this.frame.currentIndex - 1;
|
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);
|
this.frame.seekIndex(idx);
|
||||||
|
|
||||||
|
// Allow the UI to fall backwards naturally
|
||||||
|
if (idx < events.length && idx >= 0) this.uiTickTime = events[idx].time;
|
||||||
this.syncUI();
|
this.syncUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
getEl('btn-next-sub').onclick = () => {
|
getEl('btn-next-sub').onclick = () => {
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
let idx = this.frame.currentIndex + 1;
|
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);
|
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.syncUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setupDraggableTrack('track-sub-container', 'track-sub-inner', (pct) => {
|
this.setupDraggableTrack('track-sub-container', 'track-sub-inner', (pct) => {
|
||||||
if (this.currentTickEventCount === 0) return;
|
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();
|
this.syncUI();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -399,8 +427,19 @@ export class SpacetimeController {
|
|||||||
this.subRow.style.display = this.playMode === 'realtime' ? 'none' : 'flex';
|
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 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;
|
const tickPct = maxTime > 0 ? (displayTime / maxTime) * 100 : 0;
|
||||||
|
|
||||||
getEl('fill-tick').style.width = `${tickPct}%`;
|
getEl('fill-tick').style.width = `${tickPct}%`;
|
||||||
@@ -409,16 +448,38 @@ export class SpacetimeController {
|
|||||||
if (!this.opts.subticks) return;
|
if (!this.opts.subticks) return;
|
||||||
|
|
||||||
const events = this.frame.world.events;
|
const events = this.frame.world.events;
|
||||||
let activeTickTime = -Infinity;
|
if (events.length === 0) return;
|
||||||
for (const ev of events) {
|
|
||||||
if (ev.time <= this.frame.currentTime) activeTickTime = ev.time; else break;
|
// --- 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;
|
// Fix: Only auto-correct if we are NOT in realtime AND the tick isn't empty.
|
||||||
for (let i = 0; i < events.length; i++) {
|
// This stops empty ticks from auto-snapping to filled ticks.
|
||||||
if (events[i].time === activeTickTime) {
|
if (this.playMode !== 'realtime' && startIdx !== -1) {
|
||||||
if (count === 0) startIdx = i;
|
// Because scrubbing to 100% means currentIndex == startIdx + count,
|
||||||
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;
|
const dt = (time - this.lastFrameTime) / 1000;
|
||||||
this.lastFrameTime = time;
|
this.lastFrameTime = time;
|
||||||
const maxTime = this.getMaxTime();
|
const maxTime = this.getMaxTime();
|
||||||
|
|
||||||
if (this.playMode === 'realtime') {
|
if (this.playMode === 'realtime') {
|
||||||
let nextTime = this.frame.currentTime + dt * 20 * this.speed;
|
let nextTime = this.frame.currentTime + dt * 20 * this.speed;
|
||||||
if (nextTime >= maxTime) {
|
if (nextTime >= maxTime) {
|
||||||
if (this.isLooping) {
|
if (this.isLooping && maxTime > 0) {
|
||||||
this.frame.seek(nextTime % maxTime);
|
this.frame.seek(nextTime % maxTime);
|
||||||
} else {
|
} else {
|
||||||
this.frame.seek(maxTime);
|
this.frame.seek(maxTime);
|
||||||
|
this.uiTickTime = maxTime; // Snap UI to the very end
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.frame.seek(nextTime);
|
this.frame.seek(nextTime);
|
||||||
}
|
}
|
||||||
if (this.opts.timeline) this.syncUI();
|
if (this.opts.timeline) this.syncUI();
|
||||||
|
|
||||||
} else if (this.playMode === 'subtick') {
|
} else if (this.playMode === 'subtick') {
|
||||||
this.playbackAccumulator += dt * 4 * this.speed;
|
this.playbackAccumulator += dt * 4 * this.speed;
|
||||||
if (this.playbackAccumulator >= 1.0) {
|
if (this.playbackAccumulator >= 1.0) {
|
||||||
const steps = Math.floor(this.playbackAccumulator);
|
const steps = Math.floor(this.playbackAccumulator);
|
||||||
this.playbackAccumulator -= steps;
|
this.playbackAccumulator -= steps;
|
||||||
const nextIdx = this.frame.currentIndex + 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 (nextIdx > events.length) {
|
||||||
if (this.isLooping) this.frame.seekIndex(this.frame.world.initialIndex); else {
|
if (this.isLooping) {
|
||||||
this.frame.seekIndex(this.frame.world.events.length);
|
this.frame.seekIndex(initialIndex);
|
||||||
|
if (events.length > 0) this.uiTickTime = events[0].time;
|
||||||
|
} else {
|
||||||
|
this.frame.seekIndex(events.length);
|
||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.frame.seekIndex(nextIdx);
|
this.frame.seekIndex(nextIdx);
|
||||||
|
if (nextIdx < events.length) this.uiTickTime = events[nextIdx].time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.opts.timeline) this.syncUI();
|
if (this.opts.timeline) this.syncUI();
|
||||||
|
|||||||
Reference in New Issue
Block a user