From 00b9f46f465c5c305acac57b810f8d5ab5ba3cbe Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Mon, 20 Jul 2026 22:08:32 +0000 Subject: [PATCH] menuband: add web film carousel --- pop/menuband/bin/reel-lib.mjs | 18 ++++++++++++++++-- pop/menuband/bin/sim-announce.mjs | 4 ++-- pop/menuband/bin/sim-chords.mjs | 10 +++++++--- pop/menuband/bin/sim-features.mjs | 4 ++-- pop/menuband/bin/sim-scales.mjs | 10 +++++++--- system/public/menuband/index.html | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- system/public/menuband/video/meet-menuband.mp4 | 0 system/public/menuband/video/sing-the-scale.mp4 | 0 8 file(s) changed, 167 insertion(s)(+), 14 deletion(s)(-) diff --git a/pop/menuband/bin/reel-lib.mjs b/pop/menuband/bin/reel-lib.mjs --- a/pop/menuband/bin/reel-lib.mjs +++ b/pop/menuband/bin/reel-lib.mjs @@ -22,7 +22,19 @@ const HERE = dirname(fileURLToPath(import.meta.url)); export const LANE = resolve(HERE, ".."); export const OUT = `${LANE}/out`; -export const W = 1080, H = 1920, FPS = 30; +// One campaign clock, two compositions. `--format web` keeps every score, +// capture, sung take, and word-timing sidecar intact while giving the site a +// native 4:3 frame instead of cropping the 9:16 social master. +export const FORMAT = process.argv.includes("--format") + ? process.argv[process.argv.indexOf("--format") + 1] + : "reel"; +if (!new Set(["reel", "web"]).has(FORMAT)) { + throw new Error(`unknown Menu Band format: ${FORMAT} (use reel or web)`); +} +export const IS_WEB = FORMAT === "web"; +export const W = IS_WEB ? 1440 : 1080; +export const H = IS_WEB ? 1080 : 1920; +export const FPS = 30; // macOS system fonts — registered before any canvas is created. try { registerFont("/System/Library/Fonts/SFNS.ttf", { family: "MBSans" }); } catch {} @@ -396,7 +408,9 @@ /// Shared --sung plumbing: audio/base/meta paths swing to the -sung variants. export function sungMode() { const sung = process.argv.includes("--sung"); - return { sung, suffix: sung ? "-sung" : "" }; + const audioSuffix = sung ? "-sung" : ""; + const formatSuffix = IS_WEB ? "-web" : ""; + return { sung, audioSuffix, formatSuffix, suffix: `${audioSuffix}${formatSuffix}` }; } // ── notes.json helpers ───────────────────────────────────────────────────── diff --git a/pop/menuband/bin/sim-announce.mjs b/pop/menuband/bin/sim-announce.mjs --- a/pop/menuband/bin/sim-announce.mjs +++ b/pop/menuband/bin/sim-announce.mjs @@ -28,7 +28,7 @@ const SLUG = "menuband-announce"; // --sung: jeffrey's sung vocal mix + karaoke captions (sing-jingle.mjs), // writing the -sung base/meta so the originals stay untouched. -const { sung: SUNG, suffix: VAR } = sungMode(); +const { sung: SUNG, audioSuffix: AUDIO_VAR, suffix: VAR } = sungMode(); const karaoke = SUNG ? makeKaraoke(loadSungWords(SLUG)) : null; const score = loadScore(SLUG); const TOTAL = score.durationSec; @@ -137,7 +137,7 @@ karaoke?.draw(ctx, t); } await renderVideo({ - canvas, audioPath: `${OUT}/${SLUG}${VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, + canvas, audioPath: `${OUT}/${SLUG}${AUDIO_VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, total: TOTAL, drawFrame, label: `menuband announce sim${VAR}`, }); writeMeta(`${SLUG}${VAR}`, TOTAL, SCENES); diff --git a/pop/menuband/bin/sim-chords.mjs b/pop/menuband/bin/sim-chords.mjs --- a/pop/menuband/bin/sim-chords.mjs +++ b/pop/menuband/bin/sim-chords.mjs @@ -23,12 +23,16 @@ makeStage, roundRect, text, drawDesktop, vignette, makeParticles, loadStripRig, drawStrip, stripKeyX, stripKeyColor, loadScore, leadOf, litAt, makeOnsets, renderVideo, writeMeta, makeScenes, sungMode, loadSungWords, makeKaraoke, + IS_WEB, } from "./reel-lib.mjs"; const SLUG = "menuband-chords"; // --sung: jeffrey sings the chord grammar (sing-jingle.mjs) + karaoke. -const { sung: SUNG, suffix: VAR } = sungMode(); -const karaoke = SUNG ? makeKaraoke(loadSungWords(SLUG), { y: 1920 * 0.945 }) : null; +const { sung: SUNG, audioSuffix: AUDIO_VAR, suffix: VAR } = sungMode(); +const karaoke = SUNG ? makeKaraoke(loadSungWords(SLUG), { + y: H * 0.945, + size: IS_WEB ? 38 : 54, +}) : null; const score = loadScore(SLUG); const { segs } = JSON.parse(readFileSync(`${OUT}/${SLUG}.score.json`, "utf8")); const TOTAL = score.durationSec; @@ -149,7 +153,7 @@ karaoke?.draw(ctx, t); } await renderVideo({ - canvas, audioPath: `${OUT}/${SLUG}${VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, + canvas, audioPath: `${OUT}/${SLUG}${AUDIO_VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, total: TOTAL, drawFrame, label: `menuband chords sim${VAR}`, }); writeMeta(`${SLUG}${VAR}`, TOTAL, SCENES); diff --git a/pop/menuband/bin/sim-features.mjs b/pop/menuband/bin/sim-features.mjs --- a/pop/menuband/bin/sim-features.mjs +++ b/pop/menuband/bin/sim-features.mjs @@ -27,7 +27,7 @@ } from "./reel-lib.mjs"; const SLUG = "menuband-features"; // --sung: jeffrey's sung vocal mix + karaoke captions (sing-jingle.mjs). -const { sung: SUNG, suffix: VAR } = sungMode(); +const { sung: SUNG, audioSuffix: AUDIO_VAR, suffix: VAR } = sungMode(); const karaoke = SUNG ? makeKaraoke(loadSungWords(SLUG)) : null; const score = loadScore(SLUG); const TOTAL = score.durationSec; @@ -165,7 +165,7 @@ karaoke?.draw(ctx, t); } await renderVideo({ - canvas, audioPath: `${OUT}/${SLUG}${VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, + canvas, audioPath: `${OUT}/${SLUG}${AUDIO_VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, total: TOTAL, drawFrame, label: `menuband features sim${VAR}`, }); writeMeta(`${SLUG}${VAR}`, TOTAL, SCENES); diff --git a/pop/menuband/bin/sim-scales.mjs b/pop/menuband/bin/sim-scales.mjs --- a/pop/menuband/bin/sim-scales.mjs +++ b/pop/menuband/bin/sim-scales.mjs @@ -40,10 +40,11 @@ makeStage, roundRect, text, drawDesktop, vignette, drawIcon, makeParticles, loadStripRig, drawStrip, stripKeyX, stripKeyColor, loadScore, leadOf, litAt, makeOnsets, renderVideo, writeMeta, makeScenes, sungMode, loadSungWords, makeKaraoke, + IS_WEB, } from "./reel-lib.mjs"; const SLUG = "menuband-scales"; -const { sung: SUNG, suffix: VAR } = sungMode(); +const { sung: SUNG, audioSuffix: AUDIO_VAR, suffix: VAR } = sungMode(); if (!SUNG) { console.error("✗ the scales reel only exists sung — run with --sung"); process.exit(1); @@ -56,7 +57,10 @@ const onsetsBetween = makeOnsets(lead); // spoken words → normal bottom captions; sung letters drive the big keycap const allWords = loadSungWords(SLUG); -const karaoke = makeKaraoke(allWords.filter((w) => w.spoken), { y: H * 0.925 }); +const karaoke = makeKaraoke(allWords.filter((w) => w.spoken), { + y: H * 0.94, + size: IS_WEB ? 38 : 54, +}); const { canvas, ctx } = makeStage(); const rig = await loadStripRig(); @@ -348,7 +352,7 @@ karaoke.draw(ctx, t); } await renderVideo({ - canvas, audioPath: `${OUT}/${SLUG}${VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, + canvas, audioPath: `${OUT}/${SLUG}${AUDIO_VAR}.mp3`, outPath: `${OUT}/base-${SLUG}${VAR}.mp4`, total: TOTAL, drawFrame, label: `menuband scales sim${VAR}`, }); writeMeta(`${SLUG}${VAR}`, TOTAL, SCENES); diff --git a/system/public/menuband/index.html b/system/public/menuband/index.html --- a/system/public/menuband/index.html +++ b/system/public/menuband/index.html @@ -171,6 +171,92 @@ drop-shadow(0 2px 3px rgba(15, 30, 60, 0.20)) drop-shadow(0 14px 18px rgba(15, 30, 60, 0.28)); } + /* The icon is the door into the campaign films. The player keeps the + native 4:3 web composition; the icon floats over its poster until the + visitor asks for sound. */ + .film-stage { + position: relative; + aspect-ratio: 4 / 3; + width: 100%; + margin: 0 auto 18px; + overflow: hidden; + border-radius: 20px; + background: #cfc5df; + box-shadow: 0 18px 38px -18px rgba(34, 22, 62, 0.55); + } + .film-stage video { + width: 100%; + height: 100%; + display: block; + object-fit: cover; + } + .film-door { + position: absolute; + inset: 0; + border: 0; + padding: 0; + display: grid; + place-items: center; + background: rgba(224, 217, 238, 0.18); + cursor: pointer; + transition: opacity 300ms var(--ease-apple), background 300ms var(--ease-apple); + } + .film-door img { + width: 42%; + height: auto; + filter: drop-shadow(0 16px 22px rgba(29, 14, 50, 0.35)); + transition: transform 300ms var(--ease-apple); + } + .film-door::after { + content: "▶"; + position: absolute; + width: 48px; + height: 48px; + display: grid; + place-items: center; + padding-left: 3px; + border-radius: 50%; + color: white; + font-size: 18px; + background: rgba(20, 18, 28, 0.78); + box-shadow: 0 5px 18px rgba(20, 18, 28, 0.35); + } + .film-door:hover { background: rgba(255, 255, 255, 0.2); } + .film-door:hover img { transform: scale(1.035) rotate(-1deg); } + .film-stage.is-playing .film-door { opacity: 0; pointer-events: none; } + + .film-rail { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(158px, 1fr); + gap: 9px; + margin: -4px 0 22px; + overflow-x: auto; + padding: 4px 2px 10px; + scroll-snap-type: x mandatory; + scrollbar-width: thin; + } + .film-card { + appearance: none; + scroll-snap-align: start; + border: 1px solid rgba(255, 255, 255, 0.72); + border-radius: 12px; + padding: 10px 11px; + text-align: left; + color: var(--ink-head); + background: rgba(255, 255, 255, 0.48); + box-shadow: 0 4px 12px rgba(25, 20, 45, 0.08); + cursor: pointer; + } + .film-card[aria-current="true"] { + outline: 2px solid var(--tahoe-blue); + outline-offset: 1px; + background: rgba(255, 255, 255, 0.75); + } + .film-card b, .film-card small { display: block; } + .film-card b { font-family: var(--display-stack); font-size: 12px; } + .film-card small { margin-top: 2px; color: var(--ink-soft); font-size: 10px; } + /* Apple Garamond italic — the "Think different" / classic-product wordmark feel. Falls back through ITC Garamond, Hoefler Text, and Garamond on stock macOS. */ @@ -495,6 +581,8 @@ body { padding: 16px 8px 48px; } .hero { padding: 24px 18px 20px; border-radius: 14px; } .icon-stage { width: 192px; height: 192px; } .icon-stage img.app-icon { width: 192px; height: 192px; } + .film-stage { border-radius: 14px; } + .film-rail { grid-auto-columns: 72%; } h2.app-title { font-size: 40px; } section.panel { padding: 16px 18px; border-radius: 12px; } } @@ -543,6 +631,8 @@ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 12px 32px -14px rgba(0, 0, 0, 0.55); } + .film-card { background: rgba(28, 28, 30, 0.62); border-color: rgba(255,255,255,0.09); } + .film-card[aria-current="true"] { background: rgba(44, 44, 48, 0.86); } code { background: #2c2c2e; border-color: #3a3a3c; @@ -633,8 +723,21 @@
-
- Menu Band app icon +
+ + +
+
+ +

Menu Band

Taking your Mac's standard instruments out of the 🎸 Garage and kickin' it on the curb!

@@ -691,6 +794,34 @@
Aesthetic.Computer