@autoaiai/aicut 0.1.7 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANUAL.md +94 -84
- package/package.json +1 -1
- package/product.json +2 -1
- package/skills/aicut/SKILL.md +4 -4
- package/skills/aicut/references/commands.md +15 -15
- package/skills/aicut/references/film-format.md +1 -1
- package/skills/aicut/references/recipes.md +4 -4
- package/skills/aicut/references/troubleshooting.md +2 -2
- package/skills/aicut-motion/SKILL.md +75 -9
- package/skills/aicut-motion/THIRD_PARTY_NOTICES.md +14 -0
- package/skills/aicut-motion/assets/atlas/lib/harness.js +116 -0
- package/skills/aicut-motion/assets/atlas/lib/sound.js +274 -0
- package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/code.js +937 -0
- package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/code.js +909 -0
- package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/code.js +881 -0
- package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/code.js +897 -0
- package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/code.js +814 -0
- package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/code.js +990 -0
- package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/code.js +994 -0
- package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/code.js +792 -0
- package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/code.js +840 -0
- package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/code.js +1126 -0
- package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/code.js +854 -0
- package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/code.js +1124 -0
- package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/code.js +728 -0
- package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/code.js +664 -0
- package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/code.js +991 -0
- package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/code.js +801 -0
- package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/code.js +692 -0
- package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/code.js +756 -0
- package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/code.js +922 -0
- package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/code.js +852 -0
- package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/code.js +724 -0
- package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/code.js +793 -0
- package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/code.js +846 -0
- package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/code.js +1008 -0
- package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/code.js +680 -0
- package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/code.js +633 -0
- package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/code.js +757 -0
- package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/code.js +828 -0
- package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/code.js +697 -0
- package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/code.js +687 -0
- package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/code.js +727 -0
- package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/code.js +903 -0
- package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/code.js +662 -0
- package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/code.js +951 -0
- package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/code.js +759 -0
- package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/code.js +738 -0
- package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/code.js +793 -0
- package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/code.js +673 -0
- package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/code.js +907 -0
- package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/code.js +833 -0
- package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/041-manga-page/code.js +845 -0
- package/skills/aicut-motion/assets/atlas/styles/041-manga-page/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/041-manga-page/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/code.js +496 -0
- package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/043-light-painting/code.js +635 -0
- package/skills/aicut-motion/assets/atlas/styles/043-light-painting/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/043-light-painting/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/code.js +799 -0
- package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/code.js +547 -0
- package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/code.js +592 -0
- package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/code.js +768 -0
- package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/048-transit-map/code.js +666 -0
- package/skills/aicut-motion/assets/atlas/styles/048-transit-map/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/048-transit-map/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/code.js +559 -0
- package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/code.js +943 -0
- package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/code.js +606 -0
- package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/code.js +730 -0
- package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/code.js +782 -0
- package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/code.js +504 -0
- package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/code.js +517 -0
- package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/code.js +713 -0
- package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/code.js +717 -0
- package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/code.js +540 -0
- package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/code.js +524 -0
- package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/code.js +934 -0
- package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/code.js +690 -0
- package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/code.js +612 -0
- package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/code.js +666 -0
- package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/code.js +651 -0
- package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/code.js +656 -0
- package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/code.js +672 -0
- package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/code.js +548 -0
- package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/code.js +989 -0
- package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/code.js +440 -0
- package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/code.js +895 -0
- package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/code.js +563 -0
- package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/code.js +627 -0
- package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/code.js +1096 -0
- package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/code.js +595 -0
- package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/code.js +665 -0
- package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/code.js +645 -0
- package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/code.js +436 -0
- package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/code.js +1237 -0
- package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/code.js +622 -0
- package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/code.js +524 -0
- package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/code.js +689 -0
- package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/code.js +574 -0
- package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/code.js +469 -0
- package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/084-flow-field/code.js +531 -0
- package/skills/aicut-motion/assets/atlas/styles/084-flow-field/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/084-flow-field/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/code.js +675 -0
- package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/style.json +13 -0
- package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/code.js +609 -0
- package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/poster.jpg +0 -0
- package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/style.json +13 -0
- package/skills/aicut-motion/references/design-systems/README.md +1 -1
- package/skills/aicut-motion/references/motion-delivery.md +55 -0
- package/skills/aicut-motion/references/pages.md +1 -1
- package/skills/aicut-motion/references/style-index.json +690 -0
- package/skills/aicut-motion/references/style-research.md +73 -0
- package/skills/aicut-motion/references/style-selection.md +71 -0
- package/skills/aicut-motion/scripts/atlas-viewer.html +195 -0
- package/skills/aicut-motion/scripts/atlas.mjs +109 -0
- package/studio/client.mjs +3 -3
- package/studio/server/fetch-github.mjs +14 -3
- package/studio/server/server.mjs +71 -6
- package/studio/server/skill-summaries.mjs +60 -0
- package/studio/server/skills.mjs +121 -7
- package/studio/server/styles-store.mjs +235 -0
- package/studio/server/template-posters.mjs +2 -2
- package/studio/server/templates-page.mjs +93 -93
- package/studio/server/templates.mjs +32 -7
- package/studio/ui/dist/assets/ExportCenter-DasulhAv.js +1 -0
- package/studio/ui/dist/assets/SettingsModal-BZw0Hz2_.js +2 -0
- package/studio/ui/dist/assets/apple-touch-icon-ClxQ7j-O.png +0 -0
- package/studio/ui/dist/assets/es-C8XPZARI.js +1 -0
- package/studio/ui/dist/assets/favicon-Blwx8Trs.png +0 -0
- package/studio/ui/dist/assets/index-BjJVmBzB.js +1623 -0
- package/studio/ui/dist/assets/index-C3BfKZFO.css +1 -0
- package/studio/ui/dist/assets/ja-BeNkdaEj.js +1 -0
- package/studio/ui/dist/assets/ko-DcTVhdzK.js +1 -0
- package/studio/ui/dist/assets/zh-CN-Cg51ctbL.js +1 -0
- package/studio/ui/dist/index.html +4 -3
- package/studio/ui/dist/assets/ExportCenter-F-2vF-lQ.js +0 -1
- package/studio/ui/dist/assets/SettingsModal-BgVYguNu.js +0 -2
- package/studio/ui/dist/assets/es-BRUzvuoM.js +0 -1
- package/studio/ui/dist/assets/index-Bhg-9dA3.css +0 -1
- package/studio/ui/dist/assets/index-CWur1Iw9.js +0 -1625
- package/studio/ui/dist/assets/ja-Cth-uDcW.js +0 -1
- package/studio/ui/dist/assets/ko-upvw591D.js +0 -1
- package/studio/ui/dist/assets/zh-CN-BCfJFazB.js +0 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": 25,
|
|
3
|
+
"name": "Cathedral Glass",
|
|
4
|
+
"nameZh": "教堂彩绘玻璃",
|
|
5
|
+
"family": "Painterly",
|
|
6
|
+
"familyZh": "绘画质感",
|
|
7
|
+
"description": "A leaded Gothic window: hand-cut pieces of streaky, seedy antique glass held in thick dark came, painted grisaille details, and light blazing through into a dim stone interior with dusty rays.",
|
|
8
|
+
"motionNotes": "Motion: Daylight sweeps slowly across the window so pieces brighten in turn, the rays drift with floating dust, and the chip in the saint's hands pulses.",
|
|
9
|
+
"explainerNotes": "Explainer: Told as a cycle of three leaded windows along a nave, the camera walking from window to window; arrows are strips of glass in lead came with glass arrowheads, labels and the counters are grisaille painted on pale banner glass, and emphasis is ruby glass rings and light flaring through the pieces.",
|
|
10
|
+
"code": "code.js",
|
|
11
|
+
"registeredAs": "stained-glass",
|
|
12
|
+
"source": "https://www.futurepedia.io/resources/ai-style-atlas/view"
|
|
13
|
+
}
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Prestige-newspaper data graphic (FT / NYT / Economist register).
|
|
3
|
+
const BG = '#FFF1E5', INK = '#33302E', MUTED = '#66605C', GRID = 'rgba(51,48,46,0.16)';
|
|
4
|
+
const HI = '#990F3D', GREY = '#A79E95';
|
|
5
|
+
|
|
6
|
+
// Illustrative annual data, $bn
|
|
7
|
+
const YEARS = [2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026];
|
|
8
|
+
const SERIES = [
|
|
9
|
+
{ name: 'Artificial intelligence', hi: true, v: [11, 16, 24, 37, 50, 63, 58, 92, 104, 176, 262, 348, 405] },
|
|
10
|
+
{ name: 'Cloud infrastructure', v: [58, 66, 77, 88, 101, 116, 138, 152, 163, 171, 182, 191, 198] },
|
|
11
|
+
{ name: 'Cybersecurity', v: [28, 32, 36, 41, 47, 52, 60, 73, 82, 86, 92, 98, 104] },
|
|
12
|
+
{ name: 'Robotics', v: [19, 21, 24, 28, 31, 33, 30, 38, 44, 47, 52, 58, 63] },
|
|
13
|
+
{ name: 'AR/VR headsets', v: [4, 7, 11, 14, 17, 19, 24, 36, 31, 25, 22, 21, 22] },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// quarterly interpolation with a little seeded texture so lines feel like real data
|
|
17
|
+
function quarterly(v, seed, jitter) {
|
|
18
|
+
const r = H.rng(seed), out = [];
|
|
19
|
+
for (let i = 0; i < v.length - 1; i++) {
|
|
20
|
+
const p0 = v[Math.max(0, i - 1)], p1 = v[i], p2 = v[i + 1], p3 = v[Math.min(v.length - 1, i + 2)];
|
|
21
|
+
for (let q = 0; q < 4; q++) {
|
|
22
|
+
const f = q / 4, f2 = f * f, f3 = f2 * f;
|
|
23
|
+
// Catmull-Rom between annual points
|
|
24
|
+
const base = 0.5 * (2 * p1 + (-p0 + p2) * f + (2 * p0 - 5 * p1 + 4 * p2 - p3) * f2 + (-p0 + 3 * p1 - 3 * p2 + p3) * f3);
|
|
25
|
+
out.push(base + (i + q > 0 ? r.gauss() * jitter * Math.sqrt(Math.max(1, base)) : 0));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
out.push(v[v.length - 1]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function smallCaps(ctx, str, x, y, size, color, track, align) {
|
|
33
|
+
// faux small caps: capitals at full size, lower-case set as smaller capitals
|
|
34
|
+
ctx.fillStyle = color;
|
|
35
|
+
const big = `600 ${size}px "Inter"`, sm = `600 ${Math.round(size * 0.78)}px "Inter"`;
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (const ch of str) {
|
|
38
|
+
const up = ch === ch.toUpperCase() && ch !== ch.toLowerCase();
|
|
39
|
+
parts.push({ c: ch.toUpperCase(), f: up || /[0-9©]/.test(ch) ? big : sm });
|
|
40
|
+
}
|
|
41
|
+
let w = 0;
|
|
42
|
+
for (const p of parts) { ctx.font = p.f; w += ctx.measureText(p.c).width + track; }
|
|
43
|
+
let cx = align === 'right' ? x - w : x;
|
|
44
|
+
ctx.textAlign = 'left';
|
|
45
|
+
for (const p of parts) { ctx.font = p.f; ctx.fillText(p.c, cx, y); cx += ctx.measureText(p.c).width + track; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// hand-drawn leader: wobbly quadratic with a small open arrowhead
|
|
49
|
+
function leader(ctx, x0, y0, cx, cy, x1, y1, seed, color, prog = 1, lw = 1.25) {
|
|
50
|
+
if (prog <= 0) return;
|
|
51
|
+
const r = H.rng(seed);
|
|
52
|
+
ctx.save();
|
|
53
|
+
ctx.strokeStyle = color; ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
54
|
+
ctx.beginPath();
|
|
55
|
+
let px = 0, py = 0;
|
|
56
|
+
const nI = Math.max(1, Math.round(28 * prog));
|
|
57
|
+
for (let i = 0; i <= nI; i++) {
|
|
58
|
+
const u = i / 28, a = (1 - u) * (1 - u), b = 2 * (1 - u) * u, c = u * u;
|
|
59
|
+
const wob = Math.sin(u * Math.PI) * 0.8;
|
|
60
|
+
px = a * x0 + b * cx + c * x1 + r.gauss() * 0.25 * wob;
|
|
61
|
+
py = a * y0 + b * cy + c * y1 + r.gauss() * 0.25 * wob;
|
|
62
|
+
i ? ctx.lineTo(px, py) : ctx.moveTo(px, py);
|
|
63
|
+
}
|
|
64
|
+
ctx.stroke();
|
|
65
|
+
if (prog < 0.97) { ctx.restore(); return; }
|
|
66
|
+
// arrowhead from tangent at end
|
|
67
|
+
const tx = x1 - cx, ty = y1 - cy, L = Math.hypot(tx, ty) || 1, ux = tx / L, uy = ty / L;
|
|
68
|
+
const hl = 8;
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.moveTo(x1 - ux * hl - uy * hl * 0.55, y1 - uy * hl + ux * hl * 0.55);
|
|
71
|
+
ctx.lineTo(x1 + r.gauss() * 0.2, y1);
|
|
72
|
+
ctx.lineTo(x1 - ux * hl + uy * hl * 0.5, y1 - uy * hl - ux * hl * 0.5);
|
|
73
|
+
ctx.stroke();
|
|
74
|
+
ctx.restore();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// handwritten write-on: reveal text left to right
|
|
78
|
+
function writeOn(ctx, str, x, y, p) {
|
|
79
|
+
if (p <= 0) return;
|
|
80
|
+
const w = ctx.measureText(str).width;
|
|
81
|
+
ctx.save(); ctx.beginPath(); ctx.rect(x - 4, y - 30, (w + 8) * H.clamp(p), 42); ctx.clip();
|
|
82
|
+
ctx.fillText(str, x, y); ctx.restore();
|
|
83
|
+
}
|
|
84
|
+
const seg = (t, a, b) => H.clamp((t - a) / (b - a));
|
|
85
|
+
|
|
86
|
+
// ---- explainer helpers: line icons in the graphic's hairline style ----
|
|
87
|
+
function catIcon(ctx, x, y, s, col) {
|
|
88
|
+
ctx.save(); ctx.translate(x, y); ctx.scale(s, s); ctx.strokeStyle = col; ctx.fillStyle = col; ctx.lineWidth = 1.6 / s; ctx.lineJoin = 'round';
|
|
89
|
+
ctx.beginPath(); ctx.moveTo(-12, -4); ctx.lineTo(-11, -17); ctx.lineTo(-4, -10); ctx.quadraticCurveTo(0, -11.5, 4, -10); ctx.lineTo(11, -17); ctx.lineTo(12, -4);
|
|
90
|
+
ctx.quadraticCurveTo(13, 11, 0, 12); ctx.quadraticCurveTo(-13, 11, -12, -4); ctx.closePath(); ctx.stroke();
|
|
91
|
+
ctx.beginPath(); ctx.arc(-5, -1, 1.6, 0, 7); ctx.arc(5, -1, 1.6, 0, 7); ctx.fill();
|
|
92
|
+
ctx.beginPath(); ctx.moveTo(-1.5, 4); ctx.lineTo(1.5, 4); ctx.lineTo(0, 5.8); ctx.closePath(); ctx.fill();
|
|
93
|
+
ctx.beginPath(); ctx.moveTo(-3, 6); ctx.lineTo(-15, 4); ctx.moveTo(-3, 7.5); ctx.lineTo(-14, 9); ctx.moveTo(3, 6); ctx.lineTo(15, 4); ctx.moveTo(3, 7.5); ctx.lineTo(14, 9); ctx.stroke();
|
|
94
|
+
ctx.restore();
|
|
95
|
+
}
|
|
96
|
+
function dogIcon(ctx, x, y, s, col) {
|
|
97
|
+
ctx.save(); ctx.translate(x, y); ctx.scale(s, s); ctx.strokeStyle = col; ctx.fillStyle = col; ctx.lineWidth = 1.6 / s; ctx.lineJoin = 'round';
|
|
98
|
+
ctx.beginPath(); ctx.moveTo(-9, -10); ctx.quadraticCurveTo(0, -15, 9, -10); ctx.quadraticCurveTo(12, 0, 8, 7); ctx.quadraticCurveTo(0, 14, -8, 7); ctx.quadraticCurveTo(-12, 0, -9, -10); ctx.stroke();
|
|
99
|
+
ctx.beginPath(); ctx.ellipse(-12, -2, 4, 9, 0.35, 0, 7); ctx.ellipse(12, -2, 4, 9, -0.35, 0, 7); ctx.fill();
|
|
100
|
+
ctx.beginPath(); ctx.arc(-4, -3, 1.6, 0, 7); ctx.arc(4, -3, 1.6, 0, 7); ctx.fill();
|
|
101
|
+
ctx.beginPath(); ctx.ellipse(0, 5, 3.2, 2.3, 0, 0, 7); ctx.fill();
|
|
102
|
+
ctx.restore();
|
|
103
|
+
}
|
|
104
|
+
// hand-drawn emphasis ring (wobbly, overshooting like a marker circle)
|
|
105
|
+
function handRing(ctx, x, y, rx, ry, p, seed, col, lw = 1.8) {
|
|
106
|
+
if (p <= 0) return;
|
|
107
|
+
const r = H.rng(seed);
|
|
108
|
+
ctx.save(); ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.beginPath();
|
|
109
|
+
const n = Math.max(2, Math.floor(64 * p)), a0 = -2.4;
|
|
110
|
+
for (let k = 0; k <= n; k++) { const a = a0 + k / 64 * Math.PI * 2.25, w = 1 + 0.06 * Math.sin(k * 0.35) + k * 0.0012; const px = x + Math.cos(a) * rx * w, py = y + Math.sin(a) * ry * w; k ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }
|
|
111
|
+
ctx.stroke(); ctx.restore();
|
|
112
|
+
}
|
|
113
|
+
// a small network drawn as an editorial diagram: nodes + hairline weights
|
|
114
|
+
function miniNet(ctx, x, y, w, h, weights, hot, nr = 7) {
|
|
115
|
+
const cols = [3, 4, 2];
|
|
116
|
+
const P = cols.map((n, c) => Array.from({ length: n }, (_, i) => [x + c * w / 2, y + (i + 0.5) * h / n]));
|
|
117
|
+
let k = 0;
|
|
118
|
+
for (let c = 0; c < 2; c++) for (const a of P[c]) for (const b of P[c + 1]) {
|
|
119
|
+
const wt = weights[k++ % weights.length];
|
|
120
|
+
// hairline-but-dark like the hero's grid: readable at video size even for near-zero weights
|
|
121
|
+
ctx.strokeStyle = wt > 0 ? H.rgba(HI, Math.min(1, 0.5 + 0.7 * Math.abs(wt))) : H.rgba('#857D75', Math.min(1, 0.42 + 0.7 * Math.abs(wt)));
|
|
122
|
+
ctx.lineWidth = 1.4 + 2.4 * Math.abs(wt);
|
|
123
|
+
ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
|
|
124
|
+
}
|
|
125
|
+
for (const col of P) for (const [px, py] of col) {
|
|
126
|
+
ctx.fillStyle = BG; ctx.beginPath(); ctx.arc(px, py, nr, 0, 7); ctx.fill();
|
|
127
|
+
ctx.strokeStyle = hot ? HI : INK; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(px, py, nr, 0, 7); ctx.stroke();
|
|
128
|
+
}
|
|
129
|
+
return P;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
registerStyle({
|
|
133
|
+
id: 'editorial-chart',
|
|
134
|
+
name: 'Editorial Chart',
|
|
135
|
+
category: 'Data & Information',
|
|
136
|
+
description: 'A prestige-newspaper data graphic on FT salmon: serif headline and dek, hairline grid, one claret highlight series against quiet greys, direct labels and hand-annotated callouts, small-caps source line.',
|
|
137
|
+
motion: 'Headline sets, gridlines rule across, grey context lines settle, then the claret line draws left to right with its wash filling behind it and handwritten notes inking in as the line reaches them; a slow push and a soft pulse on the end point hold the shot.',
|
|
138
|
+
duration: 6,
|
|
139
|
+
fonts: ['700 34px "Libre Baskerville"', '400 17px "Libre Baskerville"', 'italic 400 15px "Libre Baskerville"',
|
|
140
|
+
'600 13px "Inter"', '400 13px "Inter"', '800 13px "Inter"', '700 22px "Caveat"', '700 92px "Libre Baskerville"', '700 46px "Libre Baskerville"', '600 28px "Inter"', '400 26px "Inter"'],
|
|
141
|
+
frameT: 5.9,
|
|
142
|
+
readsPixels: true,
|
|
143
|
+
draw(ctx, W, H_, t) {
|
|
144
|
+
// ---- paper ----
|
|
145
|
+
ctx.fillStyle = BG; ctx.fillRect(0, 0, W, H_);
|
|
146
|
+
// slow push-in across the shot
|
|
147
|
+
ctx.save();
|
|
148
|
+
const push = 1 + 0.018 * H.ease.inOut(seg(t, 1.5, 6));
|
|
149
|
+
ctx.translate(W / 2, H_ * 0.55); ctx.scale(push, push); ctx.translate(-W / 2, -H_ * 0.55);
|
|
150
|
+
|
|
151
|
+
const L = 84, R = 1080, T = 214, B = 596;
|
|
152
|
+
const yMax = 450;
|
|
153
|
+
const X = (yr) => L + (yr - 2014) / 12 * (R - L);
|
|
154
|
+
const Y = (v) => B - v / yMax * (B - T);
|
|
155
|
+
const reveal = H.ease.inOut(seg(t, 2.0, 4.0));
|
|
156
|
+
const greyIn = H.ease.out(seg(t, 1.1, 2.2));
|
|
157
|
+
|
|
158
|
+
// ---- masthead rule + kicker ----
|
|
159
|
+
const ruleP = H.ease.out(seg(t, 0, 0.5));
|
|
160
|
+
ctx.fillStyle = INK; ctx.fillRect(L, 40, 1116 * ruleP, 3);
|
|
161
|
+
ctx.fillStyle = HI; ctx.fillRect(L, 40, 46 * H.clamp(ruleP * 4), 7);
|
|
162
|
+
ctx.globalAlpha = seg(t, 0.2, 0.5);
|
|
163
|
+
smallCaps(ctx, 'Artificial Intelligence', L, 76, 13, HI, 1.6);
|
|
164
|
+
ctx.globalAlpha = 1;
|
|
165
|
+
|
|
166
|
+
// ---- headline & dek: set line by line with a short rise ----
|
|
167
|
+
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
|
|
168
|
+
const setIn = (str, font, col, y, a, b) => {
|
|
169
|
+
const p = H.ease.out(seg(t, a, b)); if (p <= 0) return;
|
|
170
|
+
ctx.globalAlpha = p; ctx.fillStyle = col; ctx.font = font; ctx.fillText(str, L, y + (1 - p) * 10); ctx.globalAlpha = 1;
|
|
171
|
+
};
|
|
172
|
+
setIn('AI spending leaves the rest behind', '700 34px "Libre Baskerville"', INK, 120, 0.15, 0.75);
|
|
173
|
+
setIn('Global corporate investment by technology category, $bn', '400 17px "Libre Baskerville"', MUTED, 152, 0.35, 0.9);
|
|
174
|
+
setIn('Annual, 2014–2026. Figures are illustrative', '400 13px "Inter"', MUTED, 177, 0.5, 1.0);
|
|
175
|
+
|
|
176
|
+
// ---- forecast band ----
|
|
177
|
+
const fx0 = X(2025), fx1 = X(2026);
|
|
178
|
+
const fb = seg(t, 1.2, 1.7);
|
|
179
|
+
ctx.fillStyle = `rgba(51,48,46,${0.055 * fb})`; ctx.fillRect(fx0, T - 6, fx1 - fx0, B - T + 6);
|
|
180
|
+
ctx.globalAlpha = fb;
|
|
181
|
+
ctx.font = 'italic 400 13px "Libre Baskerville"'; ctx.fillStyle = MUTED; ctx.textAlign = 'center';
|
|
182
|
+
ctx.fillText('Forecast', (fx0 + fx1) / 2, T + 10);
|
|
183
|
+
ctx.globalAlpha = 1;
|
|
184
|
+
|
|
185
|
+
// ---- gridlines rule across, bottom first; labels sit on the line (NYT style) ----
|
|
186
|
+
ctx.textAlign = 'left'; ctx.font = '400 13px "Inter"';
|
|
187
|
+
for (let v = 0; v <= 400; v += 100) {
|
|
188
|
+
const k = v / 100, gp = H.ease.inOut(seg(t, 0.5 + k * 0.09, 1.05 + k * 0.09));
|
|
189
|
+
if (gp <= 0) continue;
|
|
190
|
+
const y = Y(v) + 0.5;
|
|
191
|
+
ctx.fillStyle = v === 0 ? INK : GRID;
|
|
192
|
+
ctx.fillRect(L, Math.round(y) - (v === 0 ? 0.5 : 0), (R - L + 24) * gp, v === 0 ? 1.4 : 1);
|
|
193
|
+
if (v) { ctx.globalAlpha = seg(gp, 0.3, 1); ctx.fillStyle = MUTED; ctx.fillText(v === 400 ? '$400bn' : String(v), L, y - 6); ctx.globalAlpha = 1; }
|
|
194
|
+
}
|
|
195
|
+
// x ticks
|
|
196
|
+
ctx.textAlign = 'center';
|
|
197
|
+
for (let yr = 2014; yr <= 2026; yr += 1) {
|
|
198
|
+
const a = seg(t, 0.75 + (yr - 2014) * 0.035, 1.05 + (yr - 2014) * 0.035); if (a <= 0) continue;
|
|
199
|
+
ctx.globalAlpha = a;
|
|
200
|
+
const x = Math.round(X(yr)) + 0.5;
|
|
201
|
+
ctx.fillStyle = INK; ctx.fillRect(x - 0.5, B, 1, yr % 2 === 0 ? 6 : 3);
|
|
202
|
+
if (yr % 2 === 0) { ctx.fillStyle = MUTED; ctx.fillText(String(yr), x, B + 24); }
|
|
203
|
+
ctx.globalAlpha = 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- grey context lines with direct labels ----
|
|
207
|
+
const pts = SERIES.map((s, i) => {
|
|
208
|
+
const q = quarterly(s.v, 100 + i, s.hi ? 0.18 : 0.12);
|
|
209
|
+
return q.map((v, k) => [X(2014 + k / 4), Y(v)]);
|
|
210
|
+
});
|
|
211
|
+
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
|
|
212
|
+
SERIES.forEach((s, i) => {
|
|
213
|
+
if (s.hi) return;
|
|
214
|
+
const p = pts[i];
|
|
215
|
+
const gp = H.ease.inOut(seg(t, 1.0 + i * 0.1, 1.9 + i * 0.1)); if (gp <= 0) return;
|
|
216
|
+
ctx.save(); ctx.beginPath(); ctx.rect(0, 0, L + (R - L) * gp + 2, H_); ctx.clip();
|
|
217
|
+
ctx.strokeStyle = GREY; ctx.lineWidth = 1.8;
|
|
218
|
+
ctx.beginPath(); p.forEach(([x, y], k) => (k ? ctx.lineTo(x, y) : ctx.moveTo(x, y))); ctx.stroke();
|
|
219
|
+
ctx.restore();
|
|
220
|
+
});
|
|
221
|
+
// label positions (nudged to avoid collisions)
|
|
222
|
+
const labY = { 'Cloud infrastructure': 0, 'Cybersecurity': 0, 'Robotics': 0, 'AR/VR headsets': 0 };
|
|
223
|
+
const ends = SERIES.map((s, i) => ({ s, y: pts[i][pts[i].length - 1][1] })).filter((e) => !e.s.hi).sort((a, b) => a.y - b.y);
|
|
224
|
+
for (let k = 1; k < ends.length; k++) if (ends[k].y - ends[k - 1].y < 17) ends[k].y = ends[k - 1].y + 17;
|
|
225
|
+
ctx.textAlign = 'left'; ctx.font = '400 13px "Inter"';
|
|
226
|
+
ends.forEach((e) => {
|
|
227
|
+
const last = pts[SERIES.indexOf(e.s)].slice(-1)[0];
|
|
228
|
+
ctx.globalAlpha = seg(t, 2.0, 2.4);
|
|
229
|
+
ctx.fillStyle = '#857D75'; ctx.fillText(e.s.name, R + 10, e.y + 4.5);
|
|
230
|
+
if (Math.abs(e.y - last[1]) > 2) { ctx.strokeStyle = 'rgba(133,125,117,0.6)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(R + 2, last[1]); ctx.lineTo(R + 8, e.y); ctx.stroke(); }
|
|
231
|
+
ctx.globalAlpha = 1;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---- highlight series: area wash + line, revealed by clip ----
|
|
235
|
+
const hp = pts[0];
|
|
236
|
+
const clipX = L + (R - L) * reveal;
|
|
237
|
+
ctx.save();
|
|
238
|
+
ctx.beginPath(); ctx.rect(0, 0, reveal > 0 ? clipX + 2 : 0, H_); ctx.clip();
|
|
239
|
+
// area
|
|
240
|
+
const g = ctx.createLinearGradient(0, T, 0, B);
|
|
241
|
+
g.addColorStop(0, 'rgba(153,15,61,0.20)'); g.addColorStop(1, 'rgba(153,15,61,0.04)');
|
|
242
|
+
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(hp[0][0], B);
|
|
243
|
+
hp.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.lineTo(hp[hp.length - 1][0], B); ctx.closePath(); ctx.fill();
|
|
244
|
+
// solid actuals, dashed forecast
|
|
245
|
+
const split = hp.findIndex(([x]) => x >= fx0 - 0.1);
|
|
246
|
+
ctx.strokeStyle = HI; ctx.lineWidth = 3.4;
|
|
247
|
+
ctx.beginPath(); for (let k = 0; k <= split; k++) (k ? ctx.lineTo(...hp[k]) : ctx.moveTo(...hp[k])); ctx.stroke();
|
|
248
|
+
ctx.setLineDash([5, 5]); ctx.lineWidth = 3;
|
|
249
|
+
ctx.beginPath(); for (let k = split; k < hp.length; k++) (k > split ? ctx.lineTo(...hp[k]) : ctx.moveTo(...hp[k])); ctx.stroke();
|
|
250
|
+
ctx.setLineDash([]);
|
|
251
|
+
ctx.restore();
|
|
252
|
+
|
|
253
|
+
// drawing head: a small dot riding the tip of the line while it draws
|
|
254
|
+
if (reveal > 0 && reveal < 1) {
|
|
255
|
+
let k = hp.findIndex(([x]) => x >= clipX); if (k < 1) k = 1;
|
|
256
|
+
const [ax, ay] = hp[k - 1], [bx, by] = hp[k], f = H.clamp((clipX - ax) / (bx - ax || 1));
|
|
257
|
+
ctx.fillStyle = HI; ctx.beginPath(); ctx.arc(ax + (bx - ax) * f, ay + (by - ay) * f, 4.2, 0, 7); ctx.fill();
|
|
258
|
+
}
|
|
259
|
+
// end marker + direct label
|
|
260
|
+
const endP = hp[hp.length - 1];
|
|
261
|
+
const pop = seg(t, 3.95, 4.3);
|
|
262
|
+
if (pop > 0) {
|
|
263
|
+
// live pulse on the latest value during the hold
|
|
264
|
+
for (let k = 0; k < 2; k++) {
|
|
265
|
+
const ph = ((t - 4.3) / 1.4 + k * 0.5) % 1;
|
|
266
|
+
if (t > 4.3) { ctx.strokeStyle = H.rgba(HI, 0.35 * (1 - ph)); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(endP[0], endP[1], 6 + ph * 16, 0, 7); ctx.stroke(); }
|
|
267
|
+
}
|
|
268
|
+
const sc = H.ease.back(pop);
|
|
269
|
+
ctx.fillStyle = BG; ctx.beginPath(); ctx.arc(endP[0], endP[1], 6.5 * sc, 0, 7); ctx.fill();
|
|
270
|
+
ctx.strokeStyle = HI; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(endP[0], endP[1], Math.max(0.1, 5 * sc), 0, 7); ctx.stroke();
|
|
271
|
+
ctx.textAlign = 'left'; ctx.globalAlpha = seg(t, 4.05, 4.45);
|
|
272
|
+
ctx.font = '800 13px "Inter"'; ctx.fillStyle = HI;
|
|
273
|
+
ctx.fillText('Artificial', R + 14, endP[1] - 3);
|
|
274
|
+
ctx.fillText('intelligence', R + 14, endP[1] + 13);
|
|
275
|
+
ctx.font = '400 13px "Inter"'; ctx.fillText('$405bn', R + 14, endP[1] + 30);
|
|
276
|
+
ctx.globalAlpha = 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- annotations: written on by hand once the line has passed ----
|
|
280
|
+
const ann = (x) => H.clamp((clipX - x) / 110);
|
|
281
|
+
ctx.font = '700 22px "Caveat"'; ctx.textAlign = 'left';
|
|
282
|
+
// 1: chatbot moment
|
|
283
|
+
let a = ann(X(2023.4));
|
|
284
|
+
ctx.fillStyle = INK;
|
|
285
|
+
writeOn(ctx, 'Chatbots go mainstream —', X(2018.75), Y(330), a * 3.2);
|
|
286
|
+
writeOn(ctx, 'spending doubles in 18 months', X(2018.75), Y(330) + 22, a * 3.2 - 1.1);
|
|
287
|
+
leader(ctx, X(2021.55), Y(330) + 34, X(2022.9), Y(292), X(2023.55) - 9, Y(222) - 4, 11, INK, seg(a, 0.68, 1));
|
|
288
|
+
// 2: pandemic dip
|
|
289
|
+
const dx = X(2020), dy = Y(58);
|
|
290
|
+
a = ann(dx + 30);
|
|
291
|
+
ctx.fillStyle = MUTED;
|
|
292
|
+
writeOn(ctx, 'A brief pandemic', X(2016.9), Y(178), a * 3);
|
|
293
|
+
writeOn(ctx, 'pause', X(2016.9), Y(178) + 21, a * 3 - 1);
|
|
294
|
+
leader(ctx, X(2017.6), Y(178) + 30, X(2019.2), Y(118), dx - 3, dy - 11, 23, MUTED, seg(a, 0.66, 1));
|
|
295
|
+
// 3: overtakes cloud
|
|
296
|
+
const cxp = X(2022.95), cyp = Y(170);
|
|
297
|
+
a = ann(cxp + 20);
|
|
298
|
+
if (a > 0) {
|
|
299
|
+
ctx.strokeStyle = MUTED; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.arc(cxp, cyp, 9, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * seg(a, 0, 0.35)); ctx.stroke();
|
|
300
|
+
}
|
|
301
|
+
ctx.fillStyle = MUTED;
|
|
302
|
+
writeOn(ctx, 'AI passes cloud', X(2023.55), Y(118), (a - 0.55) * 2.6);
|
|
303
|
+
leader(ctx, X(2023.5), Y(124), X(2023.05), Y(128), cxp + 3, cyp + 12, 31, MUTED, seg(a, 0.3, 0.6));
|
|
304
|
+
|
|
305
|
+
// ---- footer: source line in small caps ----
|
|
306
|
+
const fp = seg(t, 4.2, 4.7);
|
|
307
|
+
ctx.fillStyle = 'rgba(51,48,46,0.35)'; ctx.fillRect(L, 648, 1116 * H.ease.out(fp), 1);
|
|
308
|
+
ctx.globalAlpha = fp;
|
|
309
|
+
smallCaps(ctx, 'Source: Illustrative estimates compiled for this graphic', L, 672, 12, MUTED, 1.1);
|
|
310
|
+
smallCaps(ctx, 'Graphic by the Data Desk', L + 1116, 672, 12, MUTED, 1.1, 'right');
|
|
311
|
+
ctx.globalAlpha = 1;
|
|
312
|
+
ctx.restore();
|
|
313
|
+
|
|
314
|
+
// paper tooth (quietly alive)
|
|
315
|
+
H.grain(ctx, W, H_, 0.025, 5 + Math.floor(t * 12));
|
|
316
|
+
},
|
|
317
|
+
// Sound: a quiet newsroom reading an online graphic. Scrolls are mouse-wheel notches (one per 120 px of the
|
|
318
|
+
// eased scroll) over a soft page glide, Caveat notes and leaders are a felt pen of matching length, numerals
|
|
319
|
+
// advance with small type clicks, the 97% gets the one big hit. Times mirror explainer() below.
|
|
320
|
+
explainerSound(k) {
|
|
321
|
+
const D = 15;
|
|
322
|
+
const pan = (px) => H.clamp((px / 1280) * 2 - 1, -1, 1) * 0.6;
|
|
323
|
+
const when = (t0, t1, ease, u) => { for (let i = 0; i <= 400; i++) { const t = t0 + ((t1 - t0) * i) / 400; if (ease(i / 400) >= u - 1e-6) return t; } return t1; };
|
|
324
|
+
// felt pen note: nib touch + stroke for the write-on span
|
|
325
|
+
const pen = (a, b, g = 1, p = 0, minor = true) => {
|
|
326
|
+
k.noise(a, { dur: 0.018, filter: 'bandpass', f: 1600, q: 1.3, gain: 0.12 * g, a: 0.001, pan: p, minor });
|
|
327
|
+
k.play('stroke', a, { material: 'pen', dur: b - a, gain: 1.5 * g, pan: p, minor });
|
|
328
|
+
};
|
|
329
|
+
// a small type click for counter digits (dry, mid-low, never a chirp)
|
|
330
|
+
const type = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.016, filter: 'bandpass', f: 2300, q: 1.6, gain: 0.2 * g, a: 0.001, pan: p }); k.tone(t, { f: 240, dur: 0.03, gain: 0.05 * g, pan: p }); });
|
|
331
|
+
// scroll: wheel notches following the eased scroll + a smooth low page glide
|
|
332
|
+
const scroll = (t0, t1, dist) => {
|
|
333
|
+
k.noise(t0, { dur: t1 - t0 + 0.1, color: 'pink', filter: 'bandpass', f: [[0, 260], [(t1 - t0) * 0.5, 520], [t1 - t0 + 0.1, 240]], q: 0.7, gain: 0.09, a: (t1 - t0) * 0.45 });
|
|
334
|
+
for (let s = 120; s <= dist; s += 120) {
|
|
335
|
+
const tn = when(t0, t1, H.ease.inOut, s / dist);
|
|
336
|
+
k.withMinor(() => { k.noise(tn, { dur: 0.012, filter: 'bandpass', f: 1900, q: 2, gain: 0.22, a: 0.001, pan: 0.35 }); k.tone(tn, { f: 330, dur: 0.025, gain: 0.05, pan: 0.35 }); });
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
// counter ticks: one per 1/24 of the eased progress, never closer than 1/12 s (slows as it settles)
|
|
340
|
+
const counter = (t0, t1, ease, steps, p, f0) => {
|
|
341
|
+
let last = -1, lastT = -1;
|
|
342
|
+
for (let t = t0; t <= t1 + 1e-6; t += 1 / 120) {
|
|
343
|
+
const v = Math.floor(ease(seg(t, t0, t1)) * steps);
|
|
344
|
+
if (v !== last && t - lastT >= 1 / 12) { type(t, 0.8 + 0.3 * (v / steps), p); last = v; lastT = t; }
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
k.bed('room', 0, D, { gain: 0.35 });
|
|
349
|
+
|
|
350
|
+
// ---- panel 0: the rule draws, the headline sets
|
|
351
|
+
k.noise(0.0, { dur: 0.5, filter: 'bandpass', f: [[0, 500], [0.5, 900]], q: 0.8, gain: 0.06, a: 0.1, pan: -0.1, minor: true }); // rule runs across
|
|
352
|
+
k.play('tock', 0.42, { f: 300, gain: 0.45 }); // headline settles into place
|
|
353
|
+
k.play('tock', 0.95, { f: 380, gain: 0.22, minor: true }); // dek + byline
|
|
354
|
+
scroll(1.5, 2.4, 720);
|
|
355
|
+
k.play('click', 2.0, { f: 900, gain: 0.8, minor: true }); // sticky header docks
|
|
356
|
+
|
|
357
|
+
// ---- panel 1: the data
|
|
358
|
+
k.play('tock', 3.15, { f: 340, gain: 0.3, pan: pan(1000), minor: true }); // the model box fades in
|
|
359
|
+
pen(3.0, 3.25, 1, pan(140)); // "cat"
|
|
360
|
+
pen(3.3, 3.7, 0.8, pan(200)); // cat leader
|
|
361
|
+
pen(3.4, 3.6, 0.9, pan(380)); // "dog"
|
|
362
|
+
pen(3.6, 4.0, 0.9, pan(600)); // dog leader + the flow leader into the model
|
|
363
|
+
counter(2.6, 5.3, H.ease.inOut, 24, pan(930));
|
|
364
|
+
k.play('tock', 5.3, { f: 260, gain: 0.6, pan: pan(930) }); // 1,000,000 sets
|
|
365
|
+
scroll(5.9, 6.6, 720);
|
|
366
|
+
k.play('click', 6.2, { f: 900, gain: 0.7, pan: 0.6, minor: true }); // header tab: The loop
|
|
367
|
+
|
|
368
|
+
// ---- panel 2: guess, check, adjust
|
|
369
|
+
k.play('tock', 6.82, { f: 320, gain: 0.3, pan: pan(170), minor: true }); // a new photo
|
|
370
|
+
pen(6.9, 7.3, 0.8, pan(300)); // photo -> model
|
|
371
|
+
pen(7.2, 7.5, 0.8, pan(800)); // model -> output
|
|
372
|
+
pen(7.4, 7.71, 1.1, pan(950)); // "dog?"
|
|
373
|
+
// the wrong guess: claret X in two quick slashes + a muted descending pair
|
|
374
|
+
pen(7.68, 7.77, 2.0, pan(1110), false); // the X appears in one quick double slash
|
|
375
|
+
k.tone(7.68, { f: 'D4', type: 'triangle', dur: 0.24, gain: 0.2, hold: 0.08, filter: 'lowpass', f2: 1200, pan: pan(1100) });
|
|
376
|
+
k.tone(7.86, { f: 'A3', type: 'triangle', dur: 0.4, gain: 0.2, hold: 0.12, filter: 'lowpass', f2: 900, pan: pan(1100) });
|
|
377
|
+
pen(7.78, 8.1, 1.1, pan(960)); // emphasis ring (drawing since 7.7)
|
|
378
|
+
k.play('click', 8.08, { f: 900, gain: 0.7, pan: -0.5, minor: true }); // step: Check
|
|
379
|
+
k.play('tock', 8.15, { f: 240, gain: 0.55, pan: pan(970) }); // "Answer: cat" tag sets
|
|
380
|
+
pen(8.3, 9.1, 0.55, pan(800)); // error line dashes back
|
|
381
|
+
pen(8.5, 9.0, 0.7, pan(900)); // "error flows back"
|
|
382
|
+
k.noise(9.1, { dur: 0.03, filter: 'bandpass', f: 1300, q: 1.2, gain: 0.18, a: 0.001, pan: pan(570), minor: true }); // arrowhead dot
|
|
383
|
+
k.play('click', 8.9, { f: 900, gain: 0.7, pan: -0.3, minor: true }); // step: Adjust
|
|
384
|
+
for (let i = 1; i <= 6; i++) k.play('tock', when(8.9, 9.6, H.ease.inOut, i / 6), { f: 520 + i * 25, gain: 0.42, pan: pan(570), minor: true }); // weights turn
|
|
385
|
+
pen(8.95, 9.4, 0.5, pan(600)); // "weights adjust"
|
|
386
|
+
pen(9.7, 10.01, 1.1, pan(950)); // "cat"
|
|
387
|
+
pen(10.0, 10.12, 1.3, pan(1050), false); // green tick
|
|
388
|
+
k.play('success', 10.0, { f: 'G5', gain: 0.45, pan: pan(1000) }); // the correction
|
|
389
|
+
pen(10.0, 10.4, 0.6, pan(950)); // "...and round it goes again"
|
|
390
|
+
scroll(10.5, 11.2, 720);
|
|
391
|
+
k.play('click', 10.85, { f: 900, gain: 0.7, pan: 0.6, minor: true }); // header tab: Practice
|
|
392
|
+
|
|
393
|
+
// ---- panel 3: practice
|
|
394
|
+
pen(11.2, 12.8, 0.45, pan(450)); // the accuracy line draws
|
|
395
|
+
pen(11.3, 11.8, 0.6, pan(200)); // "starts at 61%"
|
|
396
|
+
pen(11.6, 11.9, 0.5, pan(110)); // its leader
|
|
397
|
+
counter(11.2, 12.8, H.ease.inOut, 36, pan(1060));
|
|
398
|
+
k.play('impact', 12.8, { gain: 0.8, pan: pan(1060) }); // 97% lands: the big hit
|
|
399
|
+
k.play('ding', 12.82, { f: 'C5', gain: 0.5, pan: pan(1060) });
|
|
400
|
+
pen(12.8, 13.3, 1.3, pan(1060), false); // the ring around 97%
|
|
401
|
+
// takeaway sets (ease.out: mostly there by ~13.45), then resolves
|
|
402
|
+
k.noise(13.2, { dur: 0.3, filter: 'bandpass', f: [[0, 400], [0.3, 800]], q: 0.8, gain: 0.07, a: 0.08, pan: -0.6, minor: true }); // claret rule
|
|
403
|
+
k.play('tock', 13.3, { f: 280, gain: 0.5 });
|
|
404
|
+
['F4', 'C5', 'A5'].forEach((f, i) => k.play('ding', 13.45 + i * 0.08, { f, gain: 0.4 - i * 0.07, pan: (i - 1) * 0.25 }));
|
|
405
|
+
},
|
|
406
|
+
explainerDuration: 15,
|
|
407
|
+
explainerNote: 'Told as a scrolling newspaper story: the page scrolls panel to panel under a sticky serif header, hand-drawn Caveat arrows and rings carry the cause and emphasis, a waffle chart and set numerals count the data, and the accuracy line draws itself like the still.',
|
|
408
|
+
explainer(ctx, W, H_, t) {
|
|
409
|
+
const S = (a, b) => seg(t, a, b);
|
|
410
|
+
ctx.fillStyle = BG; ctx.fillRect(0, 0, W, H_);
|
|
411
|
+
// ---- scroll position (the transition: the page scrolls like an online graphic) ----
|
|
412
|
+
const KEYS = [[0, 0], [1.5, 0], [2.4, 720], [5.9, 720], [6.6, 1440], [10.5, 1440], [11.2, 2160], [15, 2160]];
|
|
413
|
+
let sy = 2160;
|
|
414
|
+
for (let i = 0; i < KEYS.length - 1; i++) if (t >= KEYS[i][0] && t <= KEYS[i + 1][0]) { sy = H.lerp(KEYS[i][1], KEYS[i + 1][1], H.ease.inOut((t - KEYS[i][0]) / (KEYS[i + 1][0] - KEYS[i][0]))); break; }
|
|
415
|
+
const L = 70;
|
|
416
|
+
ctx.save(); ctx.translate(0, -sy);
|
|
417
|
+
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
|
|
418
|
+
const head = (n, title, T0) => {
|
|
419
|
+
// each panel opens with the hero's masthead rule: 3px ink with a claret tab
|
|
420
|
+
ctx.fillStyle = INK; ctx.fillRect(L, T0 + 78, 1140, 3); ctx.fillStyle = HI; ctx.fillRect(L, T0 + 78, 46, 7);
|
|
421
|
+
smallCaps(ctx, n, L, T0 + 112, 22, HI, 2); ctx.fillStyle = INK; ctx.font = '700 46px "Libre Baskerville"'; ctx.fillText(title, L, T0 + 166); };
|
|
422
|
+
|
|
423
|
+
// ===== panel 0: the hook =====
|
|
424
|
+
{
|
|
425
|
+
const a = H.ease.out(S(0, 0.6));
|
|
426
|
+
ctx.fillStyle = INK; ctx.fillRect(L, 210, 1140 * H.ease.out(S(0, 0.5)), 4);
|
|
427
|
+
ctx.fillStyle = HI; ctx.fillRect(L, 210, 60, 9);
|
|
428
|
+
ctx.globalAlpha = a; smallCaps(ctx, 'Artificial Intelligence', L, 270, 24, HI, 2);
|
|
429
|
+
ctx.fillStyle = INK; ctx.font = '700 92px "Libre Baskerville"'; ctx.fillText('How does AI learn?', L, 390 + (1 - a) * 14);
|
|
430
|
+
ctx.globalAlpha = H.ease.out(S(0.4, 1.0));
|
|
431
|
+
ctx.fillStyle = MUTED; ctx.font = '400 30px "Libre Baskerville"'; ctx.fillText('A visual guide to how machines get better by guessing', L, 452);
|
|
432
|
+
smallCaps(ctx, 'By the Data Desk', L, 512, 20, MUTED, 1.4);
|
|
433
|
+
ctx.globalAlpha = 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ===== panel 1: the data (top at 720) =====
|
|
437
|
+
{
|
|
438
|
+
const T0 = 720;
|
|
439
|
+
head('1. The data', 'It starts with labelled examples', T0);
|
|
440
|
+
// waffle: each square = 5,000 examples; claret = cat, grey = dog
|
|
441
|
+
const cols = 20, rows = 10, sq = 25, gp = 4, wx = L, wy = T0 + 260;
|
|
442
|
+
const filled = Math.floor(cols * rows * H.ease.inOut(S(2.6, 5.3)));
|
|
443
|
+
const wr = H.rng(31);
|
|
444
|
+
let catCell = null, dogCell = null;
|
|
445
|
+
for (let i = 0; i < cols * rows; i++) {
|
|
446
|
+
const c = i % cols, r = Math.floor(i / cols), isCat = wr.chance(0.52);
|
|
447
|
+
const x = wx + c * (sq + gp), y = wy + r * (sq + gp);
|
|
448
|
+
if (i === 24) catCell = [x + sq / 2, y + sq / 2]; if (i === 47) dogCell = [x + sq / 2, y + sq / 2];
|
|
449
|
+
ctx.fillStyle = i < filled ? (isCat ? H.rgba(HI, 0.85) : '#B9AFA5') : 'rgba(51,48,46,0.06)';
|
|
450
|
+
ctx.fillRect(x, y, sq, sq);
|
|
451
|
+
}
|
|
452
|
+
// direct labels with hand-drawn leaders landing on a cat square and a dog square
|
|
453
|
+
ctx.font = '700 36px "Caveat"';
|
|
454
|
+
const la = S(3.0, 3.5);
|
|
455
|
+
if (la > 0 && catCell) {
|
|
456
|
+
catIcon(ctx, wx + 22, wy - 36, 1.15 * H.ease.back(la), HI);
|
|
457
|
+
ctx.fillStyle = HI; writeOn(ctx, 'cat', wx + 50, wy - 26, la * 2);
|
|
458
|
+
leader(ctx, wx + 104, wy - 32, wx + 150, wy - 30, catCell[0] + 2, catCell[1] - 9, 3, HI, S(3.3, 3.7), 2);
|
|
459
|
+
dogIcon(ctx, wx + 270, wy - 36, 1.15 * H.ease.back(S(3.4, 3.8)), MUTED);
|
|
460
|
+
ctx.fillStyle = MUTED; writeOn(ctx, 'dog', wx + 300, wy - 26, S(3.4, 3.8) * 2);
|
|
461
|
+
leader(ctx, wx + 312, wy - 12, wx + 260, wy + 10, dogCell[0] + 5, dogCell[1] - 8, 5, MUTED, S(3.6, 4.0), 2);
|
|
462
|
+
}
|
|
463
|
+
// the counter: hero numeral in set type
|
|
464
|
+
const n = Math.round(1e6 * H.ease.inOut(S(2.6, 5.3)));
|
|
465
|
+
const nx = 800;
|
|
466
|
+
ctx.fillStyle = INK; ctx.font = '700 84px "Libre Baskerville"'; ctx.fillText(n.toLocaleString('en-US'), nx - 20, T0 + 300);
|
|
467
|
+
ctx.font = '400 26px "Inter"'; ctx.fillStyle = MUTED; ctx.fillText('labelled examples', nx - 16, T0 + 340);
|
|
468
|
+
ctx.font = '400 28px "Inter"'; ctx.fillText('each square = 5,000', wx, wy + rows * (sq + gp) + 34);
|
|
469
|
+
// the model box and the flow of examples into it
|
|
470
|
+
const mx = 800, my = T0 + 400, mw = 400, mh = 220;
|
|
471
|
+
const ma = S(3.0, 3.5);
|
|
472
|
+
ctx.globalAlpha = ma;
|
|
473
|
+
ctx.strokeStyle = INK; ctx.lineWidth = 2; ctx.strokeRect(mx, my, mw, mh);
|
|
474
|
+
smallCaps(ctx, 'The model', mx + 14, my + 38, 26, INK, 1.6);
|
|
475
|
+
miniNet(ctx, mx + 70, my + 52, 260, 156, [0.1, -0.2, 0.15, 0.05, -0.1, 0.2, 0.1, -0.05, 0.12, 0.1, -0.15, 0.08, 0.2, -0.1, 0.05, 0.1, 0.07, -0.12, 0.1, 0.02], false, 10);
|
|
476
|
+
ctx.globalAlpha = 1;
|
|
477
|
+
const p0 = [wx + cols * (sq + gp) + 10, wy + 210], p1 = [mx - 8, my + mh / 2], pc = [(p0[0] + p1[0]) / 2, wy + 330];
|
|
478
|
+
leader(ctx, p0[0], p0[1], pc[0], pc[1], p1[0], p1[1], 7, INK, S(3.4, 4.0), 2);
|
|
479
|
+
const fl = S(4.0, 6.2);
|
|
480
|
+
if (fl > 0 && fl < 1) for (let k = 0; k < 6; k++) {
|
|
481
|
+
const u = ((t * 0.9 + k / 6) % 1);
|
|
482
|
+
const a = (1 - u) * (1 - u), b = 2 * u * (1 - u), c = u * u;
|
|
483
|
+
const x = a * p0[0] + b * pc[0] + c * p1[0], y = a * p0[1] + b * pc[1] + c * p1[1];
|
|
484
|
+
ctx.fillStyle = k % 2 ? H.rgba(HI, 0.85) : '#B9AFA5'; ctx.fillRect(x - 8, y - 8, 16, 16);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ===== panel 2: guess, check, adjust (top at 1440) =====
|
|
489
|
+
{
|
|
490
|
+
const T0 = 1440;
|
|
491
|
+
head('2. Guess, check, adjust', 'It learns from its mistakes', T0);
|
|
492
|
+
// step strip
|
|
493
|
+
const steps = ['Guess', 'Check', 'Adjust'];
|
|
494
|
+
const active = t < 8.1 ? 0 : t < 8.9 ? 1 : t < 9.6 ? 2 : 0;
|
|
495
|
+
let sx = L;
|
|
496
|
+
steps.forEach((st, i) => {
|
|
497
|
+
const on = t > 6.8 && i === active;
|
|
498
|
+
ctx.font = '700 34px "Libre Baskerville"'; ctx.fillStyle = on ? HI : 'rgba(51,48,46,0.35)';
|
|
499
|
+
ctx.fillText(st, sx, T0 + 232); const w = ctx.measureText(st).width;
|
|
500
|
+
if (on) ctx.fillRect(sx, T0 + 244, w, 3.5);
|
|
501
|
+
sx += w + 22;
|
|
502
|
+
if (i < 2) { ctx.fillStyle = 'rgba(51,48,46,0.45)'; ctx.fillText('→', sx, T0 + 232); sx += 58; }
|
|
503
|
+
});
|
|
504
|
+
// new photo card
|
|
505
|
+
const ca = H.ease.out(S(6.7, 7.1));
|
|
506
|
+
const cx = L, cy = T0 + 330;
|
|
507
|
+
ctx.globalAlpha = ca;
|
|
508
|
+
ctx.strokeStyle = INK; ctx.lineWidth = 2; ctx.strokeRect(cx, cy, 210, 210);
|
|
509
|
+
catIcon(ctx, cx + 105, cy + 112, 3.8, INK);
|
|
510
|
+
ctx.font = '400 24px "Inter"'; ctx.fillStyle = MUTED; ctx.fillText('A new photo', cx, cy + 244);
|
|
511
|
+
ctx.globalAlpha = 1;
|
|
512
|
+
// model
|
|
513
|
+
const mx = 380, my = T0 + 320, mw = 380, mh = 240;
|
|
514
|
+
const adj = H.ease.inOut(S(8.9, 9.6));
|
|
515
|
+
const w0 = [0.1, -0.2, 0.15, 0.05, -0.1, 0.2, 0.1, -0.05, 0.12, 0.1, -0.15, 0.08, 0.2, -0.1, 0.05, 0.1, 0.07, -0.12, 0.1, 0.02];
|
|
516
|
+
const w1 = [0.5, -0.1, 0.05, 0.6, -0.4, 0.1, 0.45, -0.3, 0.05, 0.7, -0.05, 0.3, 0.1, -0.5, 0.6, 0.05, 0.55, -0.1, 0.65, 0.1];
|
|
517
|
+
const ww = w0.map((v, i) => H.lerp(v, w1[i], adj));
|
|
518
|
+
ctx.strokeStyle = INK; ctx.lineWidth = 2; ctx.strokeRect(mx, my, mw, mh);
|
|
519
|
+
smallCaps(ctx, 'The model', mx + 14, my + 38, 26, INK, 1.6);
|
|
520
|
+
miniNet(ctx, mx + 60, my + 54, 260, 170, ww, t > 8.9 && t < 9.7, 10);
|
|
521
|
+
leader(ctx, cx + 222, cy + 105, (cx + 222 + mx) / 2, cy + 80, mx - 8, my + mh / 2, 12, INK, S(6.9, 7.3), 2);
|
|
522
|
+
// output
|
|
523
|
+
const ox = 850, oy = T0 + 450;
|
|
524
|
+
leader(ctx, mx + mw + 8, my + mh / 2, ox - 40, oy - 40, ox - 12, oy - 14, 14, INK, S(7.2, 7.5), 2);
|
|
525
|
+
ctx.font = '700 70px "Caveat"';
|
|
526
|
+
const g1 = S(7.4, 7.8) * (1 - S(9.5, 9.7));
|
|
527
|
+
if (g1 > 0) {
|
|
528
|
+
ctx.fillStyle = INK; writeOn(ctx, '“dog?”', ox, oy, g1 * 1.3);
|
|
529
|
+
if (g1 > 0.7) { ctx.strokeStyle = HI; ctx.lineWidth = 5; ctx.lineCap = 'round'; const x = ox + 196, y = oy - 46; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + 34, y + 34); ctx.moveTo(x + 34, y); ctx.lineTo(x, y + 34); ctx.stroke(); }
|
|
530
|
+
handRing(ctx, ox + 112, oy - 22, 150, 56, S(7.7, 8.1), 21, HI, 3);
|
|
531
|
+
}
|
|
532
|
+
// check: the truth tag + the error signal flowing back
|
|
533
|
+
const ck = S(8.1, 8.4) * (1 - S(9.5, 9.7));
|
|
534
|
+
if (ck > 0) {
|
|
535
|
+
ctx.globalAlpha = ck;
|
|
536
|
+
ctx.fillStyle = HI; ctx.fillRect(ox, oy + 50, 230, 50);
|
|
537
|
+
ctx.fillStyle = BG; ctx.font = '600 28px "Inter"'; ctx.fillText('Answer: cat', ox + 18, oy + 85);
|
|
538
|
+
ctx.globalAlpha = 1;
|
|
539
|
+
const ep = S(8.3, 9.1);
|
|
540
|
+
if (ep > 0) {
|
|
541
|
+
ctx.save(); ctx.strokeStyle = HI; ctx.lineWidth = 3.4; ctx.setLineDash([10, 8]); ctx.lineDashOffset = t * 50;
|
|
542
|
+
ctx.beginPath();
|
|
543
|
+
const n = Math.floor(40 * ep);
|
|
544
|
+
for (let k = 0; k <= n; k++) { const u = k / 40, x = H.lerp(ox + 110, mx + mw / 2, u), y = H.lerp(oy - 80, my - 14, u) - Math.sin(u * Math.PI) * 50; k ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }
|
|
545
|
+
ctx.stroke(); ctx.restore();
|
|
546
|
+
if (ep >= 1) { const ex = mx + mw / 2, ey = my - 14; ctx.fillStyle = HI; ctx.beginPath(); ctx.moveTo(ex, ey + 10); ctx.lineTo(ex - 9, ey - 6); ctx.lineTo(ex + 9, ey - 8); ctx.closePath(); ctx.fill(); }
|
|
547
|
+
ctx.font = '700 38px "Caveat"'; ctx.fillStyle = HI; writeOn(ctx, 'error flows back', ox - 10, my - 26, S(8.5, 9.0));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (t > 8.9 && t < 9.9) { ctx.font = '700 38px "Caveat"'; ctx.fillStyle = HI; writeOn(ctx, 'weights adjust', mx + 90, my + mh + 50, S(8.95, 9.4)); }
|
|
551
|
+
const g2 = S(9.7, 10.1);
|
|
552
|
+
if (g2 > 0) {
|
|
553
|
+
ctx.font = '700 70px "Caveat"'; ctx.fillStyle = INK; writeOn(ctx, '“cat”', ox, oy, g2 * 1.3);
|
|
554
|
+
if (g2 > 0.75) { ctx.strokeStyle = '#2E7D4F'; ctx.lineWidth = 6; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const x = ox + 168, y = oy - 30; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + 12, y + 15); ctx.lineTo(x + 40, y - 22); ctx.stroke(); }
|
|
555
|
+
ctx.font = '700 36px "Caveat"'; ctx.fillStyle = MUTED; writeOn(ctx, '…and round it goes again', ox - 10, oy + 80, S(10.0, 10.4));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ===== panel 3: progress + takeaway (top at 2160) =====
|
|
560
|
+
{
|
|
561
|
+
const T0 = 2160;
|
|
562
|
+
head('3. Practice', 'Millions of loops later, it rarely misses', T0);
|
|
563
|
+
const cl = L, cr = 880, ct = T0 + 230, cb = T0 + 560;
|
|
564
|
+
const Y = (v) => cb - (v - 50) / 50 * (cb - ct), X = (u) => cl + u * (cr - cl);
|
|
565
|
+
ctx.font = '400 27px "Inter"';
|
|
566
|
+
for (let v = 50; v <= 100; v += 10) {
|
|
567
|
+
ctx.fillStyle = v === 50 ? INK : GRID; ctx.fillRect(cl, Math.round(Y(v)), cr - cl + 20, v === 50 ? 1.6 : 1);
|
|
568
|
+
if (v === 100) { ctx.fillStyle = MUTED; ctx.fillText('100% accuracy', cl, Y(v) - 9); }
|
|
569
|
+
// numeric ticks hang in the left margin, centred on their gridline, so the curve start never runs through "60"
|
|
570
|
+
else if (v > 50) { ctx.fillStyle = MUTED; ctx.textAlign = 'right'; ctx.fillText(String(v), cl - 9, Y(v) + 9); ctx.textAlign = 'left'; }
|
|
571
|
+
}
|
|
572
|
+
ctx.fillStyle = MUTED; ctx.textAlign = 'right'; ctx.font = '400 28px "Inter"'; ctx.fillText('Training rounds →', cr + 20, cb + 36); ctx.textAlign = 'left';
|
|
573
|
+
const acc = (u) => 61 + 36 * (1 - Math.exp(-u * 4.2)) / (1 - Math.exp(-4.2)) + Math.sin(u * 40) * 1.2 * (1 - u);
|
|
574
|
+
const rp = H.ease.inOut(S(11.2, 12.8));
|
|
575
|
+
const g = ctx.createLinearGradient(0, ct, 0, cb); g.addColorStop(0, 'rgba(153,15,61,0.18)'); g.addColorStop(1, 'rgba(153,15,61,0.03)');
|
|
576
|
+
const N = 120, n = Math.floor(N * rp);
|
|
577
|
+
if (n > 1) {
|
|
578
|
+
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(X(0), cb);
|
|
579
|
+
for (let k = 0; k <= n; k++) ctx.lineTo(X(k / N), Y(acc(k / N))); ctx.lineTo(X(n / N), cb); ctx.closePath(); ctx.fill();
|
|
580
|
+
ctx.strokeStyle = HI; ctx.lineWidth = 5; ctx.lineJoin = 'round'; ctx.beginPath();
|
|
581
|
+
for (let k = 0; k <= n; k++) (k ? ctx.lineTo(X(k / N), Y(acc(k / N))) : ctx.moveTo(X(0), Y(acc(0)))); ctx.stroke();
|
|
582
|
+
const hx = X(n / N), hy = Y(acc(n / N));
|
|
583
|
+
ctx.fillStyle = HI; ctx.beginPath(); ctx.arc(hx, hy, 7, 0, 7); ctx.fill();
|
|
584
|
+
}
|
|
585
|
+
ctx.font = '700 36px "Caveat"'; ctx.fillStyle = MUTED;
|
|
586
|
+
writeOn(ctx, 'starts at 61%', X(0.06), Y(53.5), S(11.3, 11.8));
|
|
587
|
+
leader(ctx, X(0.055), Y(56.2), X(0.02), Y(57.2), X(0.009), Y(60.4), 41, MUTED, S(11.6, 11.9), 2);
|
|
588
|
+
// hero numeral at right + emphasis ring
|
|
589
|
+
const big = rp >= 1 ? 97 : Math.round(61 + 36 * rp);
|
|
590
|
+
ctx.globalAlpha = S(11.2, 11.5);
|
|
591
|
+
ctx.fillStyle = HI; ctx.font = '700 128px "Libre Baskerville"'; ctx.fillText(big + '%', 920, T0 + 400);
|
|
592
|
+
ctx.font = '400 28px "Inter"'; ctx.fillStyle = MUTED; ctx.fillText('accuracy', 928, T0 + 462);
|
|
593
|
+
ctx.globalAlpha = 1;
|
|
594
|
+
handRing(ctx, 1062, T0 + 354, 158, 68, S(12.8, 13.3), 77, HI, 3.4);
|
|
595
|
+
// takeaway
|
|
596
|
+
const tk = H.ease.out(S(13.2, 13.8));
|
|
597
|
+
ctx.globalAlpha = tk;
|
|
598
|
+
ctx.fillStyle = HI; ctx.fillRect(L, T0 + 610, 80 * tk, 5);
|
|
599
|
+
ctx.fillStyle = INK; ctx.font = '700 50px "Libre Baskerville"'; ctx.fillText('More examples → better guesses', L, T0 + 676 + (1 - tk) * 8);
|
|
600
|
+
ctx.globalAlpha = 1;
|
|
601
|
+
}
|
|
602
|
+
ctx.restore();
|
|
603
|
+
|
|
604
|
+
// ---- sticky header (docks as the headline scrolls away) ----
|
|
605
|
+
const hd = S(1.7, 2.2);
|
|
606
|
+
if (hd > 0) {
|
|
607
|
+
ctx.save();
|
|
608
|
+
// the header's paper backing turns opaque faster than its type fades in, so the scrolling byline never shows through the title
|
|
609
|
+
ctx.globalAlpha = Math.min(1, hd * 2.5); ctx.fillStyle = BG; ctx.fillRect(0, 0, W, 66);
|
|
610
|
+
ctx.globalAlpha = hd;
|
|
611
|
+
ctx.fillStyle = 'rgba(51,48,46,0.35)'; ctx.fillRect(0, 66, W, 1);
|
|
612
|
+
ctx.fillStyle = INK; ctx.font = '700 28px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillText('How does AI learn?', L, 44);
|
|
613
|
+
const secs = ['Data', 'The loop', 'Practice'];
|
|
614
|
+
const cur = t < 6.2 ? 0 : t < 10.85 ? 1 : 2;
|
|
615
|
+
let x = 1210;
|
|
616
|
+
ctx.font = '600 22px "Inter"';
|
|
617
|
+
for (let i = secs.length - 1; i >= 0; i--) {
|
|
618
|
+
const w = ctx.measureText(secs[i]).width; x -= w;
|
|
619
|
+
ctx.fillStyle = i === cur ? HI : 'rgba(51,48,46,0.42)'; ctx.fillText(secs[i], x, 42);
|
|
620
|
+
if (i === cur) ctx.fillRect(x, 50, w, 3);
|
|
621
|
+
x -= 30;
|
|
622
|
+
}
|
|
623
|
+
ctx.restore();
|
|
624
|
+
// printed-graphic footer: small-caps source line pinned bottom right, as in the still
|
|
625
|
+
ctx.save(); ctx.globalAlpha = hd;
|
|
626
|
+
ctx.font = '600 19px "Inter"';
|
|
627
|
+
smallCaps(ctx, 'Source: Illustrative', 1210, 704, 19, MUTED, 1.4, 'right');
|
|
628
|
+
ctx.restore();
|
|
629
|
+
}
|
|
630
|
+
H.grain(ctx, W, H_, 0.025, 5 + Math.floor(t * 12));
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
}
|