Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
78dbb03
feat(venue): reactive crowd video layer behind the 3D highway (career…
byrongamatos Jul 11, 2026
cab652d
fix(venue-crowd): retry renderer binding + preserve mid-stinger trans…
byrongamatos Jul 11, 2026
5e5d892
fix(venue-crowd): per-video load tokens + unbind renderer on stop
byrongamatos Jul 11, 2026
1172bc3
fix(venue-crowd): flush deferred loop on stinger failure, source accu…
byrongamatos Jul 11, 2026
de10e81
fix(venue-crowd): requeue mid-crossfade loops preempted by stingers; …
byrongamatos Jul 11, 2026
9178959
fix(venue-crowd): generation-gate stinger handlers; recrop on video s…
byrongamatos Jul 11, 2026
286a242
fix(venue-crowd): bail loop-fade completion when a stinger preempted …
byrongamatos Jul 11, 2026
c73ff96
fix(venue-crowd): keep rear video layer opaque during crossfades
byrongamatos Jul 12, 2026
c83fec2
fix(venue-crowd): reset active layer with mix on stop
byrongamatos Jul 12, 2026
f9a57ba
fix(venue-crowd): reset crowd mood to neutral on song load
byrongamatos Jul 12, 2026
be78b4f
fix(venue-crowd): cancel in-flight fade when a stinger preempts it
byrongamatos Jul 12, 2026
f362c9a
fix(venue-crowd): don't let null accuracy resets wipe the end-of-song…
byrongamatos Jul 12, 2026
663e8ff
fix(venue-crowd): abort stale stinger state on song load
byrongamatos Jul 12, 2026
9d8fda6
fix(venue-crowd): always detach load listeners, gate only the callback
byrongamatos Jul 12, 2026
99812b8
feat(venue-crowd): flyover intro with crowd-ambience ducking
byrongamatos Jul 12, 2026
4abdfb5
fix(venue-crowd): fall back to the loop when the intro fails to load
byrongamatos Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions plugins/highway_3d/screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -1498,7 +1498,7 @@
const slu = n.slu;
if (Number.isFinite(sl) && sl >= 0) {
return { endFret: sl | 0, unpitched: false };
}

Check warning on line 1501 in plugins/highway_3d/screen.js

View workflow job for this annotation

GitHub Actions / ci / lint

File has too many lines (15829). Maximum allowed is 1500
if (Number.isFinite(slu) && slu >= 0) {
return { endFret: slu | 0, unpitched: true };
}
Expand Down Expand Up @@ -2418,6 +2418,13 @@
let _venueSceneAssetsLoaded = false;
let _venueSceneLoadFailed = false;
const _venueTextureCache = new Map();
// Crowd video layers (career mode). venue-crowd.js owns the <video>
// elements and the crossfade timing; the renderer only maps them onto
// two planes in front of the static plate. _venueCrowdRev bumps on any
// element (re)assignment so update() knows to rebind textures.
const _venueCrowdVideos = [null, null];
let _venueCrowdMix = 0;
let _venueCrowdRev = 0;

function _bgVenueMoodCoeffs(state) {
const s = String(state || 'idle').toLowerCase();
Expand Down Expand Up @@ -2909,6 +2916,20 @@
window.h3dVenueSceneSetMood = (state) => {
_venueMoodState = String(state || 'idle').toLowerCase();
};
// Crowd video layers (career mode) — see venue-crowd.js. Layer 0/1 are
// two coplanar backdrop planes; mix selects between them (0 → layer 0,
// 1 → layer 1) so the caller can crossfade loop videos.
window.h3dVenueBackdropSetVideo = (layer, videoEl) => {
const i = layer ? 1 : 0;
const el = videoEl || null;
if (_venueCrowdVideos[i] === el) return;
_venueCrowdVideos[i] = el;
_venueCrowdRev++;
};
window.h3dVenueBackdropSetMix = (mix) => {
const v = Number(mix);
_venueCrowdMix = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
};
window.h3dVenueSceneSetInstrumentPov = (input) => {
const next = _venueResolvePovFromInput(input);
if (_venueInstrumentPov === next) return;
Expand Down Expand Up @@ -3371,6 +3392,40 @@
() => _venueMarkFailed('failed to load small-club bg plate'),
);

// Crowd video planes (career mode): two crossfading layers
// just in front of the static plate (which stays mounted as
// the no-pack / load-failure fallback). Textures bind lazily
// in update() when venue-crowd.js assigns video elements.
state.crowd = { layers: [], rev: -1 };
for (let i = 0; i < 2; i++) {
const geo = new T.PlaneGeometry(1, 1);
const mat = new T.MeshBasicMaterial({
color: 0xffffff, transparent: true, opacity: 0,
depthWrite: false, fog: false,
});
const mesh = new T.Mesh(geo, mat);
mesh.visible = false;
// Layer 1 sits nearest so three.js's back-to-front
// transparent sort draws it after layer 0.
const layer = {
mesh, geo, mat, tex: null, videoEl: null,
cam: settings.cam,
distance: BG_BACKDROP_DISTANCE * (i === 0 ? 1.04 : 1.03),
lastAspect: 0, lastVisibleHeight: 0,
};
layer.applyCoverCrop = function () {
if (!layer.videoEl || !layer.tex) return;
_bgCoverCrop(
layer.tex,
layer.videoEl.videoWidth || 0,
layer.videoEl.videoHeight || 0,
layer.cam.aspect,
);
};
scene.add(mesh);
state.crowd.layers.push(layer);
}

const hazeGeo = new T.PlaneGeometry(280 * K, 40 * K);
const hazeMat = new T.MeshBasicMaterial({
color: 0x101820, transparent: true, opacity: coeffs.haze,
Expand Down Expand Up @@ -3402,6 +3457,64 @@
s.haze.mat.opacity = (s.haze.baseOp || VENUE_HAZE_STEADY)
* (coeffs.haze / VENUE_HAZE_STEADY);
}
if (s.crowd) {
// Rebind VideoTextures when venue-crowd.js (re)assigns
// elements. VideoTexture samples the element every frame,
// so a src change on the same element needs no rebind.
if (s.crowd.rev !== _venueCrowdRev) {
s.crowd.rev = _venueCrowdRev;
s.crowd.layers.forEach((layer, i) => {
const el = _venueCrowdVideos[i];
if (layer.videoEl === el) return;
if (layer.tex) { layer.mat.map = null; layer.tex.dispose(); layer.tex = null; }
layer.videoEl = el;
layer.lastAspect = 0; // force refit + recrop
if (el) {
const tex = new T.VideoTexture(el);
tex.colorSpace = T.SRGBColorSpace;
tex.wrapS = T.ClampToEdgeWrapping;
tex.wrapT = T.ClampToEdgeWrapping;
tex.minFilter = T.LinearFilter;
tex.magFilter = T.LinearFilter;
tex.generateMipmaps = false;
layer.tex = tex;
layer.mat.map = tex;
}
layer.mat.needsUpdate = true;
});
}
const warm = coeffs.warmth;
s.crowd.layers.forEach((layer, i) => {
const el = layer.videoEl;
// videoWidth === 0 until metadata lands — showing the
// plane before that paints a black flash over the plate.
const ready = !!el && el.videoWidth > 0;
// venue-crowd.js swaps src on the same element (loop ↔
// stinger); a new intrinsic size needs a fresh
// cover-crop, which _bgFitBackdropPlane only reapplies
// on camera aspect changes.
if (ready && (layer.lastVidW !== el.videoWidth ||
layer.lastVidH !== el.videoHeight)) {
layer.lastVidW = el.videoWidth;
layer.lastVidH = el.videoHeight;
layer.applyCoverCrop();
}
Comment on lines +3464 to +3501

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline plugins/highway_3d/screen.js --match '_bgFitBackdropPlane|_bgCoverCrop' --items all
rg -nP -C4 'function\s+_bgFitBackdropPlane|function\s+_bgCoverCrop|applyCoverCrop' plugins/highway_3d/screen.js

Repository: got-feedBack/feedBack

Length of output: 5832


Reset the crowd crop trackers on texture rebind
layer.lastAspect = 0 only forces the plane fit; applyCoverCrop() is still skipped when the new <video> keeps the same videoWidth/videoHeight as the previous source. Reset lastVidW/lastVidH here so the fresh VideoTexture gets cover-cropped once metadata lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/highway_3d/screen.js` around lines 3464 - 3501, Reset layer.lastVidW
and layer.lastVidH alongside layer.lastAspect when rebinding a crowd layer’s
video texture in the s.crowd.rev update block, ensuring applyCoverCrop() runs
after metadata becomes available even when the replacement video has identical
dimensions.

// Layer 0 (rear) stays fully opaque whenever any of the
// fade involves it: two half-transparent layers would
// let the static plate behind bleed through (~25% at
// mid-fade). The crossfade is therefore layer 1 (front)
// fading over an opaque layer 0 — in both directions.
const opacity = i === 0
? (_venueCrowdMix < 0.999 ? 1 : 0)
: _venueCrowdMix;
layer.mat.opacity = opacity;
layer.mesh.visible = ready && opacity > 0.01;
if (layer.mesh.visible) {
layer.mat.color.setRGB(warm, warm * 0.98, warm * 0.95);
_bgFitBackdropPlane(layer);
}
});
}
},
teardown(s) {
if (!s) return;
Expand All @@ -3416,6 +3529,19 @@
p.mat.dispose?.();
}
}
// Crowd planes: this style owns the VideoTextures; the
// <video> elements belong to venue-crowd.js and survive.
if (s.crowd) {
for (const layer of s.crowd.layers) {
layer.mesh?.parent?.remove(layer.mesh);
layer.geo?.dispose?.();
if (layer.mat) {
layer.mat.map = null;
layer.mat.dispose?.();
}
layer.tex?.dispose?.();
}
}
// Dispose the cached plate textures too — the module-level cache
// otherwise keeps every loaded POV plate GPU-resident for the
// page lifetime (steady VRAM growth across POV/arrangement swaps).
Expand Down
1 change: 1 addition & 0 deletions static/v3/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,7 @@ <h3>Gameplay Settings</h3>
saved 'off'/'full' motion preference on first paint. -->
<script defer src="/static/v3/venue-mood-fx.js"></script>
<script defer src="/static/v3/venue-scene-3d.js"></script>
<script defer src="/static/v3/venue-crowd.js"></script>
<script defer src="/static/v3/playlists.js"></script>
<script defer src="/static/v3/audio-routing.js"></script>
<script defer src="/static/v3/live-guitar-tone-source.js"></script>
Expand Down
Loading
Loading