Files
hyperframes/docs/public/catalog/blocks/north-korea-locked-down.json
T
Miguel Ángel 536165b6ef feat(catalog): play the real composition on catalog pages (#3168)
* feat(catalog): play the real composition on catalog pages

Catalog previews were uploaded MP4s, published by hand to the CDN. Each page
now embeds the composition itself, running in <hyperframes-player>, so a
preview is the block rather than a recording of it.

The composition is delivered as JSON under docs/public/catalog, and the player
is mounted inside an iframe. Both are forced: the docs host publishes only JSON
and images out of docs/public, and its MDX renderer strips unknown custom
elements, so a player written into the page never reaches the DOM and an .html
payload 404s in production.

Assets are inlined as data URIs, which sidesteps the file-type restriction for
fonts, scripts and models alike. 164 of 168 items build a payload; the rest
keep their MP4 and say so.

* chore: rebuild docs preview

* chore: route isolation probe

* chore: drop the route isolation probe page

* chore: probe which asset types the docs host publishes

* refactor(catalog): host preview assets instead of inlining them

Fonts were being base64'd into every payload that used them, so a handful of
files cost tens of megabytes in the repository to say the same thing over and
over. Assets are now written once, content-addressed, under
docs/public/catalog/assets and linked.

Which types can be hosted was settled by fetching one file of each from a
deployed preview: woff2, wav, mp4, svg and the image formats are published,
glb is not. Types the host drops still travel inside the payload, because a
link that 404s is worse than a larger payload.

Also fixes the four type errors the scripts typecheck caught, all of them
unchecked index access on a split() result.

* refactor(catalog): share preview assets instead of repeating them

* style: format the asset module
2026-08-10 17:54:48 -04:00

1 line
8.5 KiB
JSON

{"html":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=1920, height=1080\" />\n <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n html,\n body {\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n background: #eef3f4;\n font-family: -apple-system, BlinkMacSystemFont, \"Helvetica Neue\", Arial, sans-serif;\n }\n\n #root {\n position: relative;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n background: #eef3f4;\n color: #151515;\n }\n\n .camera {\n position: absolute;\n inset: 0;\n z-index: 0;\n transform-origin: 50% 51%;\n will-change: transform, filter;\n }\n\n .map {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .paper-depth {\n position: absolute;\n inset: 0;\n background:\n radial-gradient(circle at 52% 50%, rgba(255, 255, 255, 0.08), transparent 28%),\n linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(10, 20, 24, 0.12));\n mix-blend-mode: multiply;\n pointer-events: none;\n }\n\n .annotation {\n position: absolute;\n left: 652px;\n top: 235px;\n z-index: 4;\n width: 560px;\n height: 392px;\n overflow: visible;\n transform-origin: center;\n will-change: transform, opacity;\n }\n\n .scribble {\n position: absolute;\n inset: 0;\n width: 560px;\n height: 392px;\n overflow: visible;\n }\n\n .scribble path {\n fill: none;\n stroke: #e21d2f;\n stroke-linecap: round;\n stroke-linejoin: round;\n filter: drop-shadow(0 8px 12px rgba(226, 29, 47, 0.24));\n }\n\n #circle-a {\n stroke-width: 12;\n stroke-dasharray: 980;\n stroke-dashoffset: 980;\n }\n\n #circle-b {\n stroke-width: 6;\n opacity: 0.82;\n stroke-dasharray: 910;\n stroke-dashoffset: 910;\n }\n\n .label {\n position: absolute;\n left: 50%;\n top: -146px;\n transform: translateX(-50%) scale(0.82);\n transform-origin: 50% 100%;\n min-width: 370px;\n padding: 18px 26px 20px;\n border-radius: 18px;\n color: #ffffff;\n background: #111111;\n box-shadow:\n 0 22px 54px rgba(25, 4, 7, 0.24),\n inset 0 0 0 1px rgba(255, 255, 255, 0.12);\n font-size: 60px;\n line-height: 0.9;\n font-weight: 900;\n letter-spacing: 0;\n text-align: center;\n text-transform: uppercase;\n opacity: 0;\n }\n\n .label::after {\n content: \"\";\n position: absolute;\n left: 50%;\n bottom: -16px;\n width: 32px;\n height: 32px;\n background: #111111;\n transform: translateX(-50%) rotate(45deg);\n }\n\n .red-wash {\n position: absolute;\n inset: 0;\n z-index: 2;\n background:\n radial-gradient(\n circle at 50% 48%,\n rgba(255, 59, 48, 0.3),\n rgba(255, 59, 48, 0.06) 42%,\n rgba(79, 0, 8, 0.22) 100%\n ),\n linear-gradient(90deg, rgba(99, 0, 12, 0.12), rgba(255, 59, 48, 0.2));\n opacity: 0;\n mix-blend-mode: multiply;\n pointer-events: none;\n }\n\n .scanline {\n position: absolute;\n inset: 0;\n z-index: 3;\n opacity: 0;\n background: repeating-linear-gradient(\n 0deg,\n rgba(65, 0, 0, 0.12) 0,\n rgba(65, 0, 0, 0.12) 1px,\n transparent 1px,\n transparent 7px\n );\n mix-blend-mode: multiply;\n pointer-events: none;\n }\n\n .corner {\n position: absolute;\n z-index: 6;\n width: 84px;\n height: 84px;\n border-color: rgba(17, 17, 17, 0.62);\n opacity: 0;\n }\n\n .corner.tl {\n left: 86px;\n top: 78px;\n border-top: 5px solid;\n border-left: 5px solid;\n }\n\n .corner.tr {\n right: 86px;\n top: 78px;\n border-top: 5px solid;\n border-right: 5px solid;\n }\n\n .corner.bl {\n left: 86px;\n bottom: 78px;\n border-bottom: 5px solid;\n border-left: 5px solid;\n }\n\n .corner.br {\n right: 86px;\n bottom: 78px;\n border-bottom: 5px solid;\n border-right: 5px solid;\n }\n\n .attribution {\n position: absolute;\n right: 28px;\n bottom: 22px;\n z-index: 7;\n color: rgba(17, 17, 17, 0.44);\n font-size: 19px;\n font-weight: 650;\n opacity: 0.7;\n }\n </style>\n </head>\n <body>\n <div\n id=\"root\"\n data-composition-id=\"north-korea-locked-down\"\n data-root=\"true\"\n data-start=\"0\"\n data-duration=\"7\"\n data-width=\"1920\"\n data-height=\"1080\"\n >\n <div id=\"camera\" class=\"camera\">\n <img class=\"map\" src=\"/public/catalog/assets/38198e4cc4ae82c0.png\" alt=\"\" />\n <div class=\"paper-depth\"></div>\n </div>\n\n <div id=\"annotation\" class=\"annotation\">\n <div id=\"label\" class=\"label\">LOCKED<br />DOWN</div>\n <svg class=\"scribble\" viewBox=\"0 0 560 392\" aria-hidden=\"true\" data-layout-ignore>\n <path\n id=\"circle-a\"\n d=\"M 74 211 C 42 119 118 42 254 36 C 410 30 524 111 508 222 C 494 320 350 377 208 346 C 94 322 37 278 74 211\"\n />\n <path\n id=\"circle-b\"\n d=\"M 92 231 C 33 150 105 55 243 44 C 408 31 529 126 501 244 C 480 334 335 370 197 342 C 83 319 47 269 92 231\"\n />\n </svg>\n </div>\n\n <div id=\"redWash\" class=\"red-wash\"></div>\n <div id=\"scanline\" class=\"scanline\"></div>\n <div class=\"corner tl\"></div>\n <div class=\"corner tr\"></div>\n <div class=\"corner bl\"></div>\n <div class=\"corner br\"></div>\n <div class=\"attribution\">© OpenStreetMap contributors © CARTO</div>\n </div>\n\n <script>\n window.__timelines = window.__timelines || {};\n\n const tl = gsap.timeline({\n paused: true,\n defaults: { overwrite: \"auto\" },\n });\n\n tl.fromTo(\n \"#camera\",\n { scale: 0.92, x: -70, y: 18, filter: \"saturate(0.92) contrast(0.98)\" },\n {\n scale: 2.12,\n x: -56,\n y: -22,\n filter: \"saturate(1.08) contrast(1.04)\",\n duration: 3.18,\n ease: \"expo.inOut\",\n },\n 0,\n );\n tl.to(\"#camera\", { scale: 2.2, x: -44, y: -34, duration: 0.42, ease: \"back.out(1.5)\" }, 3.18);\n tl.from(\n \".corner\",\n { opacity: 0, scale: 0.72, duration: 0.28, stagger: 0.035, ease: \"power3.out\" },\n 0.42,\n );\n tl.to(\".corner\", { opacity: 0.72, duration: 0.18, ease: \"power2.out\" }, 1.2);\n tl.fromTo(\n \"#annotation\",\n { opacity: 0, scale: 0.92, x: 4, y: -4 },\n { opacity: 1, scale: 1, x: 0, y: 0, duration: 0.16, ease: \"power2.out\" },\n 3.12,\n );\n tl.to(\"#circle-a\", { strokeDashoffset: 0, duration: 0.62, ease: \"power2.out\" }, 3.24);\n tl.to(\"#circle-b\", { strokeDashoffset: 0, duration: 0.44, ease: \"power3.out\" }, 3.46);\n tl.to(\"#label\", { opacity: 1, scale: 1, duration: 0.28, ease: \"back.out(2.1)\" }, 3.78);\n tl.to(\"#redWash\", { opacity: 1, duration: 0.24, ease: \"power2.out\" }, 3.82);\n tl.to(\"#scanline\", { opacity: 0.44, duration: 0.18, ease: \"none\" }, 3.86);\n tl.to(\n \"#annotation\",\n { scale: 1.045, duration: 0.14, yoyo: true, repeat: 1, ease: \"sine.inOut\" },\n 4.1,\n );\n tl.to(\"#camera\", { scale: 2.06, x: -82, y: -8, duration: 2.35, ease: \"sine.inOut\" }, 4.2);\n tl.to(\"#redWash\", { opacity: 0.82, duration: 2.35, ease: \"sine.inOut\" }, 4.2);\n tl.to(\".corner\", { opacity: 0.38, duration: 0.72, ease: \"sine.inOut\" }, 5.82);\n tl.to(\"#label\", { y: -6, duration: 0.4, yoyo: true, repeat: 1, ease: \"sine.inOut\" }, 5.94);\n\n window.__timelines[\"north-korea-locked-down\"] = tl;\n </script>\n </body>\n</html>\n"}