Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 37 additions & 13 deletions scripts/generate-player-shells.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ mkdirSync(OUT, { recursive: true })
/** Every decorative card on the demo page, by section. */
const SPECS = [
// Pick a mood — 9 cards (PickAMoodSection.vue variant specs).
{ name: 'mood-auto', v: 'auto', w: 480 },
{ name: 'mood-vinyl', v: 'vinyl', a: '#C8A97E', w: 480 },
{ name: 'mood-sunset', v: 'sunset', a: '#F59E0B', w: 480 },
{ name: 'mood-midnight', v: 'midnight', a: '#8B5CF6', w: 480 },
{ name: 'mood-aurora', v: 'aurora', a: '#06B6D4', w: 480 },
{ name: 'mood-dark', v: 'dark', w: 480 },
{ name: 'mood-light', v: 'light', a: '#6750A4', w: 480 },
{ name: 'mood-transparent', v: 'transparent', w: 480, alpha: true },
{ name: 'mood-auto', v: 'auto', w: 536 },
{ name: 'mood-vinyl', v: 'vinyl', a: '#C8A97E', w: 536 },
{ name: 'mood-sunset', v: 'sunset', a: '#F59E0B', w: 536 },
{ name: 'mood-midnight', v: 'midnight', a: '#8B5CF6', w: 536 },
{ name: 'mood-aurora', v: 'aurora', a: '#06B6D4', w: 536 },
{ name: 'mood-dark', v: 'dark', w: 536 },
{ name: 'mood-light', v: 'light', a: '#6750A4', w: 536 },
{ name: 'mood-transparent', v: 'transparent', w: 536, alpha: true },
{
name: 'mood-custom-brown',
v: 'custom',
a: '#E8A87C',
w: 480,
w: 536,
bg: 'linear-gradient(135deg, #2c1610 0%, #4a2c1f 45%, #6b4226 100%)',
},
// Three widths — auto variant at the three demo breakpoints.
Expand Down Expand Up @@ -84,15 +84,39 @@ for (const s of SPECS) {
if (s.w) q.set('w', String(s.w))
if (s.bg) q.set('bg', s.bg)
await page.goto(url(q.toString()), { waitUntil: 'networkidle' })
// Transparent page so alpha captures stay alpha.
await page.addStyleTag({ content: 'html,body{background:transparent!important}' })
// Round-20 user report ("coins noirs") — the first pipeline only
// cleared html/body, but `.app` (and the showcase stage) kept their
// dark backgrounds BEHIND the player, so omitBackground had nothing
// transparent to reveal : every capture shipped opaque corners
// (hasAlpha=false). Clear the WHOLE ancestor chain — the rounded
// card then keeps true alpha corners.
await page.addStyleTag({
content:
'html,body,#app,.app,.showcase,.showcase__player{background:transparent!important}' +
'.app::before,.app::after,.showcase::before,.showcase::after{display:none!important}' +
// Round-21 ('coins sombres' follow-up) : the card's own
// box-shadow paints INSIDE the screenshot rect beyond the
// rounded corners (alpha~18 black pixels). Kill it at capture
// time — PlayerShell re-creates it at display time with
// drop-shadow, which follows the alpha (i.e. the radius).
'.showcase .mp{box-shadow:none!important}',
})
await page.waitForSelector('.showcase .mp', { timeout: 15000 })
// Let the cover image decode + the card settle.
await page.waitForTimeout(1200)
const el = page.locator('.showcase .mp').first()
const png = await el.screenshot({ omitBackground: true, type: 'png' })
const img = sharp(png)
const meta = await img.metadata()
// Round-21 — ALPHA EROSION : some component-internal overlay paints
// a faint veil (alpha ~18) across the whole rect, including the
// corners beyond the border-radius — the user's residual 'coins
// sombres' on light backdrops. Squash any alpha below 32 to 0
// (nothing legitimate in the chrome sits under ~12 % opacity ; the
// glassy `transparent` variant lives well above it).
const rawImg = sharp(png).ensureAlpha()
const { data, info } = await rawImg.raw().toBuffer({ resolveWithObject: true })
for (let i = 3; i < data.length; i += 4) if (data[i] < 32) data[i] = 0
const img = sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } })
const meta = info
const out = join(OUT, `${s.name}.webp`)
await img.webp({ quality: 84, alphaQuality: 90 }).toFile(out)
manifest[s.name] = {
Expand Down
48 changes: 23 additions & 25 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1941,27 +1941,15 @@ body.tour-running .mp[data-fab='true'] .mp__fab-chrome {
width: min(86vw, 1880px);
margin: 0 auto;
padding: clamp(60px, 6vw, 140px) clamp(24px, 3vw, 72px);
/* Round-12b FLUIDITY — the page is ~20 700 px tall with 1 900 DOM
nodes, 74 blurred layers and 87 promoted layers. content-visibility
lets the browser SKIP style/layout/paint for every offscreen plain
section (the 3 pinned showcases — .reveal, .rotate3d,
.phone-showcase — keep their own classes and are NOT affected, so
ScrollTrigger pin measurements stay exact). `auto` remembers the
real height after first render ; the 1100px estimate only seeds
the initial scrollbar. In-section animations are
IntersectionObserver-driven and keep working (intersection is
still computed for c-v subtrees). */
content-visibility: auto;
contain-intrinsic-size: auto 1100px;
}
/* The three pinned ScrollTrigger showcases also carry `.section` —
exempt them : pin spacers + scrub measurements need real layout at
refresh time, and `content-visibility` containment would skew them. */
.reveal,
.rotate3d,
.phone-showcase {
content-visibility: visible;
contain-intrinsic-size: none;
/* Round-21 — content-visibility REMOVED (was round-12b). It was
added when the page carried 25 live player instances and 74 live
blur layers ; skipping offscreen sections then saved real work.
After the shell architecture (round-14) and the decorative-blur
purge (rounds 17+21), permanent rendering is cheap — and c-v's
remaining contribution was NEGATIVE : its one-frame re-renders
were precisely the user-reported 'rame d'un coup' spikes at
section (re)entry on ascent (zone profiling : max 191-236 ms at
the bottom zones, gone without it). */
}
.section--narrow {
width: min(86vw, 1280px);
Expand Down Expand Up @@ -2245,7 +2233,8 @@ body.tour-running .mp[data-fab='true'] .mp__fab-chrome {
}
.responsive__frame {
max-width: 100%;
filter: drop-shadow(0 18px 40px rgba(0, 0, 0, 0.35));
/* Round-21 — drop-shadow moved into PlayerShell itself (the shadow
belongs to the card, and shells now ship shadow-free captures). */
}

/* ─── FAB PALETTE ──────────────────────────────────────────── */
Expand Down Expand Up @@ -2807,7 +2796,10 @@ samp,
width: clamp(220px, 30vw, 380px);
height: clamp(220px, 30vw, 380px);
border-radius: 50%;
filter: blur(60px);
/* Round-21 - blur removed : gradient-only decorative layer ; the
radial fade is already soft and the filter forced a costly raster
burst when the layer (re)entered the viewport (user-reported
hitches at pin release + page ascent). */
opacity: 0.55;
mix-blend-mode: screen;
/* The composable writes transform in vh units relative to the centre. */
Expand Down Expand Up @@ -3029,7 +3021,10 @@ samp,
rgba(139, 92, 246, 0.14) 32%,
transparent 62%
);
filter: blur(36px);
/* Round-21 - blur removed : gradient-only decorative layer ; the
radial fade is already soft and the filter forced a costly raster
burst when the layer (re)entered the viewport (user-reported
hitches at pin release + page ascent). */
z-index: -1;
pointer-events: none;
opacity: calc(0.7 + var(--pulse-ambient, 0) * 0.3);
Expand All @@ -3044,7 +3039,10 @@ samp,
bottom: -32px;
height: 28px;
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.45) 0%, transparent 70%);
filter: blur(12px);
/* Round-21 - blur removed : gradient-only decorative layer ; the
radial fade is already soft and the filter forced a costly raster
burst when the layer (re)entered the viewport (user-reported
hitches at pin release + page ascent). */
z-index: -1;
pointer-events: none;
}
Expand Down
Binary file modified src/assets/shells/face-back-sunset.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/face-front-auto.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 28 additions & 28 deletions src/assets/shells/manifest.json
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
{
"mood-auto": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-vinyl": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-sunset": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-midnight": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-aurora": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-dark": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-light": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-transparent": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"mood-custom-brown": {
"w": 960,
"h": 368,
"ratio": 2.6087
"w": 1072,
"h": 396,
"ratio": 2.7071
},
"width-320": {
"w": 640,
Expand Down Expand Up @@ -74,4 +74,4 @@
"h": 300,
"ratio": 2.2667
}
}
}
Binary file modified src/assets/shells/mood-aurora.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-auto.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-custom-brown.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-midnight.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-sunset.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-transparent.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/mood-vinyl.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/phone-auto.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/width-320.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/width-480.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shells/width-720.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 46 additions & 35 deletions src/components/AudioBars.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { onBeforeUnmount, onMounted, ref } from 'vue'
import { isScrolling } from '../composables/useScrollActivity'
import { isScrollingFast } from '../composables/useScrollActivity'

interface Engine {
eqBars: readonly number[]
Expand Down Expand Up @@ -55,11 +55,13 @@ const render = () => {
return
}
raf = requestAnimationFrame(render)
// Round-18 — freeze the draw while the page scrolls : a static
// canvas scrolls as a cached texture (zero raster), and the eye
// tracks the page motion, not the bars. Resumes within one frame
// after the scroll settles.
if (isScrolling()) return
// Round-23 ("la page tremble") — the round-18 freeze was BINARY :
// any scroll event froze the bars, then they snapped back through
// the attack smoothing 160 ms later. A slow wheel scroll therefore
// alternated freeze -> catch-up -> freeze : the user-reported
// tremor. The freeze now only engages during FAST scrolling
// (> 1500 px/s), where the eye cannot track the bars anyway.
if (isScrollingFast()) return
const c = canvas.value
if (!c) return
const ctx = c.getContext('2d')
Expand All @@ -75,49 +77,55 @@ const render = () => {
const engine = props.engine
const playing = engine?.isPlaying ?? false

// Upsample the 4 focal bars into N bands via cosine interpolation,
// plus a small symmetry curve so the visualiser feels balanced.
// Round-23 ("des vagues stables et calculées, ce sera plus joli") —
// the per-bar pseudo-random character (hash sin(i*12.9898)*43758…)
// made neighbours jitter independently : energetic, but noisy, and
// it burned a hash + ripple per bar per frame. New model : THREE
// superposed deterministic travelling sine waves (slow phase
// velocities, irrational-ish frequency ratios so the pattern never
// visibly repeats), modulated by the audio energy when playing.
// Calm, continuous, ocean-like — and cheaper : 3 sin() per bar.
//
// idle : signed-off silhouette + a barely-there breathing wave
// playing : silhouette anchor + energy envelope × wave field
const fft = engine?.eqBars ?? [0, 0, 0, 0]
// Round-7 (user feedback : "au repos elles sont très bien" mais en
// lecture "ça ne donne pas l'effet attendu") — the active branch used
// to REPLACE the idle silhouette with `base × bell`, which collapsed
// 64 bars onto 4 interpolated FFT values : everything rose and fell
// together, mushy and uniform. New model : the bars DANCE AROUND the
// resting silhouette instead of discarding it —
// target = idle-anchor + audio energy × bell × per-bar character
// with per-bar character = a stable pseudo-random gain (breaks the
// 4-band uniformity : neighbours respond differently to the same
// energy) + a travelling phase so peaks ripple across the row.
// Asymmetric smoothing (fast attack / slow release) makes kicks SNAP
// and decay like a real analyser instead of the old single-constant
// mush.
const tNow = performance.now() / 1000
// Global audio energy (mean of the 4 focal bands) — ONE smooth
// scalar instead of 64 independent reactions.
let energy = 0
if (playing) {
let sum = 0
for (let i = 0; i < fft.length; i++) sum += fft[i] || 0
energy = fft.length ? sum / fft.length : 0
}
for (let i = 0; i < N; i++) {
const t = i / (N - 1)
// Bell curve around centre — louder at the middle bands, quieter
// at edges. Matches the perceptual "spectrum" feel.
const bell = Math.sin(t * Math.PI) ** 1.6
// Sample 2 fft channels and blend.
const f0 = fft[Math.floor(t * (fft.length - 1))] ?? 0
const f1 = fft[Math.min(fft.length - 1, Math.floor(t * (fft.length - 1)) + 1)] ?? 0
const blend = (Math.floor(t * (fft.length - 1)) + t) % 1
const base = f0 * (1 - blend) + f1 * blend
// IDLE silhouette : static "spectrum at rest" (unchanged — the
// user signed off on this shape).
const idle = bell * 0.46 + Math.sin(i * 0.55) * 0.07 + Math.cos(i * 0.31) * 0.05
// Deterministic wave field, normalised to [0..1] : two travelling
// waves moving against each other + one long swell.
const x = t * Math.PI * 2
const wave =
0.5 +
0.27 * Math.sin(x * 3.0 - tNow * 1.35) +
0.16 * Math.sin(x * 5.2 + tNow * 0.9) +
0.07 * Math.sin(x * 1.3 - tNow * 0.4)
let target
if (playing) {
// Stable per-bar gain (deterministic from the index) so adjacent
// bars have personality ; travelling phase so energy ripples.
const character = 0.65 + 0.7 * Math.abs((Math.sin(i * 12.9898) * 43758.5453) % 1)
const ripple = 0.85 + 0.3 * Math.sin(i * 0.45 - tNow * 7)
const anchor = Math.max(0.14, idle * 0.55)
target = Math.min(1, anchor + base * bell * 1.5 * character * ripple)
target = Math.min(1, anchor + energy * bell * (0.55 + 0.85 * wave))
} else {
target = Math.max(0.18, idle)
// Resting silhouette with a breathing micro-swell (±0.03) so the
// strip feels alive without ever reading as motion.
target = Math.max(0.18, idle + (wave - 0.5) * 0.06)
}
// Asymmetric smoothing : ~35 ms attack, ~260 ms release at 60 fps.
const k = target > smoothed[i] ? 0.45 : 0.1
// Asymmetric smoothing — slightly softer attack than before
// (0.45 -> 0.3) : the wave model wants continuity, not snap.
const k = target > smoothed[i] ? 0.3 : 0.12
smoothed[i] += (target - smoothed[i]) * k
}

Expand Down Expand Up @@ -295,7 +303,10 @@ onBeforeUnmount(() => {
rgba(236, 72, 153, 0.1) 40%,
transparent 100%
);
filter: blur(40px);
/* Round-21 - blur removed : gradient-only decorative layer ; the
radial fade is already soft and the filter forced a costly raster
burst when the layer (re)entered the viewport (user-reported
hitches at pin release + page ascent). */
mix-blend-mode: screen;
}
.bars__canvas {
Expand Down
Loading
Loading