src/scripts/v6/badge.ts
/* the card as a rigid body: the rope's last segment sets where "down the strap" is,
and an angular spring pulls the card to hang from it */
const prev = nodes[nodes.length - 2];
/* the sign matters: with it inverted the card rotated away from the strap, which is
positive feedback, and the swing sat at its limit for ever instead of dying */
const target = Math.atan2(end.x - prev.x, end.y - prev.y);
let err = ang - target;
while (err > Math.PI) err -= 2 * Math.PI;
while (err < -Math.PI) err += 2 * Math.PI;
angVel += (-SWING_K * err - SWING_C * angVel) * DT;
ang += angVel * DT;
/* A card on a lanyard does not cartwheel. The limit is soft and takes energy with it:
a hard stop that only zeroed the outward velocity bounced the spring off it forever,
because the spring put the same energy straight back. */
const LIM = 1.1;
if (ang > LIM) {
const over = ang - LIM;
angVel -= (over * 90 + angVel * 6) * DT;
ang = LIM + over * 0.4;
} else if (ang < -LIM) {
const over = -LIM - ang;
angVel += (over * 90 - angVel * 6) * DT;
ang = -LIM - over * 0.4;
}
src/styles/v6/hero.css
.badge-band {
position: absolute;
inset: auto 0 0 0;
height: 26%;
padding: var(--s-12) var(--s-16);
display: flex;
flex-direction: column;
justify-content: center;
gap: 3px;
background: #0b0d12;
border-top: 1px solid rgb(243 241 234 / 0.1);
}
src/styles/v6/hero.css
.badge-photo {
position: absolute;
inset: 0 0 26% 0;
overflow: hidden;
}
.badge-photo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 24%;
filter: grayscale(1) contrast(1.06);
}
src/styles/v6/hero.css
.badge-name {
font-family: var(--f-display);
font-stretch: 125%;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.04em;
color: var(--ivory);
white-space: nowrap;
}
src/styles/v6/hero.css
/* ---- the bottom band: ink to violet-ink, with a six-colour hairline along its top ---- */
.badge-band {
background: linear-gradient(180deg, #0b0d12 0%, #151029 100%);
border-top: 0;
}
.badge-band::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 1px;
background: linear-gradient(90deg, #8b6cff, #2bd67b, #ff6d5a, #9945ff, #22d3ee, #ffc93c);
opacity: 0.85;
}
src/scripts/v6/badge.ts
/* the foil, the edge and the specular band all read the same tilt */
const face = tiltY + t;
card.style.setProperty('--foil', `${(deg * 3 + face * 4).toFixed(1)}deg`);
card.style.setProperty('--foil-x', `${(50 - face * 1.6).toFixed(1)}%`);
card.style.setProperty('--foil-o', Math.min(0.45, 0.1 + (Math.abs(face) + Math.abs(tiltX)) * 0.0105).toFixed(3));
card.style.setProperty('--edge-a', `${(deg * 4 + face * 6).toFixed(1)}deg`);
card.style.setProperty('--spec-x', `${(-deg * 1.6 - face * 1.4).toFixed(1)}%`);
card.style.setProperty('--band', `${(50 - deg * 2.2 - face * 1.8).toFixed(1)}%`);
card.style.setProperty('--lift', lift.toFixed(3));
src/scripts/v6/badge.ts
if (shadow) {
/* a radial gradient on the wall behind, never a blur filter */
shadow.style.transform =
`translate3d(${(left + cardW() / 2 + deg * 1.4).toFixed(1)}px, ${(y + cardH() * 0.92).toFixed(1)}px, 0) ` +
`scale(${(1 - lift * 0.08).toFixed(3)}, ${(0.5 + Math.abs(tiltX) * 0.004).toFixed(3)})`;
shadow.style.opacity = (0.5 - lift * 0.1).toFixed(2);
}
src/scripts/v6/badge.ts
/** fixed physics step, in seconds */
const DT = 1 / 120;
const SEG = 14;
/** gravity in px/s² — tuned with the rope length so a swing reads at about 1.1 s */
const G = 2200;
/** the rope may stretch about 2 %: a lanyard is webbing, not wire */
const STRETCH = 0.02;
/** pendulum: ω² for a 1.1 s period, and damping that settles it in three or four swings */
const SWING_K = (2 * Math.PI / 1.1) ** 2;
/* ζ ≈ 0.48: the swing dies in three or four visible passes rather than ringing on. At
0.175 it rang against the limit below and never settled. */
const SWING_C = 5.5;
/** torsion around the strap: stiffer and better damped, so the spin resolves */
const TWIST_K = 26;
const TWIST_C = 2.4;
/** the hover tilt spring, from the brief */
const TILT_K = 170;
const TILT_C = 18;