feat(altitude): auto-advance when the altitude's video completes (not a timer)
Replace the fixed 60s idle timer with video-completion pacing: when the base loop video finishes one playthrough (the timeupdate wrap point, or the ended safety net), drift one altitude deeper (descend, wraps) — gated by the default-on toggle and deferred during a manual drag or a playing morph. Removes the whole AUTO_ADVANCE_MS/scheduleAutoAdvance/bump timer machinery and its interaction resets. loop-recovery.spec disables the toggle to test the loop-back path in isolation. auto-advance.spec reworked to the completion-driven model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-26
@@ -328,6 +328,7 @@ function ensureClipMedia() {
|
||||
// tail. The morph element (#vid) is driven directly by the scrub.
|
||||
loopVid.addEventListener("timeupdate", () => {
|
||||
if (loopVid.dataset.loopTail === "1" && loopVid.duration && loopVid.currentTime >= loopVid.duration - 0.08) {
|
||||
if (autoAdvanceOnLoopComplete()) return; // completed a playthrough → next altitude
|
||||
try { loopVid.currentTime = loopStartFrame(); } catch (e) { /* not seekable */ }
|
||||
}
|
||||
});
|
||||
@@ -339,6 +340,7 @@ loopVid.addEventListener("timeupdate", () => {
|
||||
// rarely hits this; loaded real machines do.
|
||||
loopVid.addEventListener("ended", () => {
|
||||
if (loopVid.dataset.loopTail !== "1") return;
|
||||
if (autoAdvanceOnLoopComplete()) return; // completed a playthrough → next altitude
|
||||
try { loopVid.currentTime = loopStartFrame(); } catch (e) { /* not seekable */ }
|
||||
loopVid.play().catch(() => {});
|
||||
});
|
||||
@@ -848,7 +850,6 @@ function autoScrub(targetPos, perAltMs = PER_ALTITUDE_MS) {
|
||||
let wheelAccum = 0, wheelTimer = null;
|
||||
function onWheel(e) {
|
||||
e.preventDefault();
|
||||
bumpAutoAdvance();
|
||||
wheelAccum += e.deltaY;
|
||||
clearTimeout(wheelTimer);
|
||||
wheelTimer = setTimeout(() => {
|
||||
@@ -858,26 +859,19 @@ function onWheel(e) {
|
||||
}, 90);
|
||||
}
|
||||
|
||||
// --- Auto-advance: after the dial sits idle for AUTO_ADVANCE_MS, drift one altitude
|
||||
// deeper (descend; past the deepest it wraps back to the top), a slow kiosk tour. Any
|
||||
// manual interaction restarts the idle countdown; a default-on toggle disables it.
|
||||
// (E2E injects a short interval via window.__hefAutoAdvanceMs before load.)
|
||||
const AUTO_ADVANCE_MS = (typeof window !== "undefined" && window.__hefAutoAdvanceMs) || 60000;
|
||||
let autoAdvanceTimer = null;
|
||||
// --- Auto-advance: when the altitude's base loop VIDEO completes one playthrough, drift
|
||||
// one altitude deeper (descend; past the deepest it wraps back to the top) — a slow kiosk
|
||||
// tour paced by the footage itself. A default-on toggle disables it; a manual drag or a
|
||||
// playing morph defers the move (it happens on the NEXT completion instead). Hooked into
|
||||
// the loop element's completion points (the timeupdate wrap + the ended safety net).
|
||||
function autoAdvanceOn() { const el = $("auto-advance"); return el ? el.checked : true; }
|
||||
function scheduleAutoAdvance() {
|
||||
clearTimeout(autoAdvanceTimer);
|
||||
autoAdvanceTimer = null;
|
||||
if (!autoAdvanceOn() || !videoEverOn) return; // only while the experience is running
|
||||
autoAdvanceTimer = setTimeout(onAutoAdvance, AUTO_ADVANCE_MS);
|
||||
function autoAdvanceOnLoopComplete() {
|
||||
if (autoAdvanceOn() && videoEverOn && !dialDrag && !busy) {
|
||||
autoScrub(Math.round(pos) + 1); // completed a playthrough → next altitude
|
||||
return true; // advanced — caller skips the normal loop-back
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function onAutoAdvance() {
|
||||
// Skip a beat (but keep the cadence) if the visitor is mid-drag or a morph is playing.
|
||||
if (autoAdvanceOn() && videoEverOn && !dialDrag && !busy) autoScrub(Math.round(pos) + 1);
|
||||
scheduleAutoAdvance();
|
||||
}
|
||||
// Restart the idle countdown on any manual interaction (drag/wheel/key/knob).
|
||||
function bumpAutoAdvance() { if (videoEverOn) scheduleAutoAdvance(); }
|
||||
|
||||
// --- Altitude knob: the endless rotary encoder, drawn as a turnable dial ---
|
||||
// cosmos (highest) sits at the top; turning CLOCKWISE descends through the scales
|
||||
@@ -1086,7 +1080,6 @@ function setPos(next) {
|
||||
function onDialDown(e) {
|
||||
if (!ring || ring.scales.length < 2) return;
|
||||
e.preventDefault();
|
||||
bumpAutoAdvance();
|
||||
dialDrag = { lastAng: dialAngle(e), accum: 0, moved: 0, target: e.target, restPos: pos };
|
||||
}
|
||||
function onDialMove(e) {
|
||||
@@ -1113,7 +1106,6 @@ function onDialUp(e) {
|
||||
// short way to the CLOSEST one and settle there (frac 0 locks). The result is the
|
||||
// same as a tap/label-click: a grab-and-release always lands on a discrete altitude.
|
||||
autoScrub(Math.round(pos));
|
||||
bumpAutoAdvance();
|
||||
}
|
||||
|
||||
// Click a label → auto-scrub the SHORTEST signed way around the ring to that scale.
|
||||
@@ -1144,7 +1136,7 @@ function onDialKey(e) {
|
||||
case "End": jumpToScale(ring.scales.length - 1); break;
|
||||
default: handled = false;
|
||||
}
|
||||
if (handled) { e.preventDefault(); bumpAutoAdvance(); }
|
||||
if (handled) e.preventDefault();
|
||||
}
|
||||
|
||||
// --- Dev Mode: pool picker + live analysis, all under one toggle ---
|
||||
@@ -1695,7 +1687,6 @@ function beginExperience() {
|
||||
updateAudioLevelLabel();
|
||||
applyAudio();
|
||||
applyPlaySpeed();
|
||||
scheduleAutoAdvance(); // start the idle-drift countdown now that we're running
|
||||
}
|
||||
|
||||
// Welcome screen ----------------------------------------------------------------
|
||||
@@ -1770,9 +1761,7 @@ async function main() {
|
||||
initWelcome(); // wire the welcome-screen controls + Launch button
|
||||
renderScaleReadout();
|
||||
// Sliders stream on "input"; the toggles fire on "change".
|
||||
for (const id of ["left", "right", "mood"]) $(id).addEventListener("input", () => { bumpAutoAdvance(); debounced(); });
|
||||
// Auto-advance toggle: (re)start or cancel the idle-drift countdown on change.
|
||||
$("auto-advance").addEventListener("change", scheduleAutoAdvance);
|
||||
for (const id of ["left", "right", "mood"]) $(id).addEventListener("input", debounced);
|
||||
// Audio level dial: set the master soundtrack gain. Reconcile SYNCHRONOUSLY in this
|
||||
// input gesture so the first raise from 0 unlocks + plays on Safari (which blocks
|
||||
// play() outside a user gesture).
|
||||
|
||||
Reference in New Issue
Block a user