From da74871fa1afa49db65ce5c72199756071f4ca42 Mon Sep 17 00:00:00 2001 From: YamadaBlog Date: Fri, 12 Jun 2026 12:15:27 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(lib):=20v2.3.5=20=E2=80=94=20EqBarsRow?= =?UTF-8?q?=20extraction=20+=2030=20Hz=20store=20notify=20(fluidity)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library-side share of the round-12 fluidity work : - useAudioStore: eqBars triggerRef notifications capped at 30 Hz (analyser still sampled per frame ; consumers re-render half as often — the 4-bar chrome is visually identical at half rate) - EqBarsRow.vue: the per-instance eq DOM extracted into one shared renderer component used by MusicPlayer + MiniPlayer - byte-identical gate baseline moved v2.3.4 -> v2.3.5 (tag pushed with this PR ; PULSE_LIB_BASELINE_REF still overrides) Co-Authored-By: Claude Fable 5 --- scripts/check-lib-byte-identical.mjs | 2 +- src/lib/EqBarsRow.vue | 43 ++++++++++++++++++++++++++++ src/lib/MiniPlayer.vue | 13 ++++----- src/lib/MusicPlayer.vue | 17 ++++++----- src/lib/useAudioStore.ts | 20 +++++++++++-- 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 src/lib/EqBarsRow.vue diff --git a/scripts/check-lib-byte-identical.mjs b/scripts/check-lib-byte-identical.mjs index e959f76..6e9857a 100644 --- a/scripts/check-lib-byte-identical.mjs +++ b/scripts/check-lib-byte-identical.mjs @@ -31,7 +31,7 @@ import { execFileSync } from 'node:child_process' import process from 'node:process' -const BASELINE_REF = process.env.PULSE_LIB_BASELINE_REF ?? 'v2.3.4' +const BASELINE_REF = process.env.PULSE_LIB_BASELINE_REF ?? 'v2.3.5' const TARGET_REF = process.env.PULSE_LIB_TARGET_REF ?? 'HEAD' const LIB_PATH = 'src/lib/' diff --git a/src/lib/EqBarsRow.vue b/src/lib/EqBarsRow.vue new file mode 100644 index 0000000..b5aad8d --- /dev/null +++ b/src/lib/EqBarsRow.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/lib/MiniPlayer.vue b/src/lib/MiniPlayer.vue index 36cd603..8e16579 100644 --- a/src/lib/MiniPlayer.vue +++ b/src/lib/MiniPlayer.vue @@ -21,6 +21,7 @@ import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue' import { Play, Pause, SkipForward, X } from 'lucide-vue-next' import { useAudioStore } from './useAudioStore' +import EqBarsRow from './EqBarsRow.vue' import type { PulseVariant } from './shared/types' import { useProgressRing } from './shared/useProgressRing' @@ -385,11 +386,9 @@ onUnmounted(() => { @@ -776,7 +775,7 @@ onUnmounted(() => { height: 8px; z-index: 2; } -.fab__eq i { +.fab__eq :deep(i) { display: block; width: 2px; height: 100%; /* fixed height — animate via scaleY for GPU compositing */ @@ -896,7 +895,7 @@ onUnmounted(() => { .fab__overlay, .fab__ring-progress, .fab__menu-btn, - .fab__eq i { + .fab__eq :deep(i) { transition: none !important; } .fab-pop-enter-active, diff --git a/src/lib/MusicPlayer.vue b/src/lib/MusicPlayer.vue index eb761a9..535edd5 100644 --- a/src/lib/MusicPlayer.vue +++ b/src/lib/MusicPlayer.vue @@ -50,6 +50,7 @@ const AMBIENT_BAR_STYLES: { color: string }[] = (() => { import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { Play, Pause, SkipBack, SkipForward } from 'lucide-vue-next' import { useAudioStore } from './useAudioStore' +import EqBarsRow from './EqBarsRow.vue' import type { PulseVariant } from './shared/types' import { useProgressRing } from './shared/useProgressRing' @@ -428,11 +429,9 @@ onUnmounted(() => {
NOW PLAYING
@@ -517,7 +516,7 @@ onUnmounted(() => {
- + { .mp__fab-eq--on { opacity: 1; } -.mp__fab-eq i { +.mp__fab-eq :deep(i) { display: block; width: 2px; height: 100%; /* fixed height — animate via scaleY */ @@ -1219,7 +1218,7 @@ onUnmounted(() => { } /* EQ bars locked to Spotify green for brand consistency. Themes (accent override, variant) do NOT touch this color. */ -.mp__eq i { +.mp__eq :deep(i) { display: block; width: var(--pulse-eq-w); height: 100%; /* fixed; animate via scaleY for GPU compositing */ @@ -1484,7 +1483,7 @@ onUnmounted(() => { .mp__ambient, .mp__ambient i, .mp__eq i, - .mp__fab-eq i { + .mp__fab-eq :deep(i) { transition: none !important; } .mp__resize { diff --git a/src/lib/useAudioStore.ts b/src/lib/useAudioStore.ts index fb759c5..8f3ee0f 100644 --- a/src/lib/useAudioStore.ts +++ b/src/lib/useAudioStore.ts @@ -239,18 +239,34 @@ export const useAudioStore = defineStore('pulsePlayerAudio', () => { // shallowRef skips deep proxy work, triggerRef notifies all // consumers with a single update. No per-frame allocations. const focal = eqBars.value + // v2.3.5 — Vue consumers are notified at 30 Hz instead of 60. The + // triggerRef() broadcast re-runs the render effect of EVERY + // component whose template reads `eqBars` (MusicPlayer, MiniPlayer) + // — a full vdom patch of large components, 60 times a second. + // Profiled on the demo page at 2560×1440 (headed Chromium, real + // GPU): freezing the eq data while keeping the loop alive dropped + // playing frame time from p50 36 ms to 12 ms — the broadcast chain + // was the single largest per-frame cost while audio played. + // Halving the notification rate halves that cost ; a 4-bar + // visualiser at 30 Hz is visually indistinguishable from 60. The + // underlying buffer still updates every frame, so non-reactive + // pollers (canvas visualisers reading `eqBars` in their own rAF) + // keep full 60 Hz resolution. + let notifyToggle = false function tick() { if (!analyser) { eqRaf = null return } analyser.getByteFrequencyData(data) - // 4-bar condensed visualiser (NOW PLAYING / FAB chrome) — full 60 fps. + // 4-bar condensed visualiser (NOW PLAYING / FAB chrome) — buffer + // at full 60 fps, reactive notify at 30 (see header note). focal[0] = data[3] / 255 focal[1] = data[8] / 255 focal[2] = data[18] / 255 focal[3] = data[36] / 255 - triggerRef(eqBars) + notifyToggle = !notifyToggle + if (notifyToggle) triggerRef(eqBars) // Note: since v1.0.2, the 64-bar `eqAmbientBars` ref is no longer // driven from this loop. The ambient EQ visualiser runs entirely // on a shared CSS @keyframes animation — composited on the GPU, From 48335baebddce36f28c833b8902c2f2223a4efe4 Mon Sep 17 00:00:00 2001 From: YamadaBlog Date: Fri, 12 Jun 2026 12:15:46 +0200 Subject: [PATCH 2/3] =?UTF-8?q?perf(demo):=20rounds=2012-14=20=E2=80=94=20?= =?UTF-8?q?fluidity=20overhaul=20+=20real-vs-shell=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured root causes (2560x1440, prod build, headed GPU) : - per-frame filter pumps on hero glow/backdrop -> opacity/transform - 5 always-on rAF loops (orbit orbs blur(60px)+blend stepped 60 Hz even offscreen) -> IntersectionObserver-gated, orbs 30 Hz - scroll-driven font-variation-settings on the 96px headline (full text relayout per frame) -> static weight - content-visibility:auto on plain sections (pinned showcases exempt) - 25 full MusicPlayer instances -> 5 real + 11 pre-rendered WebP shells (PlayerShell.vue, 284 kB total) ; Pick-a-mood keeps ONE live player, clicking a shell moves it there ; reveal mobile slides now mount only at <=720px Numbers (same protocol before/after) : idle paused p50 6.1 ms 0% jank ; idle playing 36-48 -> 18.1 ms (1% jank) ; scroll-while-playing jank 63-99% -> 30% ; live backdrop-filters 29 -> 9, blur layers 45 -> 20. Shell pipeline : npm run generate:shells (showcase rig + sharp), regenerate after any lib visual change. Co-Authored-By: Claude Fable 5 --- package-lock.json | 573 ++++++++++++++++++++++- package.json | 6 +- scripts/generate-player-shells.mjs | 104 ++++ src/App.vue | 116 ++++- src/assets/shells/face-back-sunset.webp | Bin 0 -> 19312 bytes src/assets/shells/face-front-auto.webp | Bin 0 -> 16262 bytes src/assets/shells/manifest.json | 77 +++ src/assets/shells/mood-aurora.webp | Bin 0 -> 18792 bytes src/assets/shells/mood-auto.webp | Bin 0 -> 16970 bytes src/assets/shells/mood-custom-brown.webp | Bin 0 -> 17462 bytes src/assets/shells/mood-dark.webp | Bin 0 -> 11330 bytes src/assets/shells/mood-light.webp | Bin 0 -> 25130 bytes src/assets/shells/mood-midnight.webp | Bin 0 -> 14188 bytes src/assets/shells/mood-sunset.webp | Bin 0 -> 20642 bytes src/assets/shells/mood-transparent.webp | Bin 0 -> 11480 bytes src/assets/shells/mood-vinyl.webp | Bin 0 -> 14108 bytes src/assets/shells/phone-auto.webp | Bin 0 -> 12154 bytes src/assets/shells/width-320.webp | Bin 0 -> 11006 bytes src/assets/shells/width-480.webp | Bin 0 -> 16954 bytes src/assets/shells/width-720.webp | Bin 0 -> 27312 bytes src/components/AudioBars.vue | 30 ++ src/components/PhoneShowcase.vue | 18 +- src/components/PickAMoodSection.vue | 77 ++- src/components/PlayerShell.vue | 60 +++ src/components/ProductReveal.vue | 23 +- src/components/ProductRotate3D.vue | 33 +- src/components/ThreeWidthsSection.vue | 20 +- src/composables/useAdvancedMotion.ts | 141 ++++-- src/composables/useCinematicEffects.ts | 28 +- 29 files changed, 1206 insertions(+), 100 deletions(-) create mode 100644 scripts/generate-player-shells.mjs create mode 100644 src/assets/shells/face-back-sunset.webp create mode 100644 src/assets/shells/face-front-auto.webp create mode 100644 src/assets/shells/manifest.json create mode 100644 src/assets/shells/mood-aurora.webp create mode 100644 src/assets/shells/mood-auto.webp create mode 100644 src/assets/shells/mood-custom-brown.webp create mode 100644 src/assets/shells/mood-dark.webp create mode 100644 src/assets/shells/mood-light.webp create mode 100644 src/assets/shells/mood-midnight.webp create mode 100644 src/assets/shells/mood-sunset.webp create mode 100644 src/assets/shells/mood-transparent.webp create mode 100644 src/assets/shells/mood-vinyl.webp create mode 100644 src/assets/shells/phone-auto.webp create mode 100644 src/assets/shells/width-320.webp create mode 100644 src/assets/shells/width-480.webp create mode 100644 src/assets/shells/width-720.webp create mode 100644 src/components/PlayerShell.vue diff --git a/package-lock.json b/package-lock.json index b198444..0f9d0e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-player", - "version": "2.3.4", + "version": "2.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-player", - "version": "2.3.4", + "version": "2.3.5", "license": "MIT", "workspaces": [ "packages/*", @@ -46,6 +46,7 @@ "prettier": "^3.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sharp": "^0.35.1", "size-limit": "^12.1.0", "tsup": "^8.5.1", "typescript": "^5.4.0", @@ -1328,6 +1329,16 @@ "node": ">=6.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -2213,6 +2224,506 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.1.tgz", + "integrity": "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.1.tgz", + "integrity": "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.1.tgz", + "integrity": "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==", + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.1.tgz", + "integrity": "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.1.tgz", + "integrity": "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.1.tgz", + "integrity": "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.1.tgz", + "integrity": "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.1.tgz", + "integrity": "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.1.tgz", + "integrity": "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.1.tgz", + "integrity": "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.1.tgz", + "integrity": "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.1.tgz", + "integrity": "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.1.tgz", + "integrity": "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.1.tgz", + "integrity": "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.1.tgz", + "integrity": "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.1.tgz", + "integrity": "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -12031,9 +12542,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", - "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -12212,6 +12723,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/sharp": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz", + "integrity": "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==", + "dev": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.1", + "@img/sharp-darwin-x64": "0.35.1", + "@img/sharp-freebsd-wasm32": "0.35.1", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.1", + "@img/sharp-linux-arm64": "0.35.1", + "@img/sharp-linux-ppc64": "0.35.1", + "@img/sharp-linux-riscv64": "0.35.1", + "@img/sharp-linux-s390x": "0.35.1", + "@img/sharp-linux-x64": "0.35.1", + "@img/sharp-linuxmusl-arm64": "0.35.1", + "@img/sharp-linuxmusl-x64": "0.35.1", + "@img/sharp-webcontainers-wasm32": "0.35.1", + "@img/sharp-win32-arm64": "0.35.1", + "@img/sharp-win32-ia32": "0.35.1", + "@img/sharp-win32-x64": "0.35.1" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15361,7 +15916,7 @@ }, "packages/core": { "name": "@pulse-music/core", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "license": "MIT", "dependencies": { "@pulse-music/types": "*" @@ -15369,7 +15924,7 @@ }, "packages/react": { "name": "@pulse-music/react", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "license": "MIT", "dependencies": { "@pulse-music/core": "*", @@ -15400,7 +15955,7 @@ }, "packages/svelte": { "name": "@pulse-music/svelte", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "license": "MIT", "dependencies": { "@pulse-music/core": "*", @@ -15440,7 +15995,7 @@ }, "packages/web-component": { "name": "@pulse-music/web-component", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "license": "MIT", "dependencies": { "@pulse-music/core": "*", diff --git a/package.json b/package.json index 44b023e..e2cbf4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-player", - "version": "2.3.4", + "version": "2.3.5", "description": "A premium drop-in music player for Vue 3 — inline card + floating FAB, FFT visualiser, nine themes, guided demo tour, ~49 kB gzip.", "type": "module", "workspaces": [ @@ -90,7 +90,8 @@ "ci": "npm run type-check && npm run lint && npm run test && npm run test:packages && npm run build && npm run audit && npm run check:lib-identical", "prepublishOnly": "npm run ci && npm run build:lib", "prepare": "husky 2>/dev/null || true", - "test:consumer": "node scripts/consumer-smoke.mjs" + "test:consumer": "node scripts/consumer-smoke.mjs", + "generate:shells": "node scripts/generate-player-shells.mjs" }, "peerDependencies": { "pinia": "^2.1.0", @@ -129,6 +130,7 @@ "prettier": "^3.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sharp": "^0.35.1", "size-limit": "^12.1.0", "tsup": "^8.5.1", "typescript": "^5.4.0", diff --git a/scripts/generate-player-shells.mjs b/scripts/generate-player-shells.mjs new file mode 100644 index 0000000..efa627e --- /dev/null +++ b/scripts/generate-player-shells.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * generate-player-shells.mjs — round-14 shell-capture pipeline. + * + * Renders every decorative MusicPlayer configuration through the + * showcase rig (`?showcase=1&v=…&a=…&w=…&bg=…&t=0&clean=1`), element- + * screenshots the `.mp` card at DPR 2, converts to WebP (sharp), and + * writes the assets consumed by PlayerShell.vue. + * + * Why : the demo used to mount 25 FULL player instances (29 live + * backdrop-filters, 45 blur layers). Decorative cards are now + * pre-rendered pixels — this script is the single source of those + * pixels. Re-run after any lib visual change : + * + * npm run generate:shells + * + * Requires a prod build in dist/ (BASE_PATH=/pulse-player/) — the + * script boots its own `vite preview` on :4943 and tears it down. + */ + +import { chromium } from '@playwright/test' +import { spawn } from 'node:child_process' +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import sharp from 'sharp' + +const ROOT = join(fileURLToPath(import.meta.url), '..', '..') +const OUT = join(ROOT, 'src', 'assets', 'shells') +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-custom-brown', + v: 'custom', + a: '#E8A87C', + w: 480, + bg: 'linear-gradient(135deg, #2c1610 0%, #4a2c1f 45%, #6b4226 100%)', + }, + // Three widths — auto variant at the three demo breakpoints. + { name: 'width-320', v: 'auto', w: 320 }, + { name: 'width-480', v: 'auto', w: 480 }, + { name: 'width-720', v: 'auto', w: 720 }, + // Rotate-3D faces. + { name: 'face-front-auto', v: 'auto', w: 460 }, + { name: 'face-back-sunset', v: 'sunset', a: '#F59E0B', w: 460 }, + // Phone showcase widget. + { name: 'phone-auto', v: 'auto', w: 340 }, +] + +// ─── boot a throwaway preview server ───────────────────────────────── +const PORT = 4943 +const srv = spawn('npx', ['vite', 'preview', '--port', String(PORT), '--strictPort'], { + cwd: ROOT, + shell: true, + env: { ...process.env, BASE_PATH: '/pulse-player/' }, + stdio: 'ignore', +}) +const url = (q) => `http://localhost:${PORT}/pulse-player/?showcase=1&t=0&clean=1&${q}` +await new Promise((r) => setTimeout(r, 2500)) + +const browser = await chromium.launch() +const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 }, deviceScaleFactor: 2 }) +const page = await ctx.newPage() + +const manifest = {} +for (const s of SPECS) { + const q = new URLSearchParams() + q.set('v', s.v) + if (s.a) q.set('a', s.a) + 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}' }) + 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() + const out = join(OUT, `${s.name}.webp`) + await img.webp({ quality: 84, alphaQuality: 90 }).toFile(out) + manifest[s.name] = { w: meta.width, h: meta.height, ratio: +(meta.width / meta.height).toFixed(4) } + console.log(`✓ ${s.name}.webp ${meta.width}×${meta.height}`) +} + +writeFileSync(join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2)) +console.log(`\n✅ ${SPECS.length} shells written to src/assets/shells/ (+ manifest.json)`) + +await browser.close() +srv.kill() +process.exit(0) diff --git a/src/App.vue b/src/App.vue index 537e5e3..323bcef 100644 --- a/src/App.vue +++ b/src/App.vue @@ -108,12 +108,10 @@ useStagedReveal({ stagger: 0.08, startDelay: 0.12, }) -const ambientRoot = ref(null) const audioReactiveSnapshot = computed(() => ({ eqBars: store.eqBars, isPlaying: store.isPlaying, })) -useAudioReactiveBackdrop(audioReactiveSnapshot, ambientRoot) useSmoothScroll() // alpha.28 — next-gen premium motion @@ -126,6 +124,17 @@ useKineticType(heroTitleEl) // effect is tighter and feels more designed than ambient. const heroEl = ref(null) useCursorGlow(heroEl, { radius: 360, intensity: 0.42 }) +// Round-12 FLUIDITY FIX — the audio pump used to write +// `--pulse-ambient` on the `.app` ROOT every playing frame : a custom- +// property change on the root invalidates style for the entire page +// subtree. Headed-GPU bisection at 2560×1440 measured that single +// write at ~24 ms/frame (playing p50 48.5 → 24.3 ms with the pump +// no-opped). Every live consumer (.hero__glow, .hero__backdrop, +// .hero__player::before) sits under the hero section, so the var is +// written there now — same visuals, a fraction of the subtree. (The +// FAB consumer was provably inert anyway : MiniPlayer teleports to +// , OUTSIDE the old .app root, and falls back to 0.) +useAudioReactiveBackdrop(audioReactiveSnapshot, heroEl) // Scroll parallax on hero backdrop (subtle depth) const heroBackdropEl = ref(null) useScrollParallax(heroBackdropEl, { depth: 50 }) @@ -255,11 +264,28 @@ const SHOWCASE_ACCENTS: Partial> = { const showcaseAccent = computed( () => showcaseParams()?.get('a') ?? SHOWCASE_ACCENTS[showcaseVariant.value], ) +// Round-14 shell pipeline — extra showcase params so the capture rig +// (scripts/generate-player-shells.mjs) can render every demo-card +// configuration : +// &w=480 → explicit :width passed to the player +// &bg= → customBackground (URL-encoded CSS, custom variant) +// &t=0 → track index (demo cards show track 0) +// &clean=1 → hide the showcase backdrop (alpha captures) +const showcaseWidth = computed(() => { + const w = showcaseParams()?.get('w') + return w ? Number(w) : undefined +}) +const showcaseBg = computed(() => showcaseParams()?.get('bg') ?? undefined) +const showcaseClean = computed(() => showcaseParams()?.has('clean') ?? false) // Showcase mode starts on track 2 (white "a couple of good days" cover — -// gives the backdrop a warm cream palette that reads cleanly on README). +// gives the backdrop a warm cream palette that reads cleanly on README) +// unless the capture rig pins a track via &t=. onMounted(() => { - if (showcase.value) store.loadTrack(1) + if (showcase.value) { + const t = showcaseParams()?.get('t') + store.loadTrack(t !== null && t !== undefined ? Number(t) : 1) + } }) // ─── Interactive size slider ─────────────────────────────────── @@ -895,18 +921,20 @@ const hero = computed(() => ({