diff --git a/marketing/podcast/bin/animate-pals.mjs b/marketing/podcast/bin/animate-pals.mjs old mode 100644 new mode 100755 index d3d9b5c7eb..573cf8e15c --- a/marketing/podcast/bin/animate-pals.mjs +++ b/marketing/podcast/bin/animate-pals.mjs @@ -1,36 +1,178 @@ -// animate-pals.mjs — turn a still "pals" logo tile into a seamless LOOPING -// turnaround clip with Seedance 2.0. The trick: pass the same still as both -// the start frame (image) and the end frame (endImage) so the last frame -// snaps back to the first → the object spins 360° and loops cleanly. +#!/usr/bin/env node +// animate-pals.mjs — turn still "pals" logo tiles into seamless LOOPING +// turnaround clips with Seedance 2.0, then encode each clip as an animated +// WebP (hero) and an animated PNG (APNG, for hosts that only take PNG). // -// Usage: node bin/animate-pals.mjs [slug] (default: nat-amethyst) -// → ~/Desktop/pals-turnaround-.mp4 -import { generateShot } from "../../../pop/lib/fal.mjs"; -import { homedir } from "node:os"; +// The loop trick: the same still is passed as both the start frame (image) +// and the end frame (endImage), so the last frame snaps back to the first +// and the object returns to its starting pose. The encoder drops that +// duplicated last frame so the loop has no hitch. +// +// Usage: +// node bin/animate-pals.mjs one or more pals (e.g. nat-jade chrome) +// node bin/animate-pals.mjs --all every pal in system/backend/logo.mjs +// node bin/animate-pals.mjs --missing every pal without a local .mp4 yet +// node bin/animate-pals.mjs --tray nat|cf|ig|av|materials +// +// Flags: +// --tier fast|standard Seedance tier (default standard: nicer facets/light) +// --motion - - - - - - -`; - - return { - statusCode: 200, - headers: { - "Content-Type": "text/html", - }, - body: htmlResponse, - }; - } -} - -const randomPurple = () => { - const r = Math.floor(Math.random() * 50 + 50); // Values between 200 and 255 - const g = Math.floor(Math.random() * 25); // Values between 0 and 50 - const b = Math.floor(Math.random() * 50 + 50); // Values between 200 and 255 - return `rgb(${r},${g},${b})`; -}; + + // ── named assets → CDN ────────────────────────────────────────────── + const named = assetPath.match(/^pals-([a-z0-9-]+)\.(png|webp|apng|mp4)$/); + if (named) { + const [, slug, format] = named; + const url = format === "png" ? logoUrl(slug) : turnaroundUrl(slug, format); + return url ? redirect(url) : respond(404, { error: "Pal not found." }); + } + + // ── random animated (turnaround / auth0 are legacy aliases) ───────── + const random = assetPath.match(/^(?:random|turnaround|auth0)\.(webp|apng|mp4|json)$/); + if (random) { + const format = random[1]; + if (format === "json") { + const pal = randomPal({ previous }); + return { statusCode: 200, headers: { "Content-Type": "application/json", ...CORS, ...NO_CACHE }, body: JSON.stringify(pal) }; + } + const url = turnaroundUrl(null, format); + if (!url) return respond(404, { error: "No turnarounds published." }); + return format === "mp4" ? redirect(url) : proxy(url, format); + } + + // ── random still bytes (legacy /logo.png) ─────────────────────────── + if (assetPath === "logo.png" || assetPath === "random.png" || assetPath === "still.png") { + return proxy(randomPal({ previous, animated: false }).url, "png"); + } + + // ── bare endpoint: 50/50 still or animated, fresh each hit ────────── + const pal = randomPal({ previous }); + const userAgent = event.headers["user-agent"] || ""; + const accept = event.headers["accept"] || ""; + const isServer = /curl|wget|python-requests|node-fetch/i.test(userAgent); + const wantsImage = accept.startsWith("image/") || (accept.includes("image/*") && !accept.includes("text/html")); + + if (isServer || wantsImage) return proxy(pal.url, pal.format); + + const htmlResponse = ` + + + + + pals · ${pal.slug}${pal.animated ? " ↻" : ""} + + + + + pals ${pal.slug} + + + +`; + return { statusCode: 200, headers: { "Content-Type": "text/html", ...NO_CACHE }, body: htmlResponse }; +} + +const randomPurple = () => { + const r = Math.floor(Math.random() * 50 + 50); + const g = Math.floor(Math.random() * 25); + const b = Math.floor(Math.random() * 50 + 50); + return `rgb(${r},${g},${b})`; +};