diff --git a/launch-video/README.md b/launch-video/README.md new file mode 100644 index 0000000..ae3c5c5 --- /dev/null +++ b/launch-video/README.md @@ -0,0 +1,40 @@ +# WhatStack — launch film + +![Launch film preview](preview.gif) + +The GIF above is a silent, low-resolution preview. The full films are: + +- **[whatstack-launch.mp4](whatstack-launch.mp4)**: landscape, 1920×1080, for X, LinkedIn and YouTube +- **[whatstack-launch-vertical.mp4](whatstack-launch-vertical.mp4)**: vertical, 1080×1920, for Reels, Shorts and TikTok + +Both are 32 seconds at 60fps (1,920 frames), with an original score. Poster frames: [landscape](whatstack-launch.jpg), [vertical](whatstack-launch-vertical.jpg). + +Vertical preview + +## Beats +| Time | Beat | Real footage | +|---|---|---| +| 0–3s | *What's this site built with?* | stripe.com's real HTML source, blurred and scrolling | +| 3–5s | Reveal | Logo, **WhatStack**, *What's under this page?* | +| 5–9s | *It reads the stack while you browse.* | nextjs.org → notion.com → stripe.com, each with its real toolbar badge (4, 4, 4) | +| 9–13s | *One click. The whole stack.* | The icon is clicked, and the real stripe.com popup shows *Next.js 14.2.32 · React · Webpack* | +| 13–16s | *Confidence, not guesses.* | HIGH and MEDIUM badges light up, then GitHub's real *Lower confidence* section | +| 16–19s | *See why it matched.* | The real Next.js evidence (scripts, globals and DOM) | +| 19–23s | *Any site. Instantly.* | Real popups for notion.com, nextjs.org, react.dev, netflix.com, airbnb.com and vuejs.org | +| 23–26s | *Copy as text, Markdown or JSON.* | The real MD and JSON output from the Copy buttons | +| 26–28s | *Runs entirely on your device.* | The real *Local only* pill. Sizes are from the store listing (49 KB) | +| 28–32s | CTA | The real Chrome Web Store header (logged out), clicking *Add to Chrome*, then the end card | + +## How it's made +It's rendered frame by frame from code, with no motion design app. + +- [`source/capture.mjs`](source/capture.mjs) loads the **unpacked extension (v1.7.2)** into Chromium with Playwright, visits each site, reads the real badge, and opens the real `popup.html` against the real service worker and a real deep scan at 3× resolution. The only change is that `chrome.tabs.query` is pointed at the site's tab, because a popup opened as a tab would otherwise scan itself. Copy output is recorded from the real Copy / MD / JSON buttons. +- [`source/timeline.json`](source/timeline.json) sets every scene and cue in **beats** (120 BPM). The picture and the sound both read from it. +- [`source/engine.js`](source/engine.js) is a small animation engine built on [skia-canvas](https://skia-canvas.org). Every element is a pure function of time. The browser window is drawn as a 3D plane with true perspective, and each output frame averages **10 renders** for motion blur, which never spans a hard cut. +- [`source/render.js`](source/render.js) splits the frames across 8 parallel workers and pipes them into FFmpeg. [`source/score.py`](source/score.py) generates the music and UI sounds from the same timeline. + +## Notes +- The browser window is a simple generic frame, not Chrome's real toolbar. It shows only the real WhatStack icon and badge. +- **Badge vs popup count:** the badge comes from the light scan when the page loads. The popup runs a deep scan, which can find more: on stripe.com the badge shows 4, but the popup shows "5 solid". Once the popup opens, the badge updates to 5. Both numbers are filmed exactly as the extension shows them. +- Linear is not included, because it reported Next.js "v1.0.0-beta.9", which looks like a detection bug. +- No technology count or user count is claimed. diff --git a/launch-video/preview-vertical.gif b/launch-video/preview-vertical.gif new file mode 100644 index 0000000..3f9206e Binary files /dev/null and b/launch-video/preview-vertical.gif differ diff --git a/launch-video/preview.gif b/launch-video/preview.gif new file mode 100644 index 0000000..249c03f Binary files /dev/null and b/launch-video/preview.gif differ diff --git a/launch-video/source/cap/meta.json b/launch-video/source/cap/meta.json new file mode 100644 index 0000000..9ece851 --- /dev/null +++ b/launch-video/source/cap/meta.json @@ -0,0 +1,1469 @@ +{ + "stripe": { + "url": "https://stripe.com/", + "light": { + "badge": "4", + "title": "WhatStack: Next.js · 4 signals" + }, + "deep": { + "badge": "5", + "title": "WhatStack: Next.js · 5 signals" + }, + "popup": { + "nat": 560, + "status": "5 solid · 5 total · deep · Next.js", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nNext.js 14.2.32 · React · Webpack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 111.9375 + }, + { + "title": "BUILD", + "x": 10, + "y": 304.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "OBSERVABILITY", + "x": 10, + "y": 385.578125, + "w": 340, + "h": 72.9375 + }, + { + "title": "ANALYTICS & PRODUCT", + "x": 10, + "y": 466.515625, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Next.js v14.2.32 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React HIGH", + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 266.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Webpack HIGH", + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 347.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Sentry HIGH", + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 428.265625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Google Analytics MEDIUM", + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38, + "badge": { + "x": 279.28125, + "y": 509.203125, + "w": 59.71875, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Next.js\nv14.2.32\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React\nHIGH": { + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38 + }, + "Webpack\nHIGH": { + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38 + }, + "Sentry\nHIGH": { + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38 + }, + "Google Analytics\nMEDIUM": { + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 570.453125, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 570.453125, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 570.453125, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 570.453125, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 574.703125, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://stripe.com/", + "H": 613, + "expanded": { + "status": "5 solid · 5 total · deep · Next.js", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nNext.js 14.2.32 · React · Webpack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 299.375 + }, + { + "title": "BUILD", + "x": 10, + "y": 492.078125, + "w": 340, + "h": 72.9375 + }, + { + "title": "OBSERVABILITY", + "x": 10, + "y": 573.015625, + "w": 340, + "h": 72.9375 + }, + { + "title": "ANALYTICS & PRODUCT", + "x": 10, + "y": 653.953125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Next.js v14.2.32 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React HIGH", + "x": 11, + "y": 445.078125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 453.828125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Webpack HIGH", + "x": 11, + "y": 526.015625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 534.765625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Sentry HIGH", + "x": 11, + "y": 606.953125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 615.703125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Google Analytics MEDIUM", + "x": 11, + "y": 687.890625, + "w": 338, + "h": 38, + "badge": { + "x": 279.28125, + "y": 696.640625, + "w": 59.71875, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [ + { + "x": 11, + "y": 267.640625, + "w": 338, + "h": 165.4375 + } + ], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Next.js\nv14.2.32\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React\nHIGH": { + "x": 11, + "y": 445.078125, + "w": 338, + "h": 38 + }, + "Webpack\nHIGH": { + "x": 11, + "y": 526.015625, + "w": 338, + "h": 38 + }, + "Sentry\nHIGH": { + "x": 11, + "y": 606.953125, + "w": 338, + "h": 38 + }, + "Google Analytics\nMEDIUM": { + "x": 11, + "y": 687.890625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 757.890625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 757.890625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 757.890625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 757.890625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 762.140625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://stripe.com/", + "H": 801, + "evidenceText": [ + "scripthttps://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/polyfills-42372ed130431b0a.js", + "domscript#__NEXT_DATA__", + "globalwindow.__NEXT_DATA__", + "globalwindow.webpackChunk_N_E", + "globalwindow.next v14.2.32", + "domnext-route-announcer", + "dom#__next" + ] + }, + "exports": { + "text": "WhatStack for https://stripe.com/:\nStack: Next.js 14.2.32 · React · Webpack\nFrameworks: Next.js@14.2.32 (high), React (high)\nBuild: Webpack (high)\nObservability: Sentry (high)\nAnalytics & product: Google Analytics (medium)", + "textStatus": "Copied text summary", + "md": "## WhatStack\n**URL:** https://stripe.com/\n**Stack:** Next.js 14.2.32 · React · Webpack\n| Tech | Category | Confidence | Version |\n| --- | --- | --- | --- |\n| Next.js | Frameworks | high | 14.2.32 |\n| React | Frameworks | high | — |\n| Webpack | Build | high | — |\n| Sentry | Observability | high | — |\n| Google Analytics | Analytics & product | medium | — |", + "mdStatus": "Copied Markdown", + "json": "{\n \"tool\": \"WhatStack\",\n \"url\": \"https://stripe.com/\",\n \"pass\": \"deep\",\n \"headline\": \"Next.js 14.2.32 · React · Webpack\",\n \"primary\": {\n \"id\": \"nextjs\",\n \"name\": \"Next.js\"\n },\n \"hits\": [\n {\n \"id\": \"nextjs\",\n \"name\": \"Next.js\",\n \"category\": \"framework\",\n \"confidence\": \"high\",\n \"version\": \"14.2.32\",\n \"evidence\": [\n {\n \"type\": \"script\",\n \"snippet\": \"https://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/polyfills-42372ed130431b0a.js\",\n \"weight\": 5\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"script#__NEXT_DATA__\",\n \"weight\": 5\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.__NEXT_DATA__\",\n \"weight\": 5\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.webpackChunk_N_E\",\n \"weight\": 4\n },\n {\n \"type\": \"global\",\n \"snippet\": \"window.next v14.2.32\",\n \"weight\": 2\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"next-route-announcer\",\n \"weight\": 4\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"#__next\",\n \"weight\": 2\n }\n ]\n },\n {\n \"id\": \"react\",\n \"name\": \"React\",\n \"category\": \"framework\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.__reactContainer\",\n \"weight\": 5\n },\n {\n \"type\": \"dom\",\n \"snippet\": \"confirmed via Next.js stack\",\n \"weight\": 3\n }\n ]\n },\n {\n \"id\": \"webpack\",\n \"name\": \"Webpack\",\n \"category\": \"build\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.webpackChunk\",\n \"weight\": 4\n },\n {\n \"type\": \"script\",\n \"snippet\": \"https://b.stripecdn.com/mkt-ssr-statics/assets/_next/static/chunks/webpack-f85e96e588c53278.js\",\n \"weight\": 3\n }\n ]\n },\n {\n \"id\": \"sentry\",\n \"name\": \"Sentry\",\n \"category\": \"observability\",\n \"confidence\": \"high\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"global\",\n \"snippet\": \"window.__SENTRY__\",\n \"weight\": 5\n }\n ]\n },\n {\n \"id\": \"google-analytics\",\n \"name\": \"Google Analytics\",\n \"category\": \"analytics\",\n \"confidence\": \"medium\",\n \"version\": null,\n \"evidence\": [\n {\n \"type\": \"cookie\",\n \"snippet\": \"_ga\",\n \"weight\": 2\n }\n ]\n }\n ]\n}", + "jsonStatus": "Copied JSON" + } + } + }, + "notion": { + "url": "https://www.notion.com/", + "light": { + "badge": "4", + "title": "WhatStack: Next.js · 4 signals" + }, + "deep": { + "badge": "5", + "title": "WhatStack: Next.js · 5 signals" + }, + "popup": { + "nat": 560, + "status": "5 solid · 5 total · deep · Next.js", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nNext.js 16.3.2 · React · Turbopack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 111.9375 + }, + { + "title": "BUILD", + "x": 10, + "y": 304.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "OBSERVABILITY", + "x": 10, + "y": 385.578125, + "w": 340, + "h": 72.9375 + }, + { + "title": "ANALYTICS & PRODUCT", + "x": 10, + "y": 466.515625, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Next.js v16.3.2 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React HIGH", + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 266.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Turbo Turbopack HIGH", + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 347.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Sentry HIGH", + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 428.265625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Google Analytics HIGH", + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 509.203125, + "w": 42.03125, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Next.js\nv16.3.2\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React\nHIGH": { + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38 + }, + "Turbo\nTurbopack\nHIGH": { + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38 + }, + "Sentry\nHIGH": { + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38 + }, + "Google Analytics\nHIGH": { + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 570.453125, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 570.453125, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 570.453125, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 570.453125, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 574.703125, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://www.notion.com/", + "H": 613 + } + }, + "nextjs": { + "url": "https://nextjs.org/", + "light": { + "badge": "4", + "title": "WhatStack: Next.js · 4 signals" + }, + "deep": { + "badge": "4", + "title": "WhatStack: Next.js · 4 signals" + }, + "popup": { + "nat": 533, + "status": "4 solid · 4 total · deep · Next.js", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nNext.js 16.4.0-canary.38 · React · Turbopack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 111.9375 + }, + { + "title": "BUILD", + "x": 10, + "y": 304.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "UI / CSS", + "x": 10, + "y": 385.578125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Next.js v16.4.0-canary.38 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React HIGH", + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 266.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Turbo Turbopack HIGH", + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 347.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Tailwind CSS MEDIUM", + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38, + "badge": { + "x": 279.28125, + "y": 428.265625, + "w": 59.71875, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Next.js\nv16.4.0-canary.38\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React\nHIGH": { + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38 + }, + "Turbo\nTurbopack\nHIGH": { + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38 + }, + "Tailwind CSS\nMEDIUM": { + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 489.515625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 489.515625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 489.515625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 489.515625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 493.765625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://nextjs.org/", + "H": 533 + } + }, + "netflix": { + "url": "https://www.netflix.com/", + "light": { + "badge": "1", + "title": "WhatStack: 1 stack signals" + }, + "deep": { + "badge": "3", + "title": "WhatStack: React · 3 signals" + }, + "popup": { + "nat": 494, + "status": "3 solid · 3 total · deep · React", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nReact · Webpack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 72.9375 + }, + { + "title": "BUILD", + "x": 10, + "y": 265.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "UI / CSS", + "x": 10, + "y": 346.578125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "React HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Webpack HIGH", + "x": 11, + "y": 299.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 308.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "💅 Emotion HIGH", + "x": 11, + "y": 380.515625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 389.265625, + "w": 42.03125, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "React\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "Webpack\nHIGH": { + "x": 11, + "y": 299.578125, + "w": 338, + "h": 38 + }, + "💅\nEmotion\nHIGH": { + "x": 11, + "y": 380.515625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 450.515625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 450.515625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 450.515625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 450.515625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 454.765625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://www.netflix.com/", + "H": 494 + } + }, + "react": { + "url": "https://react.dev/", + "light": { + "badge": "5", + "title": "WhatStack: Next.js · 5 signals" + }, + "deep": { + "badge": "5", + "title": "WhatStack: Next.js · 5 signals" + }, + "popup": { + "nat": 560, + "status": "5 solid · 5 total · deep · Next.js", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nNext.js 15.1.12 · React · Webpack", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 111.9375 + }, + { + "title": "BUILD", + "x": 10, + "y": 304.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "UI / CSS", + "x": 10, + "y": 385.578125, + "w": 340, + "h": 72.9375 + }, + { + "title": "ANALYTICS & PRODUCT", + "x": 10, + "y": 466.515625, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Next.js v15.1.12 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React HIGH", + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 266.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Webpack HIGH", + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 347.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Tailwind CSS MEDIUM", + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38, + "badge": { + "x": 279.28125, + "y": 428.265625, + "w": 59.71875, + "h": 20.5 + } + }, + { + "text": "Google Analytics HIGH", + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 509.203125, + "w": 42.03125, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Next.js\nv15.1.12\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React\nHIGH": { + "x": 11, + "y": 257.640625, + "w": 338, + "h": 38 + }, + "Webpack\nHIGH": { + "x": 11, + "y": 338.578125, + "w": 338, + "h": 38 + }, + "Tailwind CSS\nMEDIUM": { + "x": 11, + "y": 419.515625, + "w": 338, + "h": 38 + }, + "Google Analytics\nHIGH": { + "x": 11, + "y": 500.453125, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 570.453125, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 570.453125, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 570.453125, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 570.453125, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 574.703125, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://react.dev/", + "H": 613 + } + }, + "vuejs": { + "url": "https://vuejs.org/", + "light": { + "badge": "", + "title": "WhatStack: no high/medium signals" + }, + "deep": { + "badge": "1", + "title": "WhatStack: Vue · 1 signals" + }, + "popup": { + "nat": 332, + "status": "1 solid · 1 total · deep · Vue", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nVue 3.5.42", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Vue v3.5.42 HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Vue\nv3.5.42\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 288.640625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 288.640625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 288.640625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 288.640625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 292.890625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://vuejs.org/", + "H": 332 + } + }, + "airbnb": { + "url": "https://www.airbnb.com/", + "light": { + "badge": "1", + "title": "WhatStack: 1 stack signals" + }, + "deep": { + "badge": "3", + "title": "WhatStack: React · 3 signals" + }, + "popup": { + "nat": 494, + "status": "3 solid · 3 total · deep · React", + "headline": { + "x": 10, + "y": 116.015625, + "w": 340, + "h": 58.6875 + }, + "headlineText": "CORE STACK\n\nReact", + "sections": [ + { + "title": "FRAMEWORKS", + "x": 10, + "y": 184.703125, + "w": 340, + "h": 72.9375 + }, + { + "title": "DATA", + "x": 10, + "y": 265.640625, + "w": 340, + "h": 72.9375 + }, + { + "title": "ANALYTICS & PRODUCT", + "x": 10, + "y": 346.578125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "React HIGH", + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 227.390625, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "React Router HIGH", + "x": 11, + "y": 299.578125, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 308.328125, + "w": 42.03125, + "h": 20.5 + } + }, + { + "text": "Google Analytics MEDIUM", + "x": 11, + "y": 380.515625, + "w": 338, + "h": 38, + "badge": { + "x": 279.28125, + "y": 389.265625, + "w": 59.71875, + "h": 20.5 + } + } + ], + "low": null, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "React\nHIGH": { + "x": 11, + "y": 218.640625, + "w": 338, + "h": 38 + }, + "React Router\nHIGH": { + "x": 11, + "y": 299.578125, + "w": 338, + "h": 38 + }, + "Google Analytics\nMEDIUM": { + "x": 11, + "y": 380.515625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 450.515625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 450.515625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 450.515625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 450.515625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 454.765625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://www.airbnb.com/", + "H": 494 + } + }, + "github": { + "url": "https://github.com/", + "light": { + "badge": "", + "title": "WhatStack: no high/medium signals" + }, + "deep": { + "badge": "1", + "title": "WhatStack: 1 stack signals" + }, + "popup": { + "nat": 367, + "status": "1 solid · 2 total · deep", + "headline": { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + }, + "headlineText": "\n Core stack\n \n ", + "sections": [ + { + "title": "BUILD", + "x": 10, + "y": 116.015625, + "w": 340, + "h": 96.4375 + }, + { + "title": "DATA", + "x": 10, + "y": 220.453125, + "w": 340, + "h": 72.9375 + } + ], + "hits": [ + { + "text": "Import Maps LOW", + "x": 11, + "y": 173.453125, + "w": 338, + "h": 38, + "badge": { + "x": 300.8125, + "y": 182.203125, + "w": 38.1875, + "h": 20.5 + } + }, + { + "text": "React Router v8.3.0 HIGH", + "x": 11, + "y": 254.390625, + "w": 338, + "h": 38, + "badge": { + "x": 296.96875, + "y": 263.140625, + "w": 42.03125, + "h": 20.5 + } + } + ], + "low": { + "x": 11, + "y": 149.953125, + "w": 338, + "h": 23.5 + }, + "evidence": [], + "buttons": { + "Rescan": { + "x": 282.609375, + "y": 15.34375, + "w": 63.390625, + "h": 31 + }, + "Import Maps\nLOW": { + "x": 11, + "y": 173.453125, + "w": 338, + "h": 38 + }, + "React Router\nv8.3.0\nHIGH": { + "x": 11, + "y": 254.390625, + "w": 338, + "h": 38 + }, + "Copy": { + "x": 14, + "y": 324.390625, + "w": 50.828125, + "h": 31 + }, + "MD": { + "x": 70.828125, + "y": 324.390625, + "w": 41.984375, + "h": 31 + }, + "JSON": { + "x": 118.8125, + "y": 324.390625, + "w": 51.3125, + "h": 31 + }, + "Wrong?": { + "x": 178.125, + "y": 324.390625, + "w": 66.46875, + "h": 31 + } + }, + "local": { + "x": 281.203125, + "y": 328.640625, + "w": 64.796875, + "h": 22.5 + }, + "url": "https://github.com/", + "H": 367 + } + }, + "store": { + "add": { + "x": 1109, + "y": 262, + "w": 147, + "h": 40 + }, + "addText": "Add to Chrome", + "title": { + "x": 260, + "y": 261.5, + "w": 724, + "h": 41 + } + } +} \ No newline at end of file diff --git a/launch-video/source/capture.mjs b/launch-video/source/capture.mjs new file mode 100644 index 0000000..c9cffb2 --- /dev/null +++ b/launch-video/source/capture.mjs @@ -0,0 +1,78 @@ +// Real captures of the unpacked WhatStack extension (v1.7.2) on real sites. +// Popup: the real popup.html + real service worker + real deep scan. Only chrome.tabs.query is pointed at the target tab +// (a popup opened as a tab would otherwise scan itself). Clipboard writes are recorded so exports are the real output. +import {chromium} from '/projects/sandbox/tools/node_modules/playwright/index.mjs'; +import fs from 'fs';import os from 'os';import path from 'path'; +const EXT='/projects/sandbox/whatstack';const OUT='/projects/sandbox/whatstack-video/cap/';fs.mkdirSync(OUT,{recursive:true}); +const only=process.argv[2]?process.argv[2].split(','):null; +const ctx=await chromium.launchPersistentContext(fs.mkdtempSync(path.join(os.tmpdir(),'ws-')),{channel:'chromium',headless:true, + viewport:{width:1440,height:900},deviceScaleFactor:3,locale:'en-US',colorScheme:'dark', + args:[`--disable-extensions-except=${EXT}`,`--load-extension=${EXT}`,'--disable-features=DisableLoadExtensionCommandLineSwitch','--no-sandbox','--no-first-run']}); +const sw=ctx.serviceWorkers()[0]||await ctx.waitForEvent('serviceworker',{timeout:20000}); +const id=new URL(sw.url()).host; +const meta=fs.existsSync(OUT+'meta.json')?JSON.parse(fs.readFileSync(OUT+'meta.json')):{}; +const R=b=>b&&{x:+b.x.toFixed(1),y:+b.y.toFixed(1),w:+b.width.toFixed(1),h:+b.height.toFixed(1)}; +async function dismiss(page){ + for(const re of [/^Accept all( cookies)?$/i,/^Accept$/i,/^Allow all( cookies)?$/i,/^I agree$/i,/^Got it$/i,/^OK$/i,/^Accept All Cookies$/i,/^Only allow essential cookies$/i,/^Reject all$/i,/^Close$/i]){ + const b=page.getByRole('button',{name:re}).first();if(await b.isVisible().catch(()=>false)){await b.click({timeout:2000}).catch(()=>{});await page.waitForTimeout(500);}} +} +async function popupFor(tab,name,{expand=null,exports=false}={}){ + const pop=await ctx.newPage();await pop.setViewportSize({width:360,height:600}); + await pop.addInitScript(t=>{const q=chrome.tabs.query.bind(chrome.tabs);chrome.tabs.query=async o=>(o&&o.active)?[{id:t.id,url:t.url,active:true}]:q(o); + window.__copied=[];const w=navigator.clipboard&&navigator.clipboard.writeText.bind(navigator.clipboard); + navigator.clipboard.writeText=async s=>{window.__copied.push(s);try{await w(s)}catch(e){}};},tab); + await pop.goto(`chrome-extension://${id}/popup/popup.html`); + await pop.waitForFunction(()=>!document.querySelector('#results')?.hidden||!document.querySelector('#empty')?.hidden,null,{timeout:30000}).catch(()=>{}); + await pop.waitForTimeout(800); + // natural (capped, like the real popup) + full-height versions + const natH=await pop.evaluate(()=>Math.min(document.body.scrollHeight,560));await pop.setViewportSize({width:360,height:natH}); + await pop.screenshot({path:OUT+`pop-${name}-nat.png`,scale:'device'}); + await pop.addStyleTag({content:'body{max-height:none!important;overflow:visible!important}.header,.footer{position:static!important}'}); + const boxes=async()=>pop.evaluate(()=>{const R=e=>{const b=e.getBoundingClientRect();return {x:b.x,y:b.y,w:b.width,h:b.height}}; + return {status:document.querySelector('#status')?.innerText,headline:R(document.querySelector('#headline')),headlineText:document.querySelector('#headline')?.innerText, + sections:[...document.querySelectorAll('.section')].map(s=>({title:s.querySelector('.section-title')?.innerText,...R(s)})), + hits:[...document.querySelectorAll('.hit-row')].map(h=>({text:h.innerText.replace(/\s+/g,' '),...R(h),badge:R(h.querySelector('.badge,[class*=badge],[class*=conf]')||h.lastElementChild)})), + low:(()=>{const e=[...document.querySelectorAll('*')].find(e=>e.children.length===0&&/Lower confidence/i.test(e.textContent));return e?R(e):null})(), + evidence:[...document.querySelectorAll('.hit.open .evidence')].map(R), + buttons:Object.fromEntries([...document.querySelectorAll('button')].map(b=>[b.innerText.trim()||b.title,R(b)])), + local:(()=>{const e=[...document.querySelectorAll('*')].find(e=>e.children.length===0&&/^Local only$/.test(e.textContent.trim()));return e?R(e):null})(), + url:document.querySelector('#page-url')?.innerText,H:document.body.scrollHeight}}); + let h=await pop.evaluate(()=>document.body.scrollHeight);await pop.setViewportSize({width:360,height:h});await pop.waitForTimeout(200); + const m={nat:natH,...await boxes()};await pop.screenshot({path:OUT+`pop-${name}.png`,scale:'device'}); + if(expand){const row=pop.locator('.hit-row',{hasText:expand}).first();await row.click();await pop.waitForTimeout(400); + h=await pop.evaluate(()=>document.body.scrollHeight);await pop.setViewportSize({width:360,height:h});await pop.waitForTimeout(200); + m.expanded=await boxes();m.expanded.evidenceText=await pop.evaluate(()=>[...document.querySelectorAll('.hit.open .evidence li')].map(l=>l.innerText)); + await pop.screenshot({path:OUT+`pop-${name}-open.png`,scale:'device'});await row.click();await pop.waitForTimeout(300);} + if(exports){m.exports={};for(const [label,key] of [['Copy','text'],['MD','md'],['JSON','json']]){ + await pop.getByRole('button',{name:label,exact:true}).first().click().catch(e=>console.log('btn',label,e.message.slice(0,80)));await pop.waitForTimeout(400); + m.exports[key]=await pop.evaluate(()=>window.__copied[window.__copied.length-1]||null);m.exports[key+'Status']=await pop.evaluate(()=>document.querySelector('#status')?.innerText);}} + await pop.close();return m; +} +const sites=[['stripe','https://stripe.com/'],['notion','https://www.notion.com/'],['nextjs','https://nextjs.org/'],['netflix','https://www.netflix.com/'], + ['react','https://react.dev/'],['vuejs','https://vuejs.org/'],['airbnb','https://www.airbnb.com/'],['github','https://github.com/']]; +for(const [name,url] of sites){ if(only&&!only.includes(name))continue; + const page=await ctx.newPage(); + try{await page.goto(url,{waitUntil:'domcontentloaded',timeout:60000});}catch(e){console.log(name,'nav fail');continue;} + await page.waitForTimeout(4000);await dismiss(page);await page.waitForTimeout(2500);await page.mouse.move(2,2); + await page.screenshot({path:OUT+`page-${name}.jpg`,type:'jpeg',quality:90}); + const host=new URL(page.url()).host; + const tab=await sw.evaluate(async h=>{const t=(await chrome.tabs.query({})).find(t=>t.url&&new URL(t.url).host===h);return {id:t.id,url:t.url}},host); + const light=await sw.evaluate(async i=>({badge:await chrome.action.getBadgeText({tabId:i}),title:await chrome.action.getTitle({tabId:i})}),tab.id); + const pm=await popupFor(tab,name,{expand:name==='stripe'?'Next.js':null,exports:name==='stripe'}); + const deep=await sw.evaluate(async i=>({badge:await chrome.action.getBadgeText({tabId:i}),title:await chrome.action.getTitle({tabId:i})}),tab.id); + meta[name]={url:page.url(),light,deep,popup:pm}; + console.log(name,'| light',light.badge,'| deep',deep.badge,'|',pm.status,'|',(pm.headlineText||'').replace(/\s+/g,' ')); + if(name==='stripe'){const r=await ctx.request.get(url,{headers:{'user-agent':'Mozilla/5.0'}});fs.writeFileSync(OUT+'src-stripe.html',await r.text());} + await page.close(); +} +// Chrome Web Store listing (logged out) — top of page only +if(!only||only.includes('store')){ + const st=await ctx.newPage(); + await st.goto('https://chromewebstore.google.com/detail/whatstack/kpmbanlddakoocgimdenfeppfaidmcgk?hl=en',{waitUntil:'networkidle',timeout:60000}).catch(()=>{}); + await st.waitForTimeout(3000);await st.getByRole('button',{name:/No thanks/i}).first().click({timeout:3000}).catch(()=>{});await st.waitForTimeout(600);await dismiss(st);await st.mouse.move(2,2); + const add=st.getByRole('button',{name:/Add to Chrome|Remove from Chrome/}).first(); + meta.store={add:R(await add.boundingBox().catch(()=>null)),addText:await add.innerText().catch(()=>null), + title:R(await st.getByRole('heading',{name:'WhatStack'}).first().boundingBox().catch(()=>null))}; + await st.screenshot({path:OUT+'store.jpg',type:'jpeg',quality:90});console.log('store',meta.store);await st.close(); +} +fs.writeFileSync(OUT+'meta.json',JSON.stringify(meta,null,1));await ctx.close(); diff --git a/launch-video/source/engine.js b/launch-video/source/engine.js new file mode 100644 index 0000000..d8609c2 --- /dev/null +++ b/launch-video/source/engine.js @@ -0,0 +1,297 @@ +// WhatStack launch film — beat-timed animation engine on Skia (skia-canvas). +// Every element is a pure function of b (time in beats). timeline.json drives picture and score. +// FMT=vertical renders 1080×1920; default 1920×1080. +const {Canvas,FontLibrary,loadImage,Path2D}=require('/projects/sandbox/tools/node_modules/skia-canvas'); +const fs=require('fs'); +const ROOT=__dirname+'/',CAP=ROOT+'cap/',ICONS='/projects/sandbox/whatstack/icons/'; +const TL=JSON.parse(fs.readFileSync(ROOT+'timeline.json'));const S=TL.scenes,C=TL.cues; +const META=JSON.parse(fs.readFileSync(CAP+'meta.json')); +const V=process.env.FMT==='vertical'; +const W=V?1080:1920,H=V?1920:1080,CX=W/2,CY=H/2;const pick=(h,v)=>V?v:h; +FontLibrary.use('Inter',[ROOT+'fonts/Inter.ttf']);FontLibrary.use('JBMono',[ROOT+'fonts/JetBrainsMono.ttf']); + +// ---------- math +const clamp=(x,a=0,b=1)=>Math.min(b,Math.max(a,x)),seg=(t,a,b)=>clamp((t-a)/(b-a)),lerp=(a,b,x)=>a+(b-a)*x; +const eo=x=>1-Math.pow(1-x,3),eo5=x=>1-Math.pow(1-x,5),eio=x=>x<.5?4*x*x*x:1-Math.pow(-2*x+2,3)/2; +const eback=x=>{const c=1.4;return 1+(c+1)*Math.pow(x-1,3)+c*Math.pow(x-1,2)}; +const inS=(b,[a,z])=>b>=a&&b>>0;return()=>{s=(s*1664525+1013904223)>>>0;return s/4294967296;}} +function rot([x,y,z],rx,ry,rz){let c=Math.cos(rz*D2R),s=Math.sin(rz*D2R);[x,y]=[x*c-y*s,x*s+y*c]; + c=Math.cos(ry*D2R);s=Math.sin(ry*D2R);[x,z]=[x*c+z*s,-x*s+z*c];c=Math.cos(rx*D2R);s=Math.sin(rx*D2R);[y,z]=[y*c-z*s,y*s+z*c];return [x,y,z];} +const PERS=2400; +function project([X,Y,Z]){const k=PERS/(PERS-Z);return [CX+(X-CX)*k,CY+(Y-CY)*k,k];} +function planePt(cam,u,v){const s=cam.s??1;const p=rot([(u-cam.fx)*s,(v-cam.fy)*s,0],cam.rx||0,cam.ry||0,cam.rz||0);return project([(cam.cx??CX)+p[0],(cam.cy??CY)+p[1],p[2]]);} +function camLerp(a,b,x){const o={};for(const k in a)o[k]=lerp(a[k],b[k]??a[k],x);return o;} + +// ---------- brand +const TEAL='#2dd4bf',INDIGO='#6366f1',VIOLET='#c084fc',BLUE='#3b82f6',BADGE='#2563eb'; +const BG='#0c0f14'; +function grad(x,x0,x1){const g=x.createLinearGradient(x0,0,x1,0);g.addColorStop(0,TEAL);g.addColorStop(.5,'#818cf8');g.addColorStop(1,VIOLET);return g;} + +// ---------- assets +const A={};const SITES=['stripe','notion','nextjs','netflix','react','vuejs','airbnb','github']; +async function load(){ + for(const s of SITES){A['page-'+s]=await loadImage(CAP+`page-${s}.jpg`);A['pop-'+s]=await loadImage(CAP+`pop-${s}.png`);} + A['pop-stripe-open']=await loadImage(CAP+'pop-stripe-open.png'); + A.store=await loadImage(CAP+'store.jpg');A.logo=await loadImage(ICONS+'logo-512.png');A.icon=await loadImage(ICONS+'icon128.png'); + // hook: the real HTML source of stripe.com, set in mono and pre-blurred + const html=fs.readFileSync(CAP+'src-stripe.html','utf8').slice(0,60000).replace(/\s+/g,' '); + const cols=150,rows=180,lines=[];for(let i=0;i{let px=30;for(const tok of l.split(/(<[^>]*>)/)){if(!tok)continue; + sx.fillStyle=tok.startsWith('<')?(i%7===0?'#7dd3fc':'#93a4bd'):'#566377';sx.fillText(tok,px,24+i*30);px+=sx.measureText(tok).width;}}); + const bl=new Canvas(1300,rows*15),bx=bl.getContext('2d');bx.filter='blur(1.1px)';bx.drawImage(sc,0,0,1300,rows*15);A.src=bl; + A.grain=[];for(let g=0;g<6;g++){const c=new Canvas(256,256),x=c.getContext('2d');const d=x.createImageData(256,256);const r=rng(99+g); + for(let i=0;i[p[0],p[1]]),[0,0,w,h]);x.setTransform(m);rr(x,0,0,w,h,radius);x.clip();x.imageSmoothingQuality='high'; + if(img){if(src)x.drawImage(img,...src,0,0,w,h);else x.drawImage(img,0,0,w,h);} + if(overlay)overlay(x);x.restore(); + x.save();x.globalAlpha*=alpha*.6;x.strokeStyle='rgba(255,255,255,0.09)';x.lineWidth=1;x.beginPath();x.moveTo(q[0][0],q[0][1]);for(const p of q.slice(1))x.lineTo(p[0],p[1]);x.closePath();x.stroke();x.restore();} +const MAXW=W-120; +function line(x,b,parts,cx,y,{size=96,weight=800,align='center',out=null,color='#f5f7fb',track=-0.04,font='Inter'}={}){ + let ws,sp,total;const measure=()=>{x.font=`${weight} ${size}px ${font}`;x.letterSpacing=`${track*size}px`;ws=parts.map(p=>x.measureText(p.s).width);sp=x.measureText(' ').width;total=ws.reduce((a,c)=>a+c,0)+sp*(parts.length-1);}; + measure();const lim=align==='center'?MAXW:W-cx-60;if(total>lim){size*=lim/total;measure();} + let px=align==='center'?cx-total/2:cx;const q=out?eio(seg(b,out[0],out[1])):0; + parts.forEach((p,i)=>{const k=eo5(seg(b,p.at,p.at+1.1));const a=clamp(seg(b,p.at,p.at+.6))*(1-q); + if(a>0.002){x.save();x.globalAlpha*=a;const bl=(1-k)*12;if(bl>.3)x.filter=`blur(${bl.toFixed(2)}px)`;x.fillStyle=p.g?grad(x,px,px+ws[i]):(p.c||color);x.textAlign='left';x.fillText(p.s,px,y+(1-k)*36-q*16);x.restore();} + px+=ws[i]+sp;});} +const CURSOR=new Path2D('M4 2l16 9.5-7 1.6-3.6 6.6z'); +function cursorPath(path,b){let px=path[0][1],py=path[0][2]; + for(let i=0;i=ta&&b<=tb){const k=eio(seg(b,ta,tb));px=lerp(xa,xb,k);py=lerp(ya,yb,k);}if(b>tb){px=xb;py=yb;}} + let press=0,rip=-1;for(const p of path)if(p[3]==='c'){press=Math.max(press,seg(b,p[0]-.12,p[0])-seg(b,p[0],p[0]+.25));if(b>=p[0]&&b=0){x.save();x.globalAlpha*=(1-rip)*.9*a;x.strokeStyle='rgba(165,180,252,1)';x.lineWidth=2.5;x.beginPath();x.arc(sx,sy,12+rip*32,0,7);x.stroke();x.restore();} + x.save();x.globalAlpha*=a;x.translate(sx-5,sy-4);const s=scale*(1-.16*press);x.scale(s,s);x.shadowColor='rgba(0,0,0,.55)';x.shadowBlur=8;x.shadowOffsetY=3;x.fillStyle='#fff';x.fill(CURSOR); + x.shadowColor='transparent';x.strokeStyle='#05070b';x.lineWidth=1.3;x.lineJoin='round';x.stroke(CURSOR);x.restore();} +function band(x,h,stop=.82,sa=1){const g=x.createLinearGradient(0,0,0,h);g.addColorStop(0,`rgba(12,15,20,1)`);g.addColorStop(stop,`rgba(12,15,20,${sa})`);g.addColorStop(1,'rgba(12,15,20,0)');x.fillStyle=g;x.fillRect(0,0,W,h);} +function bandLeft(x,w){const g=x.createLinearGradient(0,0,w,0);g.addColorStop(0,'rgba(12,15,20,1)');g.addColorStop(.72,'rgba(12,15,20,1)');g.addColorStop(1,'rgba(12,15,20,0)');x.fillStyle=g;x.fillRect(0,0,w,H);} +function sceneAlpha(b,[a,z],fin=.5,fout=.5){if(b=z)return 0;return Math.min(fin?eo(seg(b,a,a+fin)):1,fout?1-eio(seg(b,z-fout,z)):1);} +function ring(x,bx,col,e,pad=5,r=null){if(e<=0)return;const s=lerp(1.4,1,eo(e));x.save();x.globalAlpha*=clamp(e*1.5);const cx=bx.x+bx.w/2,cy=bx.y+bx.h/2,w=(bx.w+pad*2)*s,h=(bx.h+pad*2)*s; + rr(x,cx-w/2,cy-h/2,w,h,r??h/2);x.shadowColor=col;x.shadowBlur=16;x.strokeStyle=col;x.lineWidth=2;x.stroke();x.restore();} + +// ---------- the browser window (a simple generic frame; not Chrome's real UI) with the real extension icon + badge +const TB=52,PX=976,PY=58,PWID=360;const ICX=1290,ICY=26; // icon centre in window coords +const HOSTS={stripe:'stripe.com',notion:'notion.com',nextjs:'nextjs.org',netflix:'netflix.com',react:'react.dev',vuejs:'vuejs.org',airbnb:'airbnb.com',github:'github.com'}; +function toolbar(x,site,badge,badgePop=1,iconPress=0){ + x.fillStyle='#1b1f27';x.fillRect(0,0,1440,TB);x.fillStyle='rgba(255,255,255,.06)';x.fillRect(0,TB-1,1440,1); + x.strokeStyle='#8b93a3';x.lineWidth=2;x.lineCap='round';x.lineJoin='round'; + const arrow=(cx,d)=>{x.beginPath();x.moveTo(cx+6*d,ICY);x.lineTo(cx-6*d,ICY);x.moveTo(cx-1*d,ICY-5);x.lineTo(cx-6*d,ICY);x.lineTo(cx-1*d,ICY+5);x.stroke();}; + arrow(28,1);x.globalAlpha*=.45;arrow(62,-1);x.globalAlpha/=.45;x.beginPath();x.arc(96,ICY,6.5,-.2*Math.PI,1.55*Math.PI);x.stroke(); + rr(x,124,9,1100,34,17);x.fillStyle='#262b35';x.fill(); + x.strokeStyle='#8b93a3';x.lineWidth=1.6;rr(x,142,20,10,8,2);x.stroke();x.beginPath();x.arc(147,20,3.5,Math.PI,0);x.stroke(); + x.font='500 15px Inter';x.letterSpacing='0px';x.fillStyle='#e6e9ef';x.fillText(HOSTS[site]||site,166,31); + // extension slot: the real WhatStack icon + blue badge + if(iconPress>0){x.fillStyle=`rgba(255,255,255,${.12*iconPress})`;x.beginPath();x.arc(ICX,ICY,17,0,7);x.fill();} + x.drawImage(A.icon,ICX-11,ICY-11,22,22); + if(badge){const s=lerp(.3,1,eback(clamp(badgePop)));x.save();x.globalAlpha*=clamp(badgePop*3);x.translate(ICX+9,ICY+9);x.scale(s,s); + x.font='700 10px Inter';const tw=Math.max(14,x.measureText(badge).width+8);rr(x,-tw/2,-7,tw,14,7);x.fillStyle=BADGE;x.fill();x.strokeStyle='#1b1f27';x.lineWidth=1.5;x.stroke(); + x.fillStyle='#fff';x.textAlign='center';x.fillText(badge,0,3.6);x.restore();} + // generic menu dots + x.fillStyle='#8b93a3';[0,1,2].forEach(i=>{x.beginPath();x.arc(1410,ICY-6+i*6,1.7,0,7);x.fill();}); +} +function popupH(site,open=false){return open?A['pop-stripe-open'].height/3:A['pop-'+site].height/3;} +function drawPopup(x,site,k=1,{open=false,overlay=null}={}){ // popup anchored under the icon, opens from its top-right + if(k<=0)return;const img=open?A['pop-stripe-open']:A['pop-'+site];const h=img.height/3; + x.save();const e=eo5(clamp(k));x.globalAlpha*=clamp(k*2.2);x.translate(PX+PWID,PY);x.scale(lerp(.94,1,e),lerp(.94,1,e));x.translate(-(PX+PWID),-PY); + x.save();x.shadowColor='rgba(0,0,0,.6)';x.shadowBlur=40;x.shadowOffsetY=18;rr(x,PX,PY,PWID,h,10);x.fillStyle=BG;x.fill();x.restore(); + x.save();rr(x,PX,PY,PWID,h,10);x.clip();x.drawImage(img,PX,PY,PWID,h);if(overlay)overlay(x);x.restore(); + x.strokeStyle='rgba(255,255,255,.10)';x.lineWidth=1;rr(x,PX+.5,PY+.5,PWID-1,h-1,10);x.stroke();x.restore();} +const P=(bx)=>({x:PX+bx.x,y:PY+bx.y,w:bx.w,h:bx.h}); // popup box -> window coords +function windowPlane(x,cam,site,{badge=null,badgePop=1,popup=0,open=false,popOverlay=null,iconPress=0,alpha=1,dim=0}={}){ + plane(x,cam,null,{w:1440,h:952,alpha,overlay:(p)=>{p.drawImage(A['page-'+site],0,TB,1440,900);if(dim>0){p.fillStyle=`rgba(12,15,20,${dim})`;p.fillRect(0,TB,1440,900);}toolbar(p,site,badge,badgePop,iconPress);drawPopup(p,site,popup,{open,overlay:popOverlay});}});} + +// ---------- ambient +function ambient(x,b,level=1){const t=b*.5; + const blob=(cx,cy,r,col,a)=>{const g=x.createRadialGradient(cx,cy,0,cx,cy,r);g.addColorStop(0,`rgba(${col},${a})`);g.addColorStop(1,`rgba(${col},0)`);x.fillStyle=g;x.fillRect(cx-r,cy-r,2*r,2*r);}; + blob(W*.12+Math.sin(t*.25)*90,H*.08+Math.cos(t*.2)*60,760,'45,212,191',.08*level); + blob(W*.88+Math.cos(t*.22)*100,H*.95+Math.sin(t*.3)*60,880,'99,102,241',.13*level); + blob(W*.9+Math.sin(t*.18)*60,H*.1,520,'192,132,252',.05*level);} +function finish(x,b,fi){const g=x.createRadialGradient(CX,CY-40,pick(720,640),CX,CY,1300);g.addColorStop(0,'rgba(12,15,20,0)');g.addColorStop(1,'rgba(12,15,20,.7)');x.fillStyle=g;x.fillRect(0,0,W,H); + x.save();x.globalAlpha=.03;const p=x.createPattern(A.grain[fi%A.grain.length],'repeat');x.fillStyle=p;x.translate((fi*37)%256,(fi*91)%256);x.fillRect(-256,-256,W+512,H+512);x.restore();} + +// ================= SCENES +function hook(x,b){const [a,z]=S.hook;if(!inS(b,[a,z]))return;x.save(); + const cut=z-.4;if(b{if(b>=t)i=j;});const site=order[i]; + const since=b-C.sitePages[i];const kk=eo(seg(b,a,z)); + const slide=eo5(seg(since,0,.5)); + let cam=V?{fx:1000,fy:470,cx:540+(1-slide)*60,cy:1180,s:lerp(1.02,1.08,kk),rx:lerp(12,8,kk),ry:lerp(-10,-6,kk)} + :{fx:720,fy:476,cx:960+(1-slide)*50,cy:620,s:lerp(.9,.95,kk),rx:lerp(14,9,kk),ry:lerp(-8,-4,kk)}; + const badge=META[site].light.badge;const bp=seg(b,C.badges[i],C.badges[i]+.5); + windowPlane(x,cam,site,{badge:b>=C.badges[i]?badge:null,badgePop:bp,alpha:1}); + // spotlight ring on the badge when it lands + const [bx,by]=planePt(cam,ICX+9,ICY+9);const e=seg(b,C.badges[i],C.badges[i]+1.2); + if(e>0&&e<1){x.save();x.globalAlpha*=(1-e)*.9;x.strokeStyle='rgba(59,130,246,1)';x.lineWidth=2.5;x.beginPath();x.arc(bx,by,10+e*46,0,7);x.stroke();x.restore();} + band(x,pick(240,560),.55,1); + if(!V)line(x,b,[{s:'It reads the stack',at:a+.3},{s:'while you browse.',at:a+.3,g:1}],CX,150,{size:84}); + else{line(x,b,[{s:'It reads the stack',at:a+.3}],CX,330,{size:92});line(x,b,[{s:'while you browse.',at:a+.55,g:1}],CX,440,{size:92});} + x.restore();} + +function clickScene(x,b){const [a,z]=S.click;if(!inS(b,[a,z]))return;const sa=sceneAlpha(b,[a,z],.4,.5);x.save();x.globalAlpha=sa; + const site='stripe';const pk=seg(b,C.popupOpen,C.popupOpen+.7);const hp=eio(seg(b,C.headlinePush,C.headlinePush+2)); + const hb=P(META.stripe.popup.headline);const hcx=hb.x+hb.w/2,hcy=hb.y+hb.h/2; + let cam=V?{fx:1000,fy:470,cx:540,cy:1180,s:1.08,rx:8,ry:-6}:{fx:720,fy:476,cx:960,cy:620,s:.95,rx:9,ry:-4}; + cam=camLerp(cam,V?{fx:1156,fy:300,cx:540,cy:1150,s:1.75,rx:6,ry:-5}:{fx:1156,fy:320,cx:1240,cy:560,s:1.45,rx:7,ry:-6},eio(seg(b,a,C.iconClick+.3))); + cam=camLerp(cam,V?{fx:hcx,fy:hcy+70,cx:540,cy:1260,s:2.75,rx:4,ry:-3}:{fx:hcx,fy:hcy+60,cx:1420,cy:560,s:2.55,rx:4,ry:-5},hp); + const badge=b>=C.badgeDeep?META.stripe.deep.badge:META.stripe.light.badge;const bpop=b>=C.badgeDeep?seg(b,C.badgeDeep,C.badgeDeep+.5):1; + const cp=cursorPath([[a+.3,pick(1100,1000),420],[C.iconClick-.12,ICX+2,ICY+3],[C.iconClick,ICX+2,ICY+3,'c'],[C.iconClick+.9,ICX+60,ICY+140]],b); + windowPlane(x,cam,site,{badge,badgePop:bpop,popup:pk,iconPress:cp.press,dim:.78*eo(seg(b,C.popupOpen,C.popupOpen+1)),popOverlay:(p)=>{ring(p,{x:hb.x,y:hb.y,w:hb.w,h:hb.h},'rgba(129,140,248,1)',seg(b,C.headlinePush+1.2,C.headlinePush+2),3,12);}}); + const [sx,sy]=planePt(cam,cp.px,cp.py);drawCursor(x,sx,sy,cp,pick(1.4,1.7)); + if(!V){bandLeft(x,1000);line(x,b,[{s:'One click.',at:a+.8}],120,470,{size:92,align:'left'});line(x,b,[{s:'The whole',at:C.headlinePush+.2},{s:'stack.',at:C.headlinePush+.2,g:1}],120,582,{size:92,align:'left'});} + else{band(x,600,.6,1);line(x,b,[{s:'One click.',at:a+.8}],CX,330,{size:100});line(x,b,[{s:'The whole',at:C.headlinePush+.2},{s:'stack.',at:C.headlinePush+.2,g:1}],CX,450,{size:100});} + x.restore();} + +function conf(x,b){const [a,z]=S.conf;if(!inS(b,[a,z]))return;const sa=sceneAlpha(b,[a,z],.4,.5);x.save();x.globalAlpha=sa; + const gh=b>=C.githubCut;const site=gh?'github':'stripe';const pop=META[site].popup; + if(!gh){const k=eo(seg(b,a,C.githubCut));const cam=V?{fx:1156,fy:lerp(420,440,k),cx:540,cy:1290,s:lerp(2.55,2.65,k),rx:5,ry:-4}:{fx:1156,fy:lerp(360,420,k),cx:1260,cy:560,s:lerp(1.75,1.85,k),rx:6,ry:-6}; + windowPlane(x,cam,site,{badge:META.stripe.deep.badge,popup:1,dim:.78,popOverlay:(p)=>{pop.hits.forEach((h,i)=>{const bd=h.badge;const med=/MEDIUM/.test(h.text); + ring(p,{x:PX+bd.x,y:PY+bd.y,w:bd.w,h:bd.h},med?'#fbbf24':'#34d399',seg(b,C.rings[i],C.rings[i]+.6),4);});}});} + else{const k=eo(seg(b,C.githubCut,z));const lw=P(pop.low);const cam=V?{fx:1156,fy:lw.y+40,cx:540,cy:1260,s:lerp(2.7,2.8,k),rx:5,ry:-4}:{fx:1156,fy:lw.y+40,cx:1260,cy:560,s:lerp(2.1,2.2,k),rx:6,ry:-6}; + windowPlane(x,cam,site,{badge:META.github.deep.badge,popup:1,dim:.78,popOverlay:(p)=>{const s=pop.sections.find(q=>/BUILD/i.test(q.title)); + ring(p,{x:PX+s.x,y:PY+s.y,w:s.w,h:s.h},'#94a3b8',seg(b,C.lowRing,C.lowRing+.6),3,10);}});} + if(!V){bandLeft(x,1000);line(x,b,[{s:'Confidence,',at:a+.2}],120,480,{size:100,align:'left'});line(x,b,[{s:'not',at:a+.5},{s:'guesses.',at:a+.5,g:1}],120,600,{size:100,align:'left'}); + line(x,b,[{s:'High',at:C.rings[0],c:'#34d399'},{s:'·',at:C.rings[0],c:'#475569'},{s:'Medium',at:C.rings[4],c:'#fbbf24'},{s:'·',at:C.rings[4],c:'#475569'},{s:'Low',at:C.lowRing,c:'#94a3b8'}],124,690,{size:40,weight:600,align:'left',track:-0.01});} + else{band(x,700,.86,1);line(x,b,[{s:'Confidence,',at:a+.2}],CX,320,{size:100});line(x,b,[{s:'not',at:a+.5},{s:'guesses.',at:a+.5,g:1}],CX,440,{size:100}); + line(x,b,[{s:'High',at:C.rings[0],c:'#34d399'},{s:'·',at:C.rings[0],c:'#475569'},{s:'Medium',at:C.rings[4],c:'#fbbf24'},{s:'·',at:C.rings[4],c:'#475569'},{s:'Low',at:C.lowRing,c:'#94a3b8'}],CX,530,{size:44,weight:600,track:-0.01});} + x.restore();} + +function evid(x,b){const [a,z]=S.evid;if(!inS(b,[a,z]))return;const sa=sceneAlpha(b,[a,z],.4,.5);x.save();x.globalAlpha=sa; + const open=b>=C.rowClick+.05;const row=P(META.stripe.popup.hits[0]);const ex=META.stripe.popup.expanded;const ev=ex.evidence[0]?P(ex.evidence[0]):{x:row.x,y:row.y+38,w:338,h:150}; + const k=eio(seg(b,C.rowClick,C.rowClick+1.4)); + let cam=V?{fx:1156,fy:row.y+20,cx:540,cy:1180,s:2.7,rx:5,ry:-4}:{fx:1156,fy:row.y+20,cx:1260,cy:470,s:2.2,rx:6,ry:-6}; + cam=camLerp(cam,V?{fx:1156,fy:ev.y+ev.h/2-10,cx:540,cy:1300,s:2.75}:{fx:1156,fy:ev.y+ev.h/2-10,cx:1260,cy:560,s:2.25},k); + const rev=eio(seg(b,C.evidReveal[0],C.evidReveal[1])); + const cp=cursorPath([[a+.2,PX+300,row.y+170],[C.rowClick-.12,PX+120,row.y+19],[C.rowClick,PX+120,row.y+19,'c'],[C.rowClick+1,PX+250,row.y-40]],b); + windowPlane(x,cam,'stripe',{badge:META.stripe.deep.badge,popup:1,open,dim:.78,popOverlay:(p)=>{if(!open)return; + // evidence lines type in top-to-bottom (the list itself is the real expanded popup) + const y0=ev.y+ev.h*rev;p.fillStyle=BG;p.fillRect(ev.x-6,y0,ev.w+12,ev.y+ev.h-y0+2); + if(rev<1){const g=p.createLinearGradient(ev.x,0,ev.x+ev.w,0);g.addColorStop(0,'rgba(59,130,246,0)');g.addColorStop(.5,'rgba(96,165,250,.9)');g.addColorStop(1,'rgba(59,130,246,0)');p.fillStyle=g;p.fillRect(ev.x,y0-1,ev.w,2);}}}); + const [sx,sy]=planePt(cam,cp.px,cp.py);drawCursor(x,sx,sy,cp,pick(1.4,1.7)); + if(!V){bandLeft(x,1000);line(x,b,[{s:'See why',at:a+.2}],120,480,{size:100,align:'left'});line(x,b,[{s:'it matched.',at:a+.45,g:1}],120,600,{size:100,align:'left'}); + line(x,b,[{s:'Scripts, globals and DOM —',at:C.evidReveal[1]-.4}],124,690,{size:34,weight:500,align:'left',color:'#aab6c8',track:-0.01}); + line(x,b,[{s:'never marketing copy.',at:C.evidReveal[1]-.2}],124,738,{size:34,weight:500,align:'left',color:'#aab6c8',track:-0.01});} + else{band(x,720,.86,1);line(x,b,[{s:'See why',at:a+.2}],CX,320,{size:100});line(x,b,[{s:'it matched.',at:a+.45,g:1}],CX,440,{size:100}); + line(x,b,[{s:'Scripts, globals and DOM —',at:C.evidReveal[1]-.4}],CX,535,{size:38,weight:500,color:'#aab6c8',track:-0.01}); + line(x,b,[{s:'never marketing copy.',at:C.evidReveal[1]-.2}],CX,588,{size:38,weight:500,color:'#aab6c8',track:-0.01});} + x.restore();} + +function any(x,b){const [a,z]=S.any;if(!inS(b,[a,z]))return;const sa=sceneAlpha(b,[a,z],0,.5);x.save();x.globalAlpha=sa; + const order=['notion','nextjs','react','netflix','airbnb','vuejs'];let i=0;C.anyCuts.forEach((t,j)=>{if(b>=t)i=j;});const site=order[i]; + const since=b-C.anyCuts[i];const k=seg(since,0,1.333);const sgn=i%2?1:-1; + const cam=V?{fx:1120,fy:340,cx:540+sgn*lerp(18,0,eo(k)),cy:1180,s:lerp(1.6,1.68,k),rx:7,ry:sgn*lerp(7,4,k)} + :{fx:1060,fy:360,cx:1250+sgn*lerp(20,0,eo(k)),cy:600,s:lerp(1.2,1.26,k),rx:7,ry:sgn*lerp(8,5,k)}; + windowPlane(x,cam,site,{badge:META[site].deep.badge,popup:1,dim:.78}); + if(!V){bandLeft(x,1000);line(x,b,[{s:'Any site.',at:a+.15}],120,480,{size:100,align:'left'});line(x,b,[{s:'Instantly.',at:a+.5,g:1}],120,600,{size:100,align:'left'}); + // live site label (real host of the page on screen) + line(x,b,[{s:HOSTS[site],at:C.anyCuts[i]}],124,690,{size:36,weight:600,align:'left',color:'#7dd3fc',track:0});} + else{band(x,640,.62,1);line(x,b,[{s:'Any site.',at:a+.15}],CX,330,{size:104});line(x,b,[{s:'Instantly.',at:a+.5,g:1}],CX,450,{size:104}); + line(x,b,[{s:HOSTS[site],at:C.anyCuts[i]}],CX,540,{size:40,weight:600,color:'#7dd3fc',track:0});} + x.restore();} + +function codeCard(x,title,text,b,at,{cx,cy,w,hmax,maxLines,alpha=1,fs=21}){const k=eo5(seg(b,at,at+.8));if(k<=0)return; + const lines=text.split('\n').slice(0,maxLines);const lh=fs*1.5;const h=Math.min(hmax,70+lines.length*lh+24); + x.save();x.globalAlpha*=clamp(seg(b,at,at+.4))*alpha;x.translate(cx,cy+(1-k)*60);x.scale(lerp(.96,1,k),lerp(.96,1,k)); + x.save();x.shadowColor='rgba(0,0,0,.6)';x.shadowBlur=60;x.shadowOffsetY=30;rr(x,-w/2,-h/2,w,h,16);x.fillStyle='#0b0f16';x.fill();x.restore(); + rr(x,-w/2,-h/2,w,h,16);x.strokeStyle='#243041';x.lineWidth=1.2;x.stroke(); + x.fillStyle='#141a22';rr(x,-w/2+1,-h/2+1,w-2,50,15);x.fill();x.fillStyle='#243041';x.fillRect(-w/2,-h/2+50,w,1); + x.font='600 18px Inter';x.letterSpacing='0px';x.fillStyle='#8b9bb0';x.fillText(title,-w/2+24,-h/2+32); + x.font='600 16px Inter';x.fillStyle='#34d399';x.textAlign='right';x.fillText(title==='Markdown'?'Copied Markdown':'Copied JSON',w/2-24,-h/2+32);x.textAlign='left'; + x.save();rr(x,-w/2,-h/2+51,w,h-51,16);x.clip();x.font=`400 ${fs}px JBMono`; + const shown=Math.floor(lerp(0,lines.length,eo(seg(b,at+.2,at+1.6)))); + lines.slice(0,shown).forEach((l,i)=>{const y=-h/2+70+fs+i*lh;let px=-w/2+24; + const toks=title==='JSON'?l.split(/("[^"]*"\s*:|"[^"]*")/):l.split(/(\|)/); + for(const t of toks){if(!t)continue;x.fillStyle=title==='JSON'?(/:\s*$/.test(t)?'#93c5fd':t.startsWith('"')?'#a7f3d0':'#cbd5e1'):(t==='|'?'#475569':/^\s*(##|\*\*)/.test(l)&&i<3?'#e8eef7':'#cbd5e1');x.fillText(t,px,y);px+=x.measureText(t).width;}}); + x.restore();x.restore();} +function exportScene(x,b){const [a,z]=S.export;if(!inS(b,[a,z]))return;const sa=sceneAlpha(b,[a,z],.4,.5);x.save();x.globalAlpha=sa; + const bt=META.stripe.popup.buttons;const md=P(bt.MD),js=P(bt.JSON);const k=eo(seg(b,a,z)); + const cam=V?{fx:1156,fy:md.y-40,cx:540,cy:1660,s:lerp(2.0,2.08,k),rx:6,ry:-4}:{fx:1156,fy:md.y-120,cx:1450,cy:560,s:lerp(1.5,1.56,k),rx:6,ry:-8}; + const cp=cursorPath([[a+.2,PX+300,md.y-120],[C.mdClick-.12,md.x+md.w/2,md.y+md.h/2],[C.mdClick,md.x+md.w/2,md.y+md.h/2,'c'],[C.jsonClick-.12,js.x+js.w/2,js.y+js.h/2],[C.jsonClick,js.x+js.w/2,js.y+js.h/2,'c'],[z,js.x+js.w/2+30,js.y+60]],b); + windowPlane(x,cam,'stripe',{badge:META.stripe.deep.badge,popup:1,dim:.78,popOverlay:(p)=>{ring(p,md,'#60a5fa',seg(b,C.mdClick,C.mdClick+.5)*(1-seg(b,C.jsonClick-.3,C.jsonClick)),3,8);ring(p,js,'#60a5fa',seg(b,C.jsonClick,C.jsonClick+.5),3,8);}}); + const [sx,sy]=planePt(cam,cp.px,cp.py);drawCursor(x,sx,sy,cp,pick(1.4,1.7)); + const mdOut=1-eio(seg(b,C.jsonClick-.2,C.jsonClick+.2)); + const card=V?{cx:540,cy:980,w:1020,hmax:800}:{cx:590,cy:600,w:980,hmax:760}; + if(bring(p,lo,TEAL,seg(b,C.localRing,C.localRing+.6),3)}); + if(!V){band(x,380,.8,1);line(x,b,[{s:'Runs entirely on your',at:a+.3},{s:'device.',at:a+.3,g:1}],CX,170,{size:92}); + line(x,b,[{s:'49 KB · No account · Nothing uploaded',at:a+1.4}],CX,255,{size:36,weight:500,color:'#aab6c8',track:-0.01});} + else{band(x,720,.86,1);line(x,b,[{s:'Runs entirely',at:a+.3}],CX,340,{size:100});line(x,b,[{s:'on your',at:a+.55},{s:'device.',at:a+.55,g:1}],CX,460,{size:100}); + line(x,b,[{s:'49 KB · No account · Nothing uploaded',at:a+1.4}],CX,560,{size:40,weight:500,color:'#aab6c8',track:-0.01});} + x.restore();} + +function cta(x,b){const [a,z]=S.cta;if(!inS(b,[a,z]))return;if(V)return ctaV(x,b);const sa=sceneAlpha(b,[a,z],.35,0);x.save();x.globalAlpha=sa; + // real Chrome Web Store listing header (logged out), cropped to icon + name + "Add to Chrome" + const st=META.store;const s3=3;const cropX=180,cropY=226,cropW=1120,cropH=100;const k=eo(seg(b,a,z)); + const cam=V?{fx:cropW/2,fy:cropH/2,cx:540,cy:1000,s:lerp(.88,.93,k),rx:6,ry:-5} + :{fx:cropW/2,fy:cropH/2,cx:960,cy:560,s:lerp(1.45,1.55,k),rx:7,ry:-5}; + plane(x,cam,A.store,{w:cropW,h:cropH,radius:16,src:[cropX*s3,cropY*s3,cropW*s3,cropH*s3],overlay:(p)=>{const ax=st.add.x-cropX,ay=st.add.y-cropY; + const pr=seg(b,C.addClick-.1,C.addClick)-seg(b,C.addClick,C.addClick+.25);if(pr>0){rr(p,ax,ay,st.add.w,st.add.h,20);p.fillStyle=`rgba(0,0,0,${.15*pr})`;p.fill();} + ring(p,{x:ax,y:ay,w:st.add.w,h:st.add.h},'rgba(96,165,250,1)',seg(b,C.addClick-.5,C.addClick),4);}}); + const ax=st.add.x-cropX+st.add.w*.55,ay=st.add.y-cropY+st.add.h*.6; + const cp=cursorPath([[a+.1,cropW*.5,cropH+120],[C.addClick-.1,ax,ay],[C.addClick,ax,ay,'c'],[z,ax,ay]],b);const [sx,sy]=planePt(cam,cp.px,cp.py);drawCursor(x,sx,sy,{...cp,a:clamp(seg(b,a+.1,a+.4))},pick(1.6,1.8)); + line(x,b,[{s:'Free on the',at:a+.1},{s:'Chrome Web Store',at:a+.1,g:1}],CX,pick(830,1300),{size:pick(60,64),weight:700,track:-0.03}); + x.restore();} + +function ctaV(x,b){const [a,z]=S.cta;const sa=sceneAlpha(b,[a,z],.35,0);x.save();x.globalAlpha=sa;const st=META.store;const s3=3;const k=eo(seg(b,a,z)); + const n={x:180,y:236,w:420,h:80};plane(x,{fx:n.w/2,fy:n.h/2,cx:540,cy:lerp(880,870,k),s:2.1,rx:5,ry:-4},A.store,{w:n.w,h:n.h,radius:16,src:[n.x*s3,n.y*s3,n.w*s3,n.h*s3]}); + const bt={x:st.add.x-18,y:st.add.y-14,w:st.add.w+36,h:st.add.h+28};const cam={fx:bt.w/2,fy:bt.h/2,cx:540,cy:lerp(1110,1100,k),s:3.0,rx:5,ry:-4}; + plane(x,cam,A.store,{w:bt.w,h:bt.h,radius:14,src:[bt.x*s3,bt.y*s3,bt.w*s3,bt.h*s3],overlay:(p)=>{const pr=seg(b,C.addClick-.1,C.addClick)-seg(b,C.addClick,C.addClick+.25); + if(pr>0){rr(p,18,14,st.add.w,st.add.h,20);p.fillStyle=`rgba(0,0,0,${.15*pr})`;p.fill();}ring(p,{x:18,y:14,w:st.add.w,h:st.add.h},'rgba(96,165,250,1)',seg(b,C.addClick-.5,C.addClick),4);}}); + const ax=18+st.add.w*.6,ay=14+st.add.h*.62;const cp=cursorPath([[a+.1,bt.w*.9,bt.h+50],[C.addClick-.1,ax,ay],[C.addClick,ax,ay,'c'],[z,ax,ay]],b); + const [sx,sy]=planePt(cam,cp.px,cp.py);drawCursor(x,sx,sy,{...cp,a:clamp(seg(b,a+.1,a+.4))},1.9); + line(x,b,[{s:'Free on the',at:a+.1}],CX,1360,{size:66,weight:700,track:-0.03});line(x,b,[{s:'Chrome Web Store',at:a+.25,g:1}],CX,1445,{size:66,weight:700,track:-0.03}); + x.restore();} +function end(x,b){const [a,z]=S.end;if(!inS(b,[a,z]))return;x.save(); + const lk=eo5(seg(b,a+.1,a+1.3));const ls=pick(150,210);const ly=pick(330,640); + x.save();x.globalAlpha*=clamp(seg(b,a+.1,a+.5));x.translate(CX,ly);x.scale(lerp(.9,1,lk),lerp(.9,1,lk));x.shadowColor='rgba(99,102,241,.5)';x.shadowBlur=60;x.drawImage(A.logo,-ls/2,-ls/2,ls,ls);x.restore(); + const k=eo5(seg(b,a+.3,a+1.6));x.save();x.globalAlpha*=clamp(seg(b,a+.3,a+.8));const size=pick(120,140);x.font=`800 ${size}px Inter`;x.letterSpacing=`${lerp(.05,-.045,k)*size}px`;x.textAlign='center'; + if(k<1)x.filter=`blur(${((1-k)*12).toFixed(1)}px)`;x.fillStyle='#f5f7fb';x.fillText('WhatStack',CX,pick(570,930));x.restore(); + line(x,b,[{s:'What’s under',at:a+.8},{s:'this page?',at:a+.8,g:1}],CX,pick(665,1040),{size:pick(50,58),weight:600,track:-0.02}); + line(x,b,[{s:'Free on the Chrome Web Store',at:a+1.3}],CX,pick(770,1160),{size:pick(34,40),weight:600,color:'#7dd3fc',track:0}); + x.restore();} + +// ================= frame +function drawFrame(x,b,fi){x.setTransform(1,0,0,1,0,0);x.globalAlpha=1;x.filter='none';x.fillStyle=BG;x.fillRect(0,0,W,H); + ambient(x,b,b=c)b=c-1e-4;if(bc>=c&&b — renders frames [start,end) with 10-subframe motion blur, pipes to FFmpeg. +// Frame 0 is replaced by the poster frame (posterBeat) so every platform's thumbnail is a settled shot. +const E=require('./engine');const {spawn}=require('child_process');const fs=require('fs'); +const [start,end,idx]=process.argv.slice(2).map(Number); +(async()=>{ + await E.load();const {W,H,TL}=E;const c=new E.Canvas(W,H),x=c.getContext('2d'); + const segDir=__dirname+(E.V?'/work/seg-v/':'/work/seg/');fs.mkdirSync(segDir,{recursive:true}); + const ff=spawn('ffmpeg',['-y','-hide_banner','-loglevel','error','-f','rawvideo','-pix_fmt','rgba','-s',`${W}x${H}`,'-r',String(TL.fps),'-i','-', + '-c:v','libx264','-preset','medium','-crf','16','-pix_fmt','yuv420p','-g','60','-r',String(TL.fps),segDir+`seg${String(idx).padStart(2,'0')}.mp4`],{stdio:['pipe','inherit','inherit']}); + const acc=new Uint16Array(W*H*4);const out=Buffer.alloc(W*H*4); + const fpb=TL.fps/(TL.bpm/60); + const t0=Date.now(); + for(let f=start;f>1))/n|0; + if(f===0)fs.writeFileSync(__dirname+(E.V?'/work/poster-v.rgba':'/work/poster.rgba'),out); + if(!ff.stdin.write(out))await new Promise(r=>ff.stdin.once('drain',r)); + if((f-start)%30===0)console.log(`seg${idx} frame ${f} (${f-start+1}/${end-start}) ${((Date.now()-t0)/(f-start+1)/1000).toFixed(2)}s/frame`); + } + ff.stdin.end();await new Promise(r=>ff.on('close',r));console.log(`seg${idx} done in ${((Date.now()-t0)/1000).toFixed(0)}s`); +})().catch(e=>{console.error(e);process.exit(1)}); diff --git a/launch-video/source/score.py b/launch-video/source/score.py new file mode 100644 index 0000000..182b168 --- /dev/null +++ b/launch-video/source/score.py @@ -0,0 +1,128 @@ +# Original score + UI sound design, generated from the SAME beat timeline as the picture (timeline.json). +import json, wave, numpy as np +from scipy.signal import butter, sosfilt, fftconvolve +TL=json.load(open(__file__.rsplit('/',1)[0]+'/timeline.json')) +BPM=TL['bpm']; SPB=60/BPM; S={k:[a*SPB,b*SPB] for k,(a,b) in TL['scenes'].items()} +C=TL['cues']; sec=lambda b: b*SPB +SR=48000; DUR=TL['beats']*SPB; N=int(SR*DUR) +rng=np.random.default_rng(21) +mus=np.zeros((2,N)); pb=np.zeros((2,N)); sfx=np.zeros((2,N)) +mtof=lambda m: 440*2**((m-69)/12) +tt=lambda d: np.arange(int(d*SR))/SR +def filt(x,kind,f,order=2): return sosfilt(butter(order,np.array(np.atleast_1d(f))/(SR/2),btype=kind,output='sos'),x) +def put(bus,x,t0,g=1.0,pan=0.0,width=0.0): + i=int(t0*SR) + if i>=N: return + j=min(N,i+len(x)); x=x[:j-i]*g; l=np.sqrt((1-pan)/2); r=np.sqrt((1+pan)/2) + bus[0,i:j]+=x*l; d=int(width*0.012*SR) + bus[1,i:j]+=(np.concatenate([np.zeros(d),x])[:len(x)] if d else x)*r +def adsr(n,a,d,s,r,total): + t=np.arange(n)/SR; e=np.where(t=a)&(t=a+d,s,e) + return np.where(t>total-r,e*np.clip(1-(t-(total-r))/r,0,1),e) +def saw(f,t,det=0.0): ph=f*(1+det)*t; return 2*(ph-np.floor(ph+0.5)) +def supersaw(f,t,n=5,spread=0.012): return sum(saw(f,t,(k-(n-1)/2)*spread/(n-1)*2) for k in range(n))/n +def sub(f=45,d=0.5,dec=0.18): x=tt(d); fr=f+60*np.exp(-x/0.03); return np.sin(2*np.pi*np.cumsum(fr)/SR)*np.exp(-x/dec) +def bell(m,d=2.5): + x=tt(d); f=mtof(m); y=np.sin(2*np.pi*f*x)+0.35*np.sin(2*np.pi*2*f*x)*np.exp(-x/0.4)+0.12*np.sin(2*np.pi*3.01*f*x)*np.exp(-x/0.15) + return y*np.minimum(1,x/0.004)*np.exp(-x/0.9) +def impact(): x=tt(1.6); return sub(38,1.6,0.45)+0.15*filt(rng.standard_normal(len(x)),'high',3000)*np.exp(-x/0.25) +def riser(d,f0=300,f1=7000): + x=tt(d); n=rng.standard_normal(len(x)); out=np.zeros_like(n); st=int(0.05*SR) + for i in range(0,len(n),st): + fc=f0*(f1/f0)**(i/len(n)); out[i:i+st]=filt(n[i:i+st+2000],'band',[fc*0.7,min(fc*1.4,20000)])[:len(out[i:i+st])] + return out*np.linspace(0,1,len(x))**2 +prog=[[57,60,64,71],[53,57,60,67],[48,52,55,62],[55,59,62,69]]; bassn=[33,29,36,31] +chord=lambda t: int(t//(4*SPB))%4 # one chord per bar (4 beats) + +# ---- HOOK: low drone + muffled scrolling-source ticks, cut to silence before the reveal +cut=sec(TL['scenes']['hook'][1]-0.4); t=tt(cut) +put(pb,(np.sin(2*np.pi*55*t)+0.5*filt(saw(55,t,.003)+saw(55,t,-.004),'low',300))*np.minimum(1,t/0.8),0,0.22) +put(pb,filt(rng.standard_normal(len(t)),'band',[400,2200])*np.linspace(0.05,0.35,len(t)),0,0.09,0,1) +def tick(): x=tt(0.03); return filt(rng.standard_normal(len(x)),'band',[1500,5000])*np.exp(-x/0.006) +tk=0.0 +while tk=d) for d in [0,0.011,0.022])+np.exp(-x/0.12)*0.6) +def bass(m,d): x=tt(d); f=mtof(m); return np.tanh(1.4*(np.sin(2*np.pi*f*x)+0.35*filt(saw(f,x),'low',700)))*adsr(len(x),0.005,0.1,0.8,0.06,d) +K,HC,HO,CL=kick(),hat(),hat(True),clap() +b0,b1=TL['scenes']['browse'][0],TL['scenes']['export'][1]; anyA,anyZ=TL['scenes']['any'] +kicks=[] +for b in range(b0,b1): + ts=sec(b); drop=anyA-2<=b=TL['scenes']['click'][0] and b%2==1: put(mus,CL,ts,0.15 if not anyA<=b=TL['scenes']['conf'][0] and b%4==3: put(mus,HO,ts+SPB/2,0.05,0.4) + m=bassn[chord(ts)]; put(mus,bass(m,0.22),ts+SPB/2,0.26) + if b%4==0: put(mus,bass(m,0.2),ts,0.18) +t0=sec(anyA-4) +while t00.06) +def blip(m): x=tt(0.18); f=mtof(m)*(1+0.3*np.exp(-x/0.01)); return np.sin(2*np.pi*np.cumsum(f)/SR)*np.exp(-x/0.05) +def whoosh(d=0.55): x=tt(d); return filt(rng.standard_normal(len(x)),'band',[500,5000])*np.sin(np.pi*x/d)**2 +for b in C['clicks']: put(sfx,mclick(),sec(b),0.26,0.1) +for b,m in zip(C['badges'],[76,79,81]): put(sfx,blip(m),sec(b),0.10,0.3) +put(sfx,blip(84),sec(C['badgeDeep']),0.10,0.3); put(sfx,sub(42,0.9,0.3),sec(C['iconClick']),0.3) +put(sfx,whoosh(0.4),sec(C['popupOpen']),0.10,0,1) +for b,m in zip(C['rings'],[81,84,86,88,76]): put(sfx,bell(m,0.8),sec(b),0.045,0.2*np.sin(m)) +put(sfx,bell(69,1.0),sec(C['lowRing']),0.05,-0.2) +for k in range(9): put(sfx,mclick(),sec(C['evidReveal'][0])+k*(sec(C['evidReveal'][1])-sec(C['evidReveal'][0]))/9,0.07,0.15) +put(sfx,confirm(76),sec(C['mdClick'])+0.08,0.06); put(sfx,confirm(81),sec(C['jsonClick'])+0.08,0.06) +put(sfx,bell(88,1.2),sec(C['localRing']),0.06,0,1) +for b in C['whooshes']: put(sfx,whoosh(),sec(b),0.11,0,1) +for b in C['anyCuts'][1:]: put(sfx,whoosh(0.3),sec(b)-0.12,0.08,0,1) +put(sfx,confirm(84),sec(C['addClick'])+0.08,0.06) + +# ---- mix +def reverb(x,dec=1.8,wet=0.22): + it=tt(dec); ir=rng.standard_normal((2,len(it)))*np.exp(-it/(dec/5)); ir=filt(ir,'low',6000); ir/=np.sqrt((ir**2).sum(axis=1,keepdims=True)) + return np.stack([fftconvolve(x[c],ir[c])[:N] for c in range(2)])*wet +sc=np.ones(N) +for ts in kicks: + i=int(ts*SR); n=min(int(0.3*SR),N-i); sc[i:i+n]=np.minimum(sc[i:i+n],1-0.45*np.exp(-np.arange(n)/SR/0.09)) +T=np.arange(N)/SR +secg=np.interp(T,[0,S['browse'][0]-.01,S['browse'][0],S['any'][0]-.01,S['any'][0],S['any'][1],S['any'][1]+.01,DUR],[1,1,.8,.8,1,1,.85,.85]) +m2=mus*sc*secg +gate=np.clip((cut-T)/0.02,0,1) +mix=m2+reverb(m2,2.2,0.2)+(pb+reverb(pb,1.6,0.25))*gate*0.55+sfx*0.9+reverb(sfx,1.2,0.12) +mix*=np.clip((DUR-T)/1.2,0,1); mix=filt(mix,'high',28) +mix=mix/np.abs(mix).max()*0.95; mix=np.tanh(mix*1.25)/np.tanh(1.25)*0.86 +w=wave.open(__file__.rsplit('/',1)[0]+'/work/score.wav','wb'); w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR); w.writeframes((mix.T*32767).astype(' [mb] — renders stills (optionally motion-blurred) + contact sheet +const E=require('./engine');const {execSync}=require('child_process');const fs=require('fs'); +(async()=>{ + const beats=process.argv[2].split(',').map(Number);const name=process.argv[3]||'sheet';const mb=process.argv[4]==='mb'; + fs.mkdirSync(__dirname+'/work/stills',{recursive:true}); + await E.load();const c=new E.Canvas(E.W,E.H),x=c.getContext('2d');const files=[]; + for(const b of beats){const t0=Date.now();const f=`${__dirname}/work/stills/${E.V?'v':''}t${b.toFixed(2)}.jpg`; + if(mb){const fi=Math.round(b*30);const acc=new Uint16Array(E.W*E.H*4);const ss=E.samples(fi); + for(const s of ss){E.drawFrame(x,s,fi);const buf=await c.toBuffer('raw');for(let i=0;i{console.error(e);process.exit(1)}); diff --git a/launch-video/source/timeline.json b/launch-video/source/timeline.json new file mode 100644 index 0000000..f9a395f --- /dev/null +++ b/launch-video/source/timeline.json @@ -0,0 +1,43 @@ +{ + "bpm": 120, + "fps": 60, + "beats": 64, + "subframes": 10, + "shutter": 0.5, + "posterBeat": 23.5, + "scenes": { + "hook": [0, 6], + "reveal": [6, 10], + "browse": [10, 18], + "click": [18, 26], + "conf": [26, 32], + "evid": [32, 38], + "any": [38, 46], + "export": [46, 52], + "private": [52, 56], + "cta": [56, 58], + "end": [58, 64] + }, + "cues": { + "hookText": 1.0, + "sitePages": [10.0, 12.667, 15.333], + "badges": [11.0, 13.667, 16.333], + "iconClick": 19.5, + "popupOpen": 19.55, + "badgeDeep": 20.4, + "headlinePush": 21.0, + "rings": [26.6, 27.1, 27.6, 28.1, 28.6], + "githubCut": 29.5, + "lowRing": 30.1, + "rowClick": 33.0, + "evidReveal": [33.1, 34.4], + "anyCuts": [38.0, 39.333, 40.667, 42.0, 43.333, 44.667], + "mdClick": 47.0, + "jsonClick": 49.5, + "localRing": 52.6, + "addClick": 57.3, + "clicks": [19.5, 33.0, 47.0, 49.5, 57.3], + "whooshes": [5.6, 9.6, 17.6, 25.6, 31.6, 37.6, 45.6, 51.6, 55.6, 57.6], + "impacts": [6.0, 19.5, 38.0, 58.0] + } +} diff --git a/launch-video/whatstack-launch-vertical.jpg b/launch-video/whatstack-launch-vertical.jpg new file mode 100644 index 0000000..06d8a54 Binary files /dev/null and b/launch-video/whatstack-launch-vertical.jpg differ diff --git a/launch-video/whatstack-launch-vertical.mp4 b/launch-video/whatstack-launch-vertical.mp4 new file mode 100644 index 0000000..5ed9393 Binary files /dev/null and b/launch-video/whatstack-launch-vertical.mp4 differ diff --git a/launch-video/whatstack-launch.jpg b/launch-video/whatstack-launch.jpg new file mode 100644 index 0000000..7013009 Binary files /dev/null and b/launch-video/whatstack-launch.jpg differ diff --git a/launch-video/whatstack-launch.mp4 b/launch-video/whatstack-launch.mp4 new file mode 100644 index 0000000..3ad4aad Binary files /dev/null and b/launch-video/whatstack-launch.mp4 differ