Synthwave verges stay off the road; camera stays level unless the tail says otherwise

Synthwave Run: roadside passers stream on the verges, not on the road.
Each passer places at roadHalf + psize*0.92 + gap where roadHalf mirrors
the grid's abs(p.x*perspective) < 0.8, so even a large chorus form's
edge stays clear of the centre lane reserved for the protagonist.
Lateral wobble reduced to worst ~0.025 inward vs. minimum 0.07 gap so
it can never carry back onto the road. Four random chorus objects
rushing horizon->camera make each song's traffic its own; on-road
traffic the hero weaves around is left as a separate kind later.
Also carries the previous chorus passer loop, hero castSolid, and
texture 0.4 / form+staging declarations still pending in the working
tree from the last step.

Personality camera: uniform boxes put as many tracks at the wild edge
as near level, so the grid swung too often. Replace with a bell curve
(Box-Muller over the seeded Rng) — mode at level, rarity in the tails.
driftRate/sway/swayRate/spin/breathe now via gaussAround(centre,sigma)
centred near level (spin at 0, sign from the Gaussian itself) with
audio tilting the centre (fast→more drift/sway/spin, dynamic→breathe)
rather than displacing the whole box.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-20 11:25:42 +02:00
co-authored by Claude
parent c216e31413
commit df609424d5
2 changed files with 98 additions and 7 deletions
+28 -6
View File
@@ -46,6 +46,15 @@ export const TRAITS = ['shape', 'camera', 'space', 'style'];
const clamp01 = (x) => Math.max(0, Math.min(1, x));
// Box-Muller standard normal from the seeded rng. Two uniform draws per
// sample; the mode is at 0 and the tails are rare, which is the whole
// point — uniform boxes put as many tracks at the wild edge as near level.
function gauss(rng) {
const u1 = Math.max(rng.next(), 1e-7);
const u2 = rng.next();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
/**
* Traits eligible to be a track's signature, and how often.
*
@@ -137,16 +146,29 @@ export function generatePersonality(summary, rng, countEligible = null, sections
tilt: rng.range(0, Math.PI),
};
// Camera: bell-curve over level vs. wild. Most tracks stay near
// level — a little drift, a little sway, almost no roll — and only
// the tail really swings. Uniform boxes put as many tracks at the
// wild edge as near level, which is why the grid read as wild so
// often; a Gaussian puts the mode at level and rarity in the tails.
// Audio still tilts the centre (fast → more drift/sway/spin,
// dynamic → more breathe) but the seed picks within a bell around it.
const gaussAround = (centre, sigma, lo, hi) =>
Math.max(lo, Math.min(hi, centre + gauss(rng) * sigma));
const camera = {
driftAngle: rng.range(0, Math.PI * 2),
// A slow track should not be filmed from a moving car.
driftRate: rng.range(0.01, 0.06) * (0.6 + fast * 0.8),
sway: around(0.01 + fast * 0.035, 0.02, 0, 0.06),
swayRate: around(0.08 + fast * 0.1, 0.06, 0.05, 0.22),
spin: around((rng.bool() ? 1 : -1) * fast * 0.03, 0.025, -0.05, 0.05),
// A slow track should not be filmed from a moving car. Centre near
// level (0.016 + tempo), sigma narrow so wild drift is a tail event.
driftRate: gaussAround(0.016 + fast * 0.016, 0.010, 0.004, 0.065),
sway: gaussAround(0.010 + fast * 0.018, 0.009, 0, 0.055),
swayRate: gaussAround(0.09 + fast * 0.06, 0.028, 0.05, 0.22),
// Spin centred at zero — most tracks stay level, only the tails roll.
// Spread scales with tempo so a fast track has a wider bell, not a
// displaced one. Sign comes from the Gaussian itself.
spin: gaussAround(0, 0.012 + fast * 0.012, -0.05, 0.05),
// Breathing is locked to the bar, so it is the one camera move that
// reads as musical rather than as drifting. A dynamic track breathes.
breathe: around(dynamic * 0.03, 0.02, 0, 0.05),
breathe: gaussAround(dynamic * 0.022, 0.010, 0, 0.048),
};
const space = {