@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,951 @@
|
|
|
1
|
+
{
|
|
2
|
+
// =====================================================================================
|
|
3
|
+
// Cosmic Star Chart: deep-space illustration (nebulae, lit planets, comets, spiky stars)
|
|
4
|
+
// crossed with an engraved celestial atlas overlay (hairline gold / pale-cyan chart work).
|
|
5
|
+
// =====================================================================================
|
|
6
|
+
const TAU = Math.PI * 2;
|
|
7
|
+
const GOLD = [230, 198, 128], GOLDB = [255, 226, 168], CYAN = [160, 226, 236], RED = [255, 86, 74];
|
|
8
|
+
const CREAM = [248, 240, 224], WHITE = [255, 255, 255];
|
|
9
|
+
const rgba = (c, a) => `rgba(${c[0] | 0},${c[1] | 0},${c[2] | 0},${a})`;
|
|
10
|
+
const CORM = '"Cormorant Garamond"', SYNE = '"Syne"', BASK = '"Libre Baskerville"';
|
|
11
|
+
const LV = (() => { const v = [0.74, -0.46, 0.48], n = Math.hypot(v[0], v[1], v[2]); return v.map((a) => a / n); })();
|
|
12
|
+
const C = {}; // cached textures (built once, output still depends only on t)
|
|
13
|
+
const wrap = (v, m) => ((v % m) + m) % m;
|
|
14
|
+
const sm = (a, b, x) => H.smooth(a, b, x);
|
|
15
|
+
const bump = (a, b, c, d, x) => sm(a, b, x) * (1 - sm(c, d, x));
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------- glow sprites
|
|
18
|
+
function glow(c) {
|
|
19
|
+
const key = 'g' + c.join(',');
|
|
20
|
+
if (C[key]) return C[key];
|
|
21
|
+
const cv = H.layer(128, 128), x = cv.getContext('2d'), g = x.createRadialGradient(64, 64, 0, 64, 64, 64);
|
|
22
|
+
g.addColorStop(0, rgba(c, 1)); g.addColorStop(0.07, rgba(c, 0.85)); g.addColorStop(0.2, rgba(c, 0.28));
|
|
23
|
+
g.addColorStop(0.45, rgba(c, 0.07)); g.addColorStop(1, rgba(c, 0));
|
|
24
|
+
x.fillStyle = g; x.fillRect(0, 0, 128, 128);
|
|
25
|
+
return (C[key] = cv);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------- nebula (layered fbm, cached)
|
|
29
|
+
// 640x450 texture standing for a 1920x1350 sky (drawn 3x up: nebulae are soft anyway)
|
|
30
|
+
function nebula() {
|
|
31
|
+
if (C.neb) return C.neb;
|
|
32
|
+
const w = 640, h = 450, cv = H.layer(w, h), x = cv.getContext('2d'), img = x.createImageData(w, h), d = img.data;
|
|
33
|
+
const nz = H.makeNoise(7713);
|
|
34
|
+
const P = { teal: [20, 150, 165], violet: [98, 50, 178], rose: [222, 70, 124], gold: [242, 182, 96] };
|
|
35
|
+
for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) {
|
|
36
|
+
const bx = i / w, by = j / h, u = bx * 3.2, v = by * 2.25;
|
|
37
|
+
const qx = nz.fbm3(u, v, 0.1, 3), qy = nz.fbm3(u + 5.2, v + 1.3, 0.1, 3);
|
|
38
|
+
const n = nz.fbm3(u + 1.5 * qx, v + 1.5 * qy, 1.7, 5) * 0.5 + 0.5;
|
|
39
|
+
const n2 = nz.fbm3(u * 1.25 + 3 + 1.3 * qy, v * 1.25 - 2 + 1.3 * qx, 4.4, 5) * 0.5 + 0.5;
|
|
40
|
+
// the nebula lives in a winding diagonal river across the sky
|
|
41
|
+
const dist = by - (0.84 - 0.62 * bx) - 0.07 * Math.sin(bx * 6.5 + 1);
|
|
42
|
+
const band = Math.exp(-(dist * dist) / (2 * 0.17 * 0.17));
|
|
43
|
+
const m = 0.12 + 0.88 * band;
|
|
44
|
+
const A = sm(0.44, 0.8, n) * m, B = sm(0.47, 0.82, n2) * m * (0.4 + 0.6 * sm(0.2, 0.7, bx + 0.2 * qx));
|
|
45
|
+
const hueA = H.clamp(nz.fbm3(u * 0.5 + 9, v * 0.5, 3.3, 3) * 1.2 + 0.5 + (bx - 0.45) * 0.8);
|
|
46
|
+
let r = P.violet[0] + (P.rose[0] - P.violet[0]) * hueA, g = P.violet[1] + (P.rose[1] - P.violet[1]) * hueA, b = P.violet[2] + (P.rose[2] - P.violet[2]) * hueA;
|
|
47
|
+
const inner = 0.55 + 0.75 * sm(0.5, 0.9, n); // brighter where the gas is thickest: reads as lit from within
|
|
48
|
+
let R_ = r * A * inner, G_ = g * A * inner, B_ = b * A * inner;
|
|
49
|
+
R_ += P.teal[0] * B * 0.95; G_ += P.teal[1] * B * 0.95; B_ += P.teal[2] * B * 0.95;
|
|
50
|
+
const gold = sm(0.62, 0.88, n) * sm(0.35, 0.75, hueA) * band;
|
|
51
|
+
R_ += P.gold[0] * gold * 0.9; G_ += P.gold[1] * gold * 0.9; B_ += P.gold[2] * gold * 0.9;
|
|
52
|
+
R_ *= 1.3; G_ *= 1.3; B_ *= 1.3;
|
|
53
|
+
// soft diffuse glow of the whole river
|
|
54
|
+
R_ += 40 * band * 0.35; G_ += 22 * band * 0.35; B_ += 70 * band * 0.35;
|
|
55
|
+
// dark dust lanes cut through the glow
|
|
56
|
+
const dl = nz.fbm3(u * 2.1 + 4 + qx, v * 2.1 + qy, 5.1, 5) * 0.5 + 0.5;
|
|
57
|
+
const dust = sm(0.53, 0.7, dl) * (0.25 + 0.75 * band) * 0.85;
|
|
58
|
+
R_ *= 1 - dust; G_ *= 1 - dust; B_ *= 1 - dust;
|
|
59
|
+
const k = (j * w + i) * 4;
|
|
60
|
+
const br = 3 + 6 * by, bg = 4 + 4 * by, bb = 11 + 14 * (1 - Math.abs(by - 0.5));
|
|
61
|
+
d[k] = Math.min(255, br + R_); d[k + 1] = Math.min(255, bg + G_); d[k + 2] = Math.min(255, bb + B_); d[k + 3] = 255;
|
|
62
|
+
}
|
|
63
|
+
x.putImageData(img, 0, 0);
|
|
64
|
+
return (C.neb = cv);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------- distant galaxy smear
|
|
68
|
+
function galaxy() {
|
|
69
|
+
if (C.gal) return C.gal;
|
|
70
|
+
const w = 300, h = 300, cv = H.layer(w, h), x = cv.getContext('2d'), img = x.createImageData(w, h), d = img.data, nz = H.makeNoise(91);
|
|
71
|
+
for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) {
|
|
72
|
+
const u = (i - w / 2) / (w / 2), v = (j - h / 2) / (h / 2), r = Math.hypot(u, v);
|
|
73
|
+
if (r > 1) continue;
|
|
74
|
+
const th = Math.atan2(v, u), arm = 0.5 + 0.5 * Math.cos(2 * th - 7.5 * Math.log(r + 0.06));
|
|
75
|
+
const n = nz.fbm2(u * 5, v * 5, 4) * 0.5 + 0.5;
|
|
76
|
+
const disk = Math.exp(-r * 4.5), core = Math.exp(-r * r * 70);
|
|
77
|
+
const I = disk * (0.25 + 0.75 * Math.pow(arm, 2.2)) * (0.5 + 0.9 * n) * (1 - sm(0.8, 1, r)) + core * 1.4;
|
|
78
|
+
const s = H.clamp(r * 2.2), k = (j * w + i) * 4;
|
|
79
|
+
d[k] = 255 - 90 * s; d[k + 1] = 232 - 40 * s; d[k + 2] = 196 + 59 * s; d[k + 3] = H.clamp(I) * 255;
|
|
80
|
+
}
|
|
81
|
+
x.putImageData(img, 0, 0);
|
|
82
|
+
return (C.gal = cv);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------- star fields
|
|
86
|
+
const BW = 1700, BH = 1300, FW = 1500, FH = 1150;
|
|
87
|
+
function starData() {
|
|
88
|
+
if (C.stars) return C.stars;
|
|
89
|
+
const r = H.rng(4401), tints = [[255, 246, 232], [212, 228, 255], [255, 226, 186], [200, 240, 246], [255, 212, 222]];
|
|
90
|
+
const mid = [], near = [];
|
|
91
|
+
for (let i = 0; i < 620; i++) mid.push({ x: r() * BW, y: r() * BH, m: Math.pow(r(), 3), c: r.pick(tints), ph: r() * TAU, w: 0.8 + r() * 3 });
|
|
92
|
+
for (let i = 0; i < 60; i++) near.push({ x: r() * BW, y: r() * BH, m: 0.25 + 0.75 * Math.pow(r(), 2), c: r.pick(tints), ph: r() * TAU, w: 0.6 + r() * 2 });
|
|
93
|
+
// far stars baked once, denser along the nebula river (a milky band)
|
|
94
|
+
const far = H.layer(FW, FH), fx = far.getContext('2d');
|
|
95
|
+
for (let i = 0; i < 5200; i++) {
|
|
96
|
+
const x = r() * FW, y = r() * FH, bx = x / FW, by = y / FH;
|
|
97
|
+
const dist = by - (0.84 - 0.62 * bx);
|
|
98
|
+
if (r() > 0.25 + 0.75 * Math.exp(-(dist * dist) / 0.03)) continue;
|
|
99
|
+
const m = Math.pow(r(), 2.5), c = r.pick(tints);
|
|
100
|
+
fx.fillStyle = rgba(c, 0.15 + m * 0.7); const s = 0.5 + m * 1.1; fx.fillRect(x, y, s, s);
|
|
101
|
+
}
|
|
102
|
+
return (C.stars = { mid, near, far });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// cam: { x, y (scene px), z (zoom), warp (0..1), bright, nx, ny (nebula framing offsets) }
|
|
106
|
+
function sky(ctx, W, Hh, t, cam) {
|
|
107
|
+
ctx.fillStyle = '#03040b'; ctx.fillRect(0, 0, W, Hh);
|
|
108
|
+
const neb = nebula(), S = starData(), warp = cam.warp || 0, br = cam.bright == null ? 1 : cam.bright;
|
|
109
|
+
const cx = W / 2, cy = Hh / 2;
|
|
110
|
+
// nebula layer (slowest parallax), with a zoom-blur smear during warp
|
|
111
|
+
const nzm = 1 + (cam.z - 1) * 0.22;
|
|
112
|
+
const drawNeb = (zz, a) => {
|
|
113
|
+
ctx.save(); ctx.globalAlpha = a; ctx.translate(cx, cy); ctx.scale(zz, zz);
|
|
114
|
+
ctx.translate(-960 - cam.x * 0.12 + (cam.nx || 0), -675 - cam.y * 0.12 + (cam.ny || 0));
|
|
115
|
+
ctx.imageSmoothingQuality = 'high'; ctx.drawImage(neb, 0, 0, 1920, 1350);
|
|
116
|
+
const g = galaxy(), gp = cam.gal || [920, 525, 1.5]; ctx.translate(gp[0], gp[1]); ctx.rotate(-0.38); ctx.scale(gp[2], gp[2] * 0.28);
|
|
117
|
+
ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = a * 0.75; ctx.drawImage(g, -150, -150, 300, 300);
|
|
118
|
+
ctx.restore();
|
|
119
|
+
};
|
|
120
|
+
drawNeb(nzm, br);
|
|
121
|
+
if (warp > 0.01) {
|
|
122
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
|
123
|
+
for (let i = 1; i <= 4; i++) drawNeb(nzm * (1 + warp * 0.07 * i), 0.18 * warp * br);
|
|
124
|
+
ctx.restore();
|
|
125
|
+
}
|
|
126
|
+
// far stars
|
|
127
|
+
const fz = 1 + (cam.z - 1) * 0.15;
|
|
128
|
+
ctx.save(); ctx.globalAlpha = br; ctx.translate(cx, cy); ctx.scale(fz, fz);
|
|
129
|
+
ctx.drawImage(S.far, -FW / 2 - cam.x * 0.05, -FH / 2 - cam.y * 0.05); ctx.restore();
|
|
130
|
+
// mid stars: twinkle, streak in warp
|
|
131
|
+
const zs = 1 + (cam.z - 1) * 0.5;
|
|
132
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.lineCap = 'round';
|
|
133
|
+
for (const s of S.mid) {
|
|
134
|
+
let px = wrap(s.x - cam.x * 0.3, BW) - (BW - W) / 2, py = wrap(s.y - cam.y * 0.3, BH) - (BH - Hh) / 2;
|
|
135
|
+
px = cx + (px - cx) * zs; py = cy + (py - cy) * zs;
|
|
136
|
+
if (px < -60 || py < -60 || px > W + 60 || py > Hh + 60) continue;
|
|
137
|
+
const tw = 0.62 + 0.38 * Math.sin(t * s.w + s.ph), a = (0.22 + 0.78 * s.m) * tw * br, sz = 0.7 + s.m * 1.7;
|
|
138
|
+
if (warp > 0.02) {
|
|
139
|
+
const k = warp * (0.18 + 0.5 * s.m);
|
|
140
|
+
ctx.strokeStyle = rgba(s.c, Math.min(1, a * (1 + warp))); ctx.lineWidth = sz;
|
|
141
|
+
ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px + (px - cx) * k, py + (py - cy) * k); ctx.stroke();
|
|
142
|
+
} else { ctx.fillStyle = rgba(s.c, a); ctx.fillRect(px - sz / 2, py - sz / 2, sz, sz); }
|
|
143
|
+
}
|
|
144
|
+
// near bright stars with soft glow (fastest parallax)
|
|
145
|
+
const zn = 1 + (cam.z - 1) * 1.0;
|
|
146
|
+
for (const s of S.near) {
|
|
147
|
+
let px = wrap(s.x - cam.x * 0.6, BW) - (BW - W) / 2, py = wrap(s.y - cam.y * 0.6, BH) - (BH - Hh) / 2;
|
|
148
|
+
px = cx + (px - cx) * zn; py = cy + (py - cy) * zn;
|
|
149
|
+
if (px < -80 || py < -80 || px > W + 80 || py > Hh + 80) continue;
|
|
150
|
+
const tw = 0.7 + 0.3 * Math.sin(t * s.w * 1.7 + s.ph), a = s.m * tw * br, sz = 6 + s.m * 20;
|
|
151
|
+
if (warp > 0.02) {
|
|
152
|
+
const k = warp * 0.9;
|
|
153
|
+
ctx.strokeStyle = rgba(s.c, Math.min(1, a)); ctx.lineWidth = 1 + s.m * 2;
|
|
154
|
+
ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px + (px - cx) * k, py + (py - cy) * k); ctx.stroke();
|
|
155
|
+
}
|
|
156
|
+
ctx.globalAlpha = a; ctx.drawImage(glow(s.c), px - sz / 2, py - sz / 2, sz, sz);
|
|
157
|
+
ctx.globalAlpha = 1; ctx.fillStyle = rgba(WHITE, Math.min(1, a * 1.2)); ctx.fillRect(px - 0.8, py - 0.8, 1.6, 1.6);
|
|
158
|
+
}
|
|
159
|
+
ctx.restore();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------- bright star with diffraction spikes
|
|
163
|
+
function spikeStar(ctx, x, y, s, a, tint = [255, 236, 206], rot = 0.0) { const GA = ctx.globalAlpha;
|
|
164
|
+
if (a <= 0.003) return;
|
|
165
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.translate(x, y);
|
|
166
|
+
ctx.globalAlpha = GA * a * 0.55; ctx.drawImage(glow(tint), -s * 4, -s * 4, s * 8, s * 8);
|
|
167
|
+
ctx.globalAlpha = GA * a * 0.5; ctx.drawImage(glow([150, 200, 255]), -s * 1.8, -s * 1.8, s * 3.6, s * 3.6);
|
|
168
|
+
ctx.globalAlpha = GA * a; ctx.drawImage(glow(WHITE), -s, -s, s * 2, s * 2);
|
|
169
|
+
ctx.globalAlpha = GA * 1; ctx.rotate(rot);
|
|
170
|
+
for (let k = 0; k < 8; k++) {
|
|
171
|
+
const main = k % 2 === 0, len = s * (main ? 9 : 3.4), wd = s * (main ? 0.1 : 0.06);
|
|
172
|
+
ctx.save(); ctx.rotate(k * TAU / 8);
|
|
173
|
+
const gr = ctx.createLinearGradient(0, 0, len, 0);
|
|
174
|
+
gr.addColorStop(0, rgba(WHITE, a * (main ? 0.95 : 0.55))); gr.addColorStop(0.2, rgba(tint, a * (main ? 0.45 : 0.22)));
|
|
175
|
+
gr.addColorStop(0.6, rgba([170, 200, 255], a * (main ? 0.12 : 0.05))); gr.addColorStop(1, rgba(tint, 0));
|
|
176
|
+
ctx.fillStyle = gr; ctx.beginPath(); ctx.moveTo(0, -wd); ctx.lineTo(len, 0); ctx.lineTo(0, wd); ctx.closePath(); ctx.fill();
|
|
177
|
+
ctx.restore();
|
|
178
|
+
}
|
|
179
|
+
ctx.globalAlpha = GA * a * 0.14; ctx.strokeStyle = rgba([190, 220, 255], 1); ctx.lineWidth = s * 0.22;
|
|
180
|
+
ctx.beginPath(); ctx.arc(0, 0, s * 2.4, 0, TAU); ctx.stroke();
|
|
181
|
+
ctx.restore();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------- ringed gas giant (per-pixel lit, cached)
|
|
185
|
+
const PR = 190, PTILT = 0.3, PINC = 0.27, PR0 = 1.34 * PR, PR1 = 1.96 * PR;
|
|
186
|
+
function planetSprite() {
|
|
187
|
+
if (C.planet) return C.planet;
|
|
188
|
+
const R = PR, cT = Math.cos(PTILT), sT = Math.sin(PTILT), inc = PINC, si = Math.sqrt(1 - inc * inc);
|
|
189
|
+
const Ex = [cT, sT, 0], Ez = [-sT * inc, cT * inc, si], Nn = [sT * si, -cT * si, inc];
|
|
190
|
+
const rx = PR1, ry = PR1 * inc;
|
|
191
|
+
const hw = Math.ceil(Math.max(R * 1.2, Math.sqrt((rx * cT) ** 2 + (ry * sT) ** 2)) + 6);
|
|
192
|
+
const hh = Math.ceil(Math.max(R * 1.2, Math.sqrt((rx * sT) ** 2 + (ry * cT) ** 2)) + 6);
|
|
193
|
+
const w = hw * 2, h = hh * 2, cv = H.layer(w, h), x = cv.getContext('2d'), img = x.createImageData(w, h), d = img.data;
|
|
194
|
+
const nz = H.makeNoise(2207);
|
|
195
|
+
const ringD = (rr) => {
|
|
196
|
+
const u = (rr - PR0) / (PR1 - PR0); if (u < 0 || u > 1) return 0;
|
|
197
|
+
let v = 0.6 + 0.17 * Math.sin(u * 47) + 0.12 * Math.sin(u * 131 + 1.3) + 0.08 * Math.sin(u * 289 + 0.4);
|
|
198
|
+
if (u < 0.24) v *= 0.45 + u * 1.2;
|
|
199
|
+
v *= 1 - 0.93 * (sm(0.6, 0.62, u) * (1 - sm(0.66, 0.68, u)));
|
|
200
|
+
v *= sm(0, 0.03, u) * (1 - sm(0.95, 1, u));
|
|
201
|
+
return H.clamp(v);
|
|
202
|
+
};
|
|
203
|
+
const ringC = (rr) => { const u = H.clamp((rr - PR0) / (PR1 - PR0)); return u < 0.5 ? H.mix([170, 160, 160], [242, 224, 186], u * 2) : H.mix([242, 224, 186], [200, 168, 130], (u - 0.5) * 2); };
|
|
204
|
+
const pal = [[240, 224, 194], [222, 178, 112], [184, 110, 72], [236, 210, 164], [160, 116, 142], [208, 152, 98], [244, 232, 206], [150, 160, 170]];
|
|
205
|
+
const band = (v) => { const n = pal.length, f = wrap(v, n), i = Math.floor(f), fr = f - i, s = fr * fr * (3 - 2 * fr), a = pal[i], b = pal[(i + 1) % n]; return [a[0] + (b[0] - a[0]) * s, a[1] + (b[1] - a[1]) * s, a[2] + (b[2] - a[2]) * s]; };
|
|
206
|
+
const L = LV, L2n = Math.hypot(L[0], L[1]);
|
|
207
|
+
for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) {
|
|
208
|
+
const sx = i - hw + 0.5, sy = j - hh + 0.5, r2 = sx * sx + sy * sy, rd = Math.sqrt(r2);
|
|
209
|
+
const lx = sx * cT + sy * sT, ly = -sx * sT + sy * cT, rz = ly / inc, rr = Math.hypot(lx, rz);
|
|
210
|
+
let ra = 0, rc = null, front = rz > 0;
|
|
211
|
+
if (rr > PR0 && rr < PR1) {
|
|
212
|
+
ra = ringD(rr) * 0.94;
|
|
213
|
+
if (ra > 0) {
|
|
214
|
+
const Px = sx, Py = sy, Pz = rz * si, b = Px * L[0] + Py * L[1] + Pz * L[2], c = Px * Px + Py * Py + Pz * Pz - R * R;
|
|
215
|
+
const lit = b < 0 && b * b > c ? 0.1 : 1;
|
|
216
|
+
const cc = ringC(rr), gr = 0.9 + 0.1 * nz.n2(rr * 0.9, 3.3);
|
|
217
|
+
rc = [cc[0] * lit * gr, cc[1] * lit * gr, cc[2] * lit * gr];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
let oR = 0, oG = 0, oB = 0, oA = 0; // premultiplied
|
|
221
|
+
if (ra > 0 && !front && rd > R - 0.5) { oR = rc[0] * ra; oG = rc[1] * ra; oB = rc[2] * ra; oA = ra; }
|
|
222
|
+
if (rd < R + 0.5) {
|
|
223
|
+
const nx = sx / R, ny = sy / R, nzz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));
|
|
224
|
+
const lat = Math.asin(H.clamp(nx * Nn[0] + ny * Nn[1] + nzz * Nn[2], -1, 1));
|
|
225
|
+
const lon = Math.atan2(nx * Ez[0] + ny * Ez[1] + nzz * Ez[2], nx * Ex[0] + ny * Ex[1]);
|
|
226
|
+
const turb = nz.fbm3(Math.cos(lon) * 1.8, Math.sin(lon) * 1.8, lat * 11, 5);
|
|
227
|
+
let col = band(lat * 6.2 + turb * 0.75 + 0.03 * nz.n3(lon * 9, lat * 60, 2));
|
|
228
|
+
const fine = 1 + 0.07 * Math.sin(lat * 48 + turb * 4); col = [col[0] * fine, col[1] * fine, col[2] * fine];
|
|
229
|
+
const pole = sm(0.75, 1.25, Math.abs(lat));
|
|
230
|
+
col = H.mix(col, [118, 132, 160], pole * 0.7);
|
|
231
|
+
// great storm oval
|
|
232
|
+
const de = Math.pow((wrap(lon - 0.55 + Math.PI, TAU) - Math.PI) * Math.cos(lat) / 0.2, 2) + Math.pow((lat + 0.38) / 0.075, 2);
|
|
233
|
+
if (de < 1.8) { const s = 1 - sm(0.6, 1.0, de), rim = bump(0.7, 1.0, 1.1, 1.6, de); col = H.mix(col, [196, 92, 60], s * 0.85); col = H.mix(col, [250, 236, 210], rim * 0.5); }
|
|
234
|
+
const dl = nx * L[0] + ny * L[1] + nzz * L[2];
|
|
235
|
+
const day = sm(-0.1, 0.32, dl) * (0.3 + 0.7 * Math.pow(Math.max(dl, 0), 0.7));
|
|
236
|
+
let lr = col[0] * day, lg = col[1] * day, lb = col[2] * day;
|
|
237
|
+
const term = bump(-0.1, 0.05, 0.08, 0.3, dl) * 0.25; lr += 120 * term; lg += 40 * term; // warm terminator
|
|
238
|
+
lr += 10; lg += 7; lb += 22; // violet nebula fill on the night side
|
|
239
|
+
const limb = 0.55 + 0.45 * Math.pow(nzz, 0.6); lr *= limb; lg *= limb; lb *= limb;
|
|
240
|
+
// shadow of the rings on the cloud tops
|
|
241
|
+
const Pz = nzz * R, ln = L[0] * Nn[0] + L[1] * Nn[1] + L[2] * Nn[2], pn = sx * Nn[0] + sy * Nn[1] + Pz * Nn[2];
|
|
242
|
+
const s2 = -pn / ln;
|
|
243
|
+
if (s2 > 0) { const Qx = sx + L[0] * s2, Qy = sy + L[1] * s2, Qz = Pz + L[2] * s2, qx = Qx * Ex[0] + Qy * Ex[1], qz = Qx * Ez[0] + Qy * Ez[1] + Qz * Ez[2]; const sd = ringD(Math.hypot(qx, qz)) * 0.8; lr *= 1 - sd; lg *= 1 - sd; lb *= 1 - sd; }
|
|
244
|
+
// atmospheric rim scattering
|
|
245
|
+
const rim = Math.pow(1 - nzz, 2.6) * sm(-0.35, 0.5, dl);
|
|
246
|
+
lr += 110 * rim; lg += 190 * rim; lb += 240 * rim;
|
|
247
|
+
const ea = H.clamp(R - rd + 0.5);
|
|
248
|
+
oR = Math.min(255, lr) * ea + oR * (1 - ea); oG = Math.min(255, lg) * ea + oG * (1 - ea); oB = Math.min(255, lb) * ea + oB * (1 - ea); oA = ea + oA * (1 - ea);
|
|
249
|
+
}
|
|
250
|
+
if (rd > R - 1 && rd < R * 1.2) { // halo
|
|
251
|
+
const dr = Math.max(0, rd - R), face = sm(-0.5, 0.8, (sx * L[0] + sy * L[1]) / (rd * L2n));
|
|
252
|
+
const g = Math.exp(-dr / (0.03 * R)) * (0.12 + 0.88 * face) * 0.9;
|
|
253
|
+
oR += 100 * g; oG += 185 * g; oB += 245 * g; oA = Math.min(1, oA + g * 0.9);
|
|
254
|
+
}
|
|
255
|
+
if (ra > 0 && front) { oR = rc[0] * ra + oR * (1 - ra); oG = rc[1] * ra + oG * (1 - ra); oB = rc[2] * ra + oB * (1 - ra); oA = ra + oA * (1 - ra); }
|
|
256
|
+
const k = (j * w + i) * 4;
|
|
257
|
+
if (oA > 0.001) { d[k] = Math.min(255, oR / oA); d[k + 1] = Math.min(255, oG / oA); d[k + 2] = Math.min(255, oB / oA); d[k + 3] = oA * 255; }
|
|
258
|
+
}
|
|
259
|
+
x.putImageData(img, 0, 0);
|
|
260
|
+
return (C.planet = { cv, hw, hh });
|
|
261
|
+
}
|
|
262
|
+
function drawPlanet(ctx, x, y, R, a = 1) { const GA = ctx.globalAlpha;
|
|
263
|
+
const P = planetSprite(), s = R / PR;
|
|
264
|
+
ctx.save(); ctx.globalAlpha = GA * a; ctx.imageSmoothingQuality = 'high';
|
|
265
|
+
ctx.drawImage(P.cv, x - P.hw * s, y - P.hh * s, P.cv.width * s, P.cv.height * s); ctx.restore();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------- cratered moons
|
|
269
|
+
function moonSprite(seed, base) {
|
|
270
|
+
const key = 'moon' + seed;
|
|
271
|
+
if (C[key]) return C[key];
|
|
272
|
+
const R = 40, w = R * 2 + 4, cv = H.layer(w, w), x = cv.getContext('2d'), img = x.createImageData(w, w), d = img.data;
|
|
273
|
+
const r = H.rng(seed), nz = H.makeNoise(seed), cr = [];
|
|
274
|
+
for (let i = 0; i < 16; i++) { const a = r() * TAU, b = Math.acos(r() * 2 - 1); cr.push({ v: [Math.sin(b) * Math.cos(a), Math.sin(b) * Math.sin(a), Math.cos(b)], r: 0.08 + Math.pow(r(), 2) * 0.32 }); }
|
|
275
|
+
for (let j = 0; j < w; j++) for (let i = 0; i < w; i++) {
|
|
276
|
+
const nx = (i - w / 2 + 0.5) / R, ny = (j - w / 2 + 0.5) / R, q = nx * nx + ny * ny;
|
|
277
|
+
if (q > 1.03) continue;
|
|
278
|
+
const nzz = Math.sqrt(Math.max(0, 1 - q));
|
|
279
|
+
let alb = 0.82 + 0.18 * nz.fbm3(nx * 3, ny * 3, nzz * 3, 4);
|
|
280
|
+
for (const c of cr) {
|
|
281
|
+
const dd = Math.acos(H.clamp(nx * c.v[0] + ny * c.v[1] + nzz * c.v[2], -1, 1)) / c.r;
|
|
282
|
+
if (dd < 1) alb *= 0.78 + 0.12 * dd; else if (dd < 1.25) alb *= 1 + 0.16 * (1 - (dd - 1) / 0.25);
|
|
283
|
+
}
|
|
284
|
+
const dl = nx * LV[0] + ny * LV[1] + nzz * LV[2];
|
|
285
|
+
const lit = sm(-0.04, 0.22, dl) * (0.25 + 0.75 * Math.max(0, dl)) + 0.035;
|
|
286
|
+
const rim = Math.pow(1 - nzz, 3) * sm(-0.2, 0.5, dl) * 0.35;
|
|
287
|
+
const k = (j * w + i) * 4, ea = H.clamp((1 - Math.sqrt(q)) * R + 0.5);
|
|
288
|
+
d[k] = Math.min(255, base[0] * alb * lit + 120 * rim); d[k + 1] = Math.min(255, base[1] * alb * lit + 190 * rim); d[k + 2] = Math.min(255, base[2] * alb * lit + 240 * rim); d[k + 3] = ea * 255;
|
|
289
|
+
}
|
|
290
|
+
x.putImageData(img, 0, 0);
|
|
291
|
+
return (C[key] = cv);
|
|
292
|
+
}
|
|
293
|
+
function drawMoon(ctx, x, y, r, seed, base, a = 1) { const GA = ctx.globalAlpha;
|
|
294
|
+
const m = moonSprite(seed, base), s = r / 40;
|
|
295
|
+
ctx.save(); ctx.globalAlpha = GA * a; ctx.drawImage(m, x - m.width / 2 * s, y - m.height / 2 * s, m.width * s, m.height * s); ctx.restore();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------- comet
|
|
299
|
+
// pts: positions from the head backwards along the path. dir: unit vector for the straight ion tail.
|
|
300
|
+
function comet(ctx, pts, a, sc = 1, dir = null) { const GA = ctx.globalAlpha;
|
|
301
|
+
if (a <= 0.003 || pts.length < 2) return;
|
|
302
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.lineCap = 'butt'; ctx.lineJoin = 'round';
|
|
303
|
+
const n = pts.length;
|
|
304
|
+
// dust tail: a broad curved fan of strands, widening and fading away from the head
|
|
305
|
+
const nrm = pts.map((p, i) => { const q = pts[Math.min(n - 1, i + 1)], o = pts[Math.max(0, i - 1)], dx = q[0] - o[0], dy = q[1] - o[1], l = Math.hypot(dx, dy) || 1; return [-dy / l, dx / l]; });
|
|
306
|
+
for (const [off, w, al, c] of [[0, 34, 0.03, [255, 196, 150]], [0.5, 22, 0.04, [255, 210, 170]], [1, 14, 0.05, [255, 220, 190]], [0, 10, 0.1, [255, 232, 204]], [0.3, 3, 0.22, CREAM], [0.8, 2, 0.12, CREAM]]) {
|
|
307
|
+
for (let i = 0; i < n - 1; i++) {
|
|
308
|
+
const f = i / (n - 1), f2 = (i + 1) / (n - 1), fa = Math.pow(1 - f, 1.4), o1 = off * f * f * 46 * sc, o2 = off * f2 * f2 * 46 * sc;
|
|
309
|
+
ctx.strokeStyle = rgba(c, al * fa * a); ctx.lineWidth = w * sc * (0.3 + 1.6 * f);
|
|
310
|
+
ctx.beginPath(); ctx.moveTo(pts[i][0] + nrm[i][0] * o1, pts[i][1] + nrm[i][1] * o1); ctx.lineTo(pts[i + 1][0] + nrm[i + 1][0] * o2, pts[i + 1][1] + nrm[i + 1][1] * o2); ctx.stroke();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const [hx, hy] = pts[0];
|
|
314
|
+
if (dir) {
|
|
315
|
+
const len = 230 * sc, ex = hx + dir[0] * len, ey = hy + dir[1] * len;
|
|
316
|
+
for (const [w, al] of [[7, 0.12], [1.6, 0.5]]) {
|
|
317
|
+
const g = ctx.createLinearGradient(hx, hy, ex, ey); g.addColorStop(0, rgba([140, 210, 255], al * a)); g.addColorStop(1, rgba([90, 140, 255], 0));
|
|
318
|
+
ctx.strokeStyle = g; ctx.lineWidth = w * sc; ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(ex, ey); ctx.stroke();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
ctx.globalAlpha = GA * a * 0.7; ctx.drawImage(glow([170, 230, 255]), hx - 55 * sc, hy - 55 * sc, 110 * sc, 110 * sc);
|
|
322
|
+
ctx.globalAlpha = GA * a * 0.5; ctx.drawImage(glow([200, 255, 240]), hx - 22 * sc, hy - 22 * sc, 44 * sc, 44 * sc);
|
|
323
|
+
ctx.globalAlpha = GA * a; ctx.drawImage(glow(WHITE), hx - 12 * sc, hy - 12 * sc, 24 * sc, 24 * sc);
|
|
324
|
+
ctx.restore();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------- chart line work
|
|
328
|
+
const ellPt = (cx, cy, rx, ry, rot, a) => { const c = Math.cos(rot), s = Math.sin(rot), x = rx * Math.cos(a), y = ry * Math.sin(a); return [cx + x * c - y * s, cy + x * s + y * c]; };
|
|
329
|
+
function chevron(ctx, x, y, ang, sz, c, a, lw = 1.4) {
|
|
330
|
+
ctx.save(); ctx.strokeStyle = rgba(c, a); ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
331
|
+
ctx.beginPath(); ctx.moveTo(x + Math.cos(ang + 2.6) * sz, y + Math.sin(ang + 2.6) * sz); ctx.lineTo(x, y); ctx.lineTo(x + Math.cos(ang - 2.6) * sz, y + Math.sin(ang - 2.6) * sz); ctx.stroke(); ctx.restore();
|
|
332
|
+
}
|
|
333
|
+
// dotted orbit ellipse with small open arrowheads; draws on with prog; occl hides the far half behind a disk
|
|
334
|
+
function orbit(ctx, o) {
|
|
335
|
+
const { cx, cy, rx, ry, rot = 0, a0 = 0, sweep = TAU, prog = 1, c = GOLD, a = 0.8, gap = 7, r = 1.25, arrows = 3, dir = 1, occl = null, crawl = 0 } = o;
|
|
336
|
+
if (prog <= 0 || a <= 0) return;
|
|
337
|
+
const per = Math.PI * (rx + ry) * sweep / TAU, n = Math.max(40, Math.ceil(per / 2));
|
|
338
|
+
ctx.save(); ctx.fillStyle = rgba(c, a);
|
|
339
|
+
let acc = wrap(crawl, gap), last = null;
|
|
340
|
+
for (let k = 0; k <= n * prog; k++) {
|
|
341
|
+
const th = a0 + dir * sweep * k / n, p = ellPt(cx, cy, rx, ry, rot, th);
|
|
342
|
+
if (last) acc += Math.hypot(p[0] - last[0], p[1] - last[1]);
|
|
343
|
+
last = p;
|
|
344
|
+
if (acc >= gap) {
|
|
345
|
+
acc -= gap;
|
|
346
|
+
if (occl && Math.sin(th) < 0 && Math.hypot(p[0] - occl[0], p[1] - occl[1]) < occl[2]) continue;
|
|
347
|
+
ctx.beginPath(); ctx.arc(p[0], p[1], r, 0, TAU); ctx.fill();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (let m = 0; m < arrows; m++) {
|
|
351
|
+
const f = (m + 0.5) / arrows; if (f > prog) continue;
|
|
352
|
+
const th = a0 + dir * sweep * f, p = ellPt(cx, cy, rx, ry, rot, th), q = ellPt(cx, cy, rx, ry, rot, th + dir * 0.01);
|
|
353
|
+
if (occl && Math.sin(th) < 0 && Math.hypot(p[0] - occl[0], p[1] - occl[1]) < occl[2]) continue;
|
|
354
|
+
chevron(ctx, p[0], p[1], Math.atan2(q[1] - p[1], q[0] - p[0]), 7, c, a);
|
|
355
|
+
}
|
|
356
|
+
ctx.restore();
|
|
357
|
+
}
|
|
358
|
+
// fine solid chart line (optionally dotted) drawn on from (x0,y0) to (x1,y1), quadratic via (qx,qy)
|
|
359
|
+
function chartLine(ctx, x0, y0, x1, y1, o = {}) {
|
|
360
|
+
const { prog = 1, c = GOLD, a = 0.9, lw = 1.3, q = null, arrow = true, dotted = false, glowA = 0 } = o;
|
|
361
|
+
if (prog <= 0 || a <= 0) return null;
|
|
362
|
+
const P = (u) => q ? [(1 - u) * (1 - u) * x0 + 2 * (1 - u) * u * q[0] + u * u * x1, (1 - u) * (1 - u) * y0 + 2 * (1 - u) * u * q[1] + u * u * y1] : [x0 + (x1 - x0) * u, y0 + (y1 - y0) * u];
|
|
363
|
+
const n = 48, end = Math.max(1, Math.ceil(n * prog));
|
|
364
|
+
ctx.save(); ctx.lineCap = 'round';
|
|
365
|
+
const path = () => { ctx.beginPath(); for (let i = 0; i <= end; i++) { const p = P(Math.min(prog, i / n)); i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1]); } };
|
|
366
|
+
if (glowA > 0) { ctx.globalCompositeOperation = 'lighter'; ctx.strokeStyle = rgba(c, glowA * a); ctx.lineWidth = lw * 6; path(); ctx.stroke(); ctx.globalCompositeOperation = 'source-over'; }
|
|
367
|
+
ctx.strokeStyle = rgba(c, a); ctx.lineWidth = lw; if (dotted) ctx.setLineDash([1.5, 6]);
|
|
368
|
+
path(); ctx.stroke(); ctx.setLineDash([]);
|
|
369
|
+
const p1 = P(prog), p0 = P(Math.max(0, prog - 0.02));
|
|
370
|
+
if (arrow) chevron(ctx, p1[0], p1[1], Math.atan2(p1[1] - p0[1], p1[0] - p0[0]), 9, c, a, lw + 0.2);
|
|
371
|
+
ctx.restore();
|
|
372
|
+
return p1;
|
|
373
|
+
}
|
|
374
|
+
// graduated degree ring (astrolabe scale)
|
|
375
|
+
function degreeRing(ctx, cx, cy, r, prog, c, a, from = 0, numerals = true) {
|
|
376
|
+
if (prog <= 0 || a <= 0) return;
|
|
377
|
+
ctx.save(); ctx.strokeStyle = rgba(c, a); ctx.lineWidth = 1;
|
|
378
|
+
ctx.beginPath(); ctx.arc(cx, cy, r, from, from + TAU * prog); ctx.stroke();
|
|
379
|
+
const ga = ctx.globalAlpha; ctx.beginPath(); ctx.arc(cx, cy, r + 5, from, from + TAU * prog); ctx.globalAlpha = ga * 0.5; ctx.stroke(); ctx.globalAlpha = ga;
|
|
380
|
+
ctx.beginPath();
|
|
381
|
+
for (let dg = 0; dg < 360 * prog; dg += 2) {
|
|
382
|
+
const an = from + dg * Math.PI / 180, l = dg % 30 === 0 ? 13 : dg % 10 === 0 ? 8 : 4;
|
|
383
|
+
ctx.moveTo(cx + Math.cos(an) * r, cy + Math.sin(an) * r); ctx.lineTo(cx + Math.cos(an) * (r - l), cy + Math.sin(an) * (r - l));
|
|
384
|
+
}
|
|
385
|
+
ctx.stroke();
|
|
386
|
+
if (numerals) {
|
|
387
|
+
ctx.fillStyle = rgba(c, a * 0.9); ctx.font = `600 14px ${CORM}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
388
|
+
for (let dg = 0; dg < 360 * prog; dg += 30) { const an = from + dg * Math.PI / 180; ctx.fillText(dg + '°', cx + Math.cos(an) * (r + 17), cy + Math.sin(an) * (r + 17)); }
|
|
389
|
+
}
|
|
390
|
+
ctx.restore();
|
|
391
|
+
}
|
|
392
|
+
function marker(ctx, x, y, r, c, a, lw = 1.2) {
|
|
393
|
+
if (a <= 0) return;
|
|
394
|
+
ctx.save(); ctx.strokeStyle = rgba(c, a); ctx.fillStyle = rgba(c, a); ctx.lineWidth = lw;
|
|
395
|
+
ctx.beginPath(); ctx.arc(x, y, r, 0, TAU); ctx.stroke();
|
|
396
|
+
ctx.beginPath(); ctx.arc(x, y, 1.8, 0, TAU); ctx.fill();
|
|
397
|
+
ctx.beginPath();
|
|
398
|
+
for (let k = 0; k < 4; k++) { const an = k * Math.PI / 2; ctx.moveTo(x + Math.cos(an) * (r + 3), y + Math.sin(an) * (r + 3)); ctx.lineTo(x + Math.cos(an) * (r + 9), y + Math.sin(an) * (r + 9)); }
|
|
399
|
+
ctx.stroke(); ctx.restore();
|
|
400
|
+
}
|
|
401
|
+
function label(ctx, txt, x, y, o = {}) {
|
|
402
|
+
const size = o.size || 26, caps = !o.italic && !o.font && o.caps !== false;
|
|
403
|
+
if ((o.a ?? 1) <= 0) return;
|
|
404
|
+
ctx.save();
|
|
405
|
+
ctx.font = o.font || (o.italic ? `italic 400 ${size}px ${BASK}` : `600 ${size}px ${CORM}`);
|
|
406
|
+
ctx.letterSpacing = caps ? (size * (o.track ?? 0.16)).toFixed(1) + 'px' : (o.track ? (size * o.track).toFixed(1) + 'px' : '0px');
|
|
407
|
+
ctx.textAlign = o.align || 'left'; ctx.textBaseline = o.base || 'alphabetic';
|
|
408
|
+
ctx.globalAlpha *= o.a ?? 1;
|
|
409
|
+
const s = caps ? txt.toUpperCase() : txt;
|
|
410
|
+
ctx.shadowColor = 'rgba(2,3,14,0.95)'; ctx.shadowBlur = o.halo ?? 14;
|
|
411
|
+
ctx.lineJoin = 'round'; ctx.strokeStyle = 'rgba(2,3,14,0.6)'; ctx.lineWidth = Math.max(3, size * 0.16); ctx.strokeText(s, x, y);
|
|
412
|
+
ctx.fillStyle = rgba(o.color || GOLD, 1); ctx.fillText(s, x, y);
|
|
413
|
+
ctx.shadowBlur = 0; ctx.fillText(s, x, y);
|
|
414
|
+
ctx.restore();
|
|
415
|
+
}
|
|
416
|
+
// a labelled chart star (small glow + crisp core)
|
|
417
|
+
function chartStar(ctx, x, y, m, c, a) { const GA = ctx.globalAlpha;
|
|
418
|
+
if (a <= 0) return;
|
|
419
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = GA * a;
|
|
420
|
+
const s = 10 + m * 26; ctx.drawImage(glow(c), x - s / 2, y - s / 2, s, s);
|
|
421
|
+
ctx.fillStyle = rgba(WHITE, a); ctx.beginPath(); ctx.arc(x, y, 1.2 + m * 1.8, 0, TAU); ctx.fill(); ctx.restore();
|
|
422
|
+
}
|
|
423
|
+
// constellation figures for the examples: cat (pointed ears) and dog (floppy ears)
|
|
424
|
+
const GLYPH = {
|
|
425
|
+
cat: { p: [[-0.72, -1], [-0.28, -0.42], [0.28, -0.42], [0.72, -1], [0.9, -0.15], [0.72, 0.45], [0, 0.82], [-0.72, 0.45], [-0.9, -0.15], [-0.33, 0.02], [0.33, 0.02], [0, 0.36]],
|
|
426
|
+
l: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 0], [9, 11], [11, 10]] },
|
|
427
|
+
dog: { p: [[-0.45, -0.85], [0.45, -0.85], [1.0, -0.55], [0.95, 0.35], [0.45, 0.25], [0.32, 0.75], [0, 0.9], [-0.32, 0.75], [-0.45, 0.25], [-0.95, 0.35], [-1.0, -0.55], [-0.3, -0.12], [0.3, -0.12]],
|
|
428
|
+
l: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 0], [8, 4]] },
|
|
429
|
+
};
|
|
430
|
+
function glyph(ctx, type, x, y, s, c, a, prog = 1) {
|
|
431
|
+
if (a <= 0) return;
|
|
432
|
+
const G = GLYPH[type], P = G.p.map(([u, v]) => [x + u * s, y + v * s]);
|
|
433
|
+
ctx.save(); ctx.strokeStyle = rgba(c, a * 0.85); ctx.lineWidth = 1.3; ctx.lineCap = 'round';
|
|
434
|
+
const nl = G.l.length, upto = prog * nl;
|
|
435
|
+
for (let i = 0; i < nl && i < upto; i++) {
|
|
436
|
+
const [A, B] = G.l[i], pa = P[A], pb = P[B], f = Math.min(1, upto - i), dx = pb[0] - pa[0], dy = pb[1] - pa[1], L = Math.hypot(dx, dy), g = Math.min(5, L * 0.2);
|
|
437
|
+
ctx.beginPath(); ctx.moveTo(pa[0] + dx / L * g, pa[1] + dy / L * g); ctx.lineTo(pa[0] + dx / L * (g + (L - 2 * g) * f), pa[1] + dy / L * (g + (L - 2 * g) * f)); ctx.stroke();
|
|
438
|
+
}
|
|
439
|
+
ctx.restore();
|
|
440
|
+
P.forEach((p, i) => { if (i / P.length <= prog + 0.05) chartStar(ctx, p[0], p[1], i >= 9 ? 0.15 : 0.32, c, a); });
|
|
441
|
+
}
|
|
442
|
+
const fmt = (n) => Math.round(n).toLocaleString('en-US');
|
|
443
|
+
|
|
444
|
+
// ================================================================= STILL + CLIP
|
|
445
|
+
// b = build progress per element (all 1 on the finished still)
|
|
446
|
+
function stillScene(ctx, W, Hh, t, B) {
|
|
447
|
+
const camz = 1 + 0.1 * (1 - H.ease.out(B.cam));
|
|
448
|
+
sky(ctx, W, Hh, t, { x: t * 6, y: 0, z: camz, bright: B.sky, nx: -40, ny: 40 });
|
|
449
|
+
const PX = 740, PY = 450, R = 186;
|
|
450
|
+
// the light source: a bright star with diffraction spikes
|
|
451
|
+
spikeStar(ctx, 1150, 112, 16 * (0.6 + 0.4 * B.star), B.star, [255, 232, 196], 0.12);
|
|
452
|
+
// planet + moons on their orbits
|
|
453
|
+
const om = [{ rx: 452, ry: 124, rot: PTILT, w: 0.11, ph: 0.26, r: 21, seed: 31, base: [214, 206, 196] },
|
|
454
|
+
{ rx: 330, ry: 214, rot: -0.62, w: -0.16, ph: -0.27, r: 15, seed: 47, base: [226, 190, 160] }];
|
|
455
|
+
ctx.save(); ctx.translate(PX, PY); ctx.scale(camz, camz); ctx.translate(-PX, -PY);
|
|
456
|
+
const mpos = om.map((o) => { const th = o.ph + t * o.w; return { p: ellPt(PX, PY, o.rx, o.ry, o.rot, th), back: Math.sin(th) < 0, o }; });
|
|
457
|
+
mpos.filter((m) => m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r * 0.92, m.o.seed, m.o.base, B.planet));
|
|
458
|
+
drawPlanet(ctx, PX, PY, R, B.planet);
|
|
459
|
+
mpos.filter((m) => !m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r, m.o.seed, m.o.base, B.planet));
|
|
460
|
+
// chart overlay
|
|
461
|
+
const ca = 0.85;
|
|
462
|
+
degreeRing(ctx, PX, PY, 505, B.ring, GOLD, 0.62, -Math.PI * 0.95, true);
|
|
463
|
+
om.forEach((o, i) => orbit(ctx, { cx: PX, cy: PY, rx: o.rx, ry: o.ry, rot: o.rot, prog: B.orbits, c: i ? CYAN : GOLD, a: ca * 0.8, arrows: 4, crawl: t * 9, occl: [PX, PY, R] }));
|
|
464
|
+
mpos.forEach((m, i) => marker(ctx, m.p[0], m.p[1], m.o.r + 9, i ? CYAN : GOLD, 0.8 * B.labels));
|
|
465
|
+
ctx.restore();
|
|
466
|
+
// comet streaking toward the planet
|
|
467
|
+
const cs = H.ease.out(B.comet), path = (u) => [-260 + 570 * u, 200 + 100 * u - 50 * Math.sin(u * Math.PI)];
|
|
468
|
+
const pts = []; for (let i = 0; i < 30; i++) { const u = cs - i * 0.03; if (u < -0.6) break; pts.push(path(u)); }
|
|
469
|
+
comet(ctx, pts, B.comet > 0 ? 1 : 0, 1.3, [-0.966, 0.257]);
|
|
470
|
+
// constellation of examples, bottom left
|
|
471
|
+
const cA = B.const;
|
|
472
|
+
const cst = [[92, 520, 'cat'], [204, 606, 'dog'], [318, 534, 'cat']];
|
|
473
|
+
ctx.save();
|
|
474
|
+
for (let i = 0; i < cst.length - 1; i++) chartLine(ctx, cst[i][0], cst[i][1], cst[i + 1][0], cst[i + 1][1], { prog: H.clamp(cA * 3 - i), c: GOLD, a: 0.45, lw: 1, arrow: false, dotted: true });
|
|
475
|
+
ctx.restore();
|
|
476
|
+
cst.forEach(([x, y, ty], i) => { const p = H.clamp(cA * 3 - i * 0.7); glyph(ctx, ty, x, y, 26, i % 2 ? CYAN : GOLD, p, p); label(ctx, ty, x, y + 60, { italic: true, size: 22, color: CREAM, align: 'center', a: p }); });
|
|
477
|
+
label(ctx, 'Training data', 60, 462, { size: 20, a: sm(0.6, 1, cA), color: GOLD });
|
|
478
|
+
// labels with leader lines
|
|
479
|
+
const la = B.labels;
|
|
480
|
+
chartLine(ctx, PX + 150, PY - 70, 1000, 372, { prog: la, c: GOLD, a: 0.8, arrow: false });
|
|
481
|
+
marker(ctx, PX + 150, PY - 70, 6, GOLD, la);
|
|
482
|
+
label(ctx, 'The model', 1010, 380, { size: 26, a: sm(0.4, 1, la) });
|
|
483
|
+
label(ctx, 'a planet', 1012, 408, { italic: true, size: 19, color: CREAM, a: sm(0.5, 1, la) });
|
|
484
|
+
const wp = ellPt(PX, PY, 452, 124, PTILT, 2.0);
|
|
485
|
+
marker(ctx, wp[0], wp[1], 5, CYAN, la);
|
|
486
|
+
chartLine(ctx, wp[0], wp[1], wp[0] - 14, wp[1] + 108, { prog: la, c: CYAN, a: 0.8, arrow: false });
|
|
487
|
+
label(ctx, 'Weights', wp[0] - 24, wp[1] + 134, { size: 24, color: CYAN, a: sm(0.4, 1, la) });
|
|
488
|
+
label(ctx, 'its orbiting moons', wp[0] - 22, wp[1] + 160, { italic: true, size: 18, color: CREAM, a: sm(0.5, 1, la) });
|
|
489
|
+
// title cartouche
|
|
490
|
+
const ta = B.title;
|
|
491
|
+
label(ctx, 'Chart I · the learning machine', 72, 92, { size: 18, a: sm(0, 0.4, ta), track: 0.22 });
|
|
492
|
+
ctx.save(); ctx.globalAlpha = sm(0.15, 0.7, ta);
|
|
493
|
+
ctx.font = `800 64px ${SYNE}`; ctx.fillStyle = rgba(CREAM, 1); ctx.shadowColor = 'rgba(2,3,14,0.9)'; ctx.shadowBlur = 18;
|
|
494
|
+
ctx.letterSpacing = (8 * (1 - H.ease.out(sm(0.15, 0.8, ta)))).toFixed(1) + 'px';
|
|
495
|
+
ctx.fillText('A Mind in Orbit', 68, 160); ctx.restore();
|
|
496
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.8); ctx.lineWidth = 1; const ul = 470 * sm(0.3, 1, ta);
|
|
497
|
+
ctx.beginPath(); ctx.moveTo(72, 182); ctx.lineTo(72 + ul, 182); for (let x = 0; x <= ul; x += 10) { ctx.moveTo(72 + x, 182); ctx.lineTo(72 + x, 182 + (x % 50 === 0 ? 8 : 4)); } ctx.stroke(); ctx.restore();
|
|
498
|
+
H.vignette(ctx, W, Hh, 0.6, '1,2,8');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ================================================================= EXPLAINER
|
|
502
|
+
// Timing table shared by explainer() and explainerSound()
|
|
503
|
+
const X = {
|
|
504
|
+
D: 15,
|
|
505
|
+
hookIn: [0.15, 0.95], shrink: [1.5, 2.3], ruleIn: [0.5, 1.25],
|
|
506
|
+
push: [0, 2.9],
|
|
507
|
+
model: 2.65, weights: 3.2,
|
|
508
|
+
comets: Array.from({ length: 8 }, (_, i) => ({ t0: 2.5 + i * 0.3, dur: 1.1, type: 'cddccdcd'[i] === 'c' ? 'cat' : 'dog', y0: [180, 450, 300, 520, 140, 380, 250, 470][i] })),
|
|
509
|
+
count: [2.6, 5.65],
|
|
510
|
+
warp: [5.85, 6.25, 6.7],
|
|
511
|
+
exIn: [6.45, 7.0], stars: 6.75,
|
|
512
|
+
guess: [7.05, 7.5], check: 7.95, truth: 8.15, back: [8.3, 8.85], adjust: [8.9, 9.55], guess2: [9.6, 9.95],
|
|
513
|
+
pan: [10.45, 11.15],
|
|
514
|
+
pts0: 11.05, ptsDt: 0.12, hit: 12.55,
|
|
515
|
+
take: [13.15, 13.45, 13.75],
|
|
516
|
+
};
|
|
517
|
+
const PA = { x: 900, y: 400, R: 140 };
|
|
518
|
+
const PB = { x: 300, y: 432, R: 104 };
|
|
519
|
+
const DOG = [1000, 282], CAT = [1000, 566], EXP = [176, 238];
|
|
520
|
+
const ACC = Array.from({ length: 13 }, (_, i) => i === 12 ? 97 : 61 + 36 * (1 - Math.exp(-i / 3.0)) / (1 - Math.exp(-12 / 3.0)) + [0, -1.5, 1.2, -0.8, 1.4, -1.1, 0.6, -0.7, 0.5, -0.4, 0.3, -0.2, 0][i]);
|
|
521
|
+
const cometPath = (c) => {
|
|
522
|
+
const ex = PA.x - PA.R * 0.94, ey = PA.y + (c.y0 - 330) * 0.28;
|
|
523
|
+
const q = [380, c.y0 + (ey - c.y0) * 0.15 - 40];
|
|
524
|
+
return (u) => [(1 - u) * (1 - u) * -90 + 2 * (1 - u) * u * q[0] + u * u * ex, (1 - u) * (1 - u) * c.y0 + 2 * (1 - u) * u * q[1] + u * u * ey];
|
|
525
|
+
};
|
|
526
|
+
const cometU = (c, t) => { const u = (t - c.t0) / c.dur; return u < 0 ? -1 : u * (0.55 + 0.45 * u); };
|
|
527
|
+
|
|
528
|
+
function sceneA(ctx, t) { const GA = ctx.globalAlpha;
|
|
529
|
+
const E = H.ease;
|
|
530
|
+
// moons (weights) on orbits around the model planet
|
|
531
|
+
const om = [{ rx: 318, ry: 86, rot: PTILT, w: 0.32, ph: 0.6, r: 16, seed: 31, base: [214, 206, 196] },
|
|
532
|
+
{ rx: 236, ry: 150, rot: -0.62, w: -0.42, ph: 2.4, r: 12, seed: 47, base: [226, 190, 160] }];
|
|
533
|
+
const mp = om.map((o) => { const th = o.ph + t * o.w; return { p: ellPt(PA.x, PA.y, o.rx, o.ry, o.rot, th), back: Math.sin(th) < 0, o }; });
|
|
534
|
+
mp.filter((m) => m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r * 0.9, m.o.seed, m.o.base));
|
|
535
|
+
drawPlanet(ctx, PA.x, PA.y, PA.R);
|
|
536
|
+
// arrival flashes on the planet's limb
|
|
537
|
+
for (const c of X.comets) {
|
|
538
|
+
const ta = c.t0 + c.dur, fa = bump(ta - 0.02, ta + 0.03, ta + 0.1, ta + 0.55, t);
|
|
539
|
+
if (fa > 0) { const P = cometPath(c)(1); ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = GA * fa; ctx.drawImage(glow([170, 230, 255]), P[0] - 60, P[1] - 60, 120, 120); ctx.restore(); marker(ctx, P[0], P[1], 6 + 26 * sm(ta, ta + 0.55, t), CYAN, fa * 0.9); }
|
|
540
|
+
}
|
|
541
|
+
mp.filter((m) => !m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r, m.o.seed, m.o.base));
|
|
542
|
+
// chart overlay
|
|
543
|
+
degreeRing(ctx, PA.x, PA.y, 372, E.inOut(sm(0.6, 2.4, t)), GOLD, 0.38, -Math.PI / 2, true);
|
|
544
|
+
om.forEach((o, i) => orbit(ctx, { cx: PA.x, cy: PA.y, rx: o.rx, ry: o.ry, rot: o.rot, prog: E.inOut(sm(1.0 + i * 0.3, 2.2 + i * 0.3, t)), c: i ? CYAN : GOLD, a: 0.75, arrows: 4, crawl: t * 10, occl: [PA.x, PA.y, PA.R] }));
|
|
545
|
+
// labels
|
|
546
|
+
const ml = sm(X.model, X.model + 0.35, t);
|
|
547
|
+
const m0 = [PA.x + 70, PA.y - 122];
|
|
548
|
+
chartLine(ctx, m0[0], m0[1], 976, 150, { prog: E.out(sm(X.model - 0.15, X.model + 0.2, t)), arrow: false, a: 0.85 });
|
|
549
|
+
marker(ctx, m0[0], m0[1], 6, GOLD, ml);
|
|
550
|
+
label(ctx, 'The model', 988, 148, { size: 36, a: ml });
|
|
551
|
+
label(ctx, 'a planet', 990, 188, { italic: true, size: 30, color: CREAM, a: sm(X.model + 0.2, X.model + 0.5, t) });
|
|
552
|
+
const wl = sm(X.weights, X.weights + 0.35, t), wp = ellPt(PA.x, PA.y, om[0].rx, om[0].ry, om[0].rot, 0.5);
|
|
553
|
+
if (wl > 0) {
|
|
554
|
+
chartLine(ctx, wp[0], wp[1] + 8, wp[0] - 40, 612, { prog: E.out(sm(X.weights - 0.15, X.weights + 0.2, t)), c: CYAN, arrow: false, a: 0.8 });
|
|
555
|
+
marker(ctx, wp[0], wp[1], 7, CYAN, wl * 0.9);
|
|
556
|
+
label(ctx, 'Weights', wp[0] - 110, 640, { size: 36, color: CYAN, a: wl });
|
|
557
|
+
label(ctx, 'its moons', wp[0] - 108, 678, { italic: true, size: 30, color: CREAM, a: sm(X.weights + 0.2, X.weights + 0.5, t) });
|
|
558
|
+
}
|
|
559
|
+
// comets of training data: each head is a constellation figure with its label
|
|
560
|
+
for (const c of X.comets) {
|
|
561
|
+
const u = cometU(c, t); if (u < -0.05 || u > 1.05) continue;
|
|
562
|
+
const P = cometPath(c), head = P(Math.min(1, u));
|
|
563
|
+
const pa = (1 - sm(0.97, 1.04, u)) * sm(-0.05, 0.05, u);
|
|
564
|
+
// faint dotted path ahead of the comet
|
|
565
|
+
ctx.save(); ctx.fillStyle = rgba(GOLD, 0.45 * pa); for (let s = Math.max(0, u); s < 1; s += 0.025) { const p = P(s); ctx.beginPath(); ctx.arc(p[0], p[1], 1.1, 0, TAU); ctx.fill(); } ctx.restore();
|
|
566
|
+
if (u < 0.92) { const p = P(Math.max(u + 0.05, 0.5)), p2 = P(Math.max(u + 0.06, 0.51)); chevron(ctx, p[0], p[1], Math.atan2(p2[1] - p[1], p2[0] - p[0]), 7, GOLD, 0.6 * pa); }
|
|
567
|
+
const pts = []; for (let i = 0; i < 22; i++) pts.push(P(Math.min(1, u) - i * 0.022));
|
|
568
|
+
const vx = head[0] - pts[1][0], vy = head[1] - pts[1][1], vl = Math.hypot(vx, vy) || 1;
|
|
569
|
+
const ion = [-vx / vl * 0.94 - vy / vl * 0.34, -vy / vl * 0.94 + vx / vl * 0.34 - 0.1];
|
|
570
|
+
comet(ctx, pts, pa, 0.75, ion);
|
|
571
|
+
const gs = 40 * (1 - 0.35 * sm(0.75, 1, u));
|
|
572
|
+
glyph(ctx, c.type, head[0], head[1] - 64, gs, c.type === 'cat' ? GOLDB : CYAN, pa);
|
|
573
|
+
label(ctx, c.type, head[0], head[1] + 44, { italic: true, size: 36, color: CREAM, align: 'center', a: pa * (1 - sm(0.8, 0.95, u)) });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function counterA(ctx, t) { const GA = ctx.globalAlpha;
|
|
578
|
+
const a = sm(X.count[0] - 0.1, X.count[0] + 0.3, t);
|
|
579
|
+
if (a <= 0) return;
|
|
580
|
+
const p = H.ease.inOut(sm(X.count[0], X.count[1], t)), v = p >= 1 ? 1e6 : Math.round(Math.pow(10, 6 * p));
|
|
581
|
+
label(ctx, 'Training data', 72, 540, { size: 34, a });
|
|
582
|
+
ctx.save(); ctx.globalAlpha = GA * a; ctx.font = `800 72px ${SYNE}`; ctx.fillStyle = rgba(CREAM, 1); ctx.shadowColor = 'rgba(2,3,14,0.9)'; ctx.shadowBlur = 16;
|
|
583
|
+
const s = fmt(v); ctx.fillText(s, 66, 634); const w = ctx.measureText(s).width; ctx.restore();
|
|
584
|
+
label(ctx, 'examples', 66 + w + 16, 632, { italic: true, size: 34, color: GOLD, a });
|
|
585
|
+
// fine magnitude scale under the number (log ticks 10^0..10^6)
|
|
586
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.7 * a); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(72, 660); ctx.lineTo(560, 660);
|
|
587
|
+
for (let k = 0; k <= 60; k++) { const x = 72 + k * 488 / 60; ctx.moveTo(x, 660); ctx.lineTo(x, 660 + (k % 10 === 0 ? 10 : 4)); }
|
|
588
|
+
ctx.stroke(); const x = 72 + 488 * p; ctx.fillStyle = rgba(GOLDB, a); ctx.beginPath(); ctx.moveTo(x, 656); ctx.lineTo(x - 5, 648); ctx.lineTo(x + 5, 648); ctx.fill(); ctx.restore();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function sceneB(ctx, t) { const GA = ctx.globalAlpha;
|
|
592
|
+
const E = H.ease;
|
|
593
|
+
const adj = E.inOut(sm(X.adjust[0], X.adjust[1], t));
|
|
594
|
+
const rot0 = 0.34, rot1 = -0.12, rot = rot0 + (rot1 - rot0) * adj;
|
|
595
|
+
const om = [{ rx: 236, ry: 76, rot, w: 0.34, ph: 0.4, r: 14, seed: 31, base: [214, 206, 196] }, { rx: 236, ry: 76, rot, w: 0.34, ph: 0.4 + Math.PI, r: 10, seed: 59, base: [200, 214, 222] },
|
|
596
|
+
{ rx: 168, ry: 118, rot: -0.7, w: -0.45, ph: 2.0, r: 10, seed: 47, base: [226, 190, 160] }];
|
|
597
|
+
const mp = om.map((o) => { const th = o.ph + t * o.w + adj * 1.1 * (o.rot === rot ? 1 : 0); return { p: ellPt(PB.x, PB.y, o.rx, o.ry, o.rot, th), back: Math.sin(th) < 0, o }; });
|
|
598
|
+
mp.filter((m) => m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r * 0.9, m.o.seed, m.o.base));
|
|
599
|
+
drawPlanet(ctx, PB.x, PB.y, PB.R);
|
|
600
|
+
mp.filter((m) => !m.back).forEach((m) => drawMoon(ctx, m.p[0], m.p[1], m.o.r, m.o.seed, m.o.base));
|
|
601
|
+
// ghost of the old orbit once it has been adjusted
|
|
602
|
+
if (adj > 0) orbit(ctx, { cx: PB.x, cy: PB.y, rx: 236, ry: 76, rot: rot0, c: RED, a: 0.35 * adj, arrows: 0, gap: 9, r: 1, occl: [PB.x, PB.y, PB.R] });
|
|
603
|
+
orbit(ctx, { cx: PB.x, cy: PB.y, rx: 236, ry: 76, rot, c: adj > 0 ? GOLDB : GOLD, a: 0.85, arrows: 4, crawl: t * 10, occl: [PB.x, PB.y, PB.R], r: 1.25 + adj * 0.4 * (1 - sm(9.6, 10.2, t)) });
|
|
604
|
+
orbit(ctx, { cx: PB.x, cy: PB.y, rx: 168, ry: 118, rot: -0.7, c: CYAN, a: 0.6, arrows: 3, crawl: t * 10, occl: [PB.x, PB.y, PB.R] });
|
|
605
|
+
// the target stars
|
|
606
|
+
const sa = sm(X.stars, X.stars + 0.3, t);
|
|
607
|
+
for (const [P, nm] of [[DOG, 'dog'], [CAT, 'cat']]) {
|
|
608
|
+
spikeStar(ctx, P[0], P[1], 7, sa * 0.9, nm === 'dog' ? [200, 220, 255] : [255, 226, 180], 0.2);
|
|
609
|
+
}
|
|
610
|
+
// the new example arrives as a comet and parks top-left
|
|
611
|
+
const eu = E.out(sm(X.exIn[0], X.exIn[1], t)), ep = [EXP[0] - 260 * (1 - eu), EXP[1] - 150 * (1 - eu)];
|
|
612
|
+
if (t > X.exIn[0]) {
|
|
613
|
+
const pts = []; for (let i = 0; i < 16; i++) { const uu = Math.max(0, eu - i * 0.06); pts.push([EXP[0] - 260 * (1 - uu) - i * 4 * (1 - eu), EXP[1] - 150 * (1 - uu) - i * 2.3 * (1 - eu)]); }
|
|
614
|
+
comet(ctx, pts, 1 - 0.75 * sm(X.exIn[1], X.exIn[1] + 0.4, t), 0.8, null);
|
|
615
|
+
glyph(ctx, 'cat', ep[0], ep[1], 60, GOLDB, sm(X.exIn[0], X.exIn[0] + 0.2, t));
|
|
616
|
+
label(ctx, 'New example', 92, EXP[1] - 82, { size: 32, a: sm(X.exIn[1] - 0.1, X.exIn[1] + 0.2, t) });
|
|
617
|
+
chartLine(ctx, EXP[0] + 30, EXP[1] + 62, PB.x - 70, PB.y - 92, { prog: E.out(sm(X.exIn[1], X.guess[0], t)), q: [EXP[0] + 90, EXP[1] + 110], a: 0.7, c: GOLD });
|
|
618
|
+
}
|
|
619
|
+
// labels on stars
|
|
620
|
+
label(ctx, 'dog', DOG[0] + 38, DOG[1] + 14, { italic: true, size: 42, color: CREAM, a: sa * (1 - sm(X.guess[1] - 0.1, X.guess[1], t)) });
|
|
621
|
+
label(ctx, 'cat', CAT[0] + 38, CAT[1] + 14, { italic: true, size: 42, color: CREAM, a: sa * (1 - sm(X.guess2[1] - 0.1, X.guess2[1], t)) });
|
|
622
|
+
// GUESS: a beam from the model to the "dog" star
|
|
623
|
+
const o0 = [PB.x + PB.R * 0.92, PB.y - 40];
|
|
624
|
+
const g1 = E.inOut(sm(X.guess[0], X.guess[1], t)), wrong = sm(X.check, X.check + 0.12, t);
|
|
625
|
+
const bc = H.mix(CYAN, RED, wrong);
|
|
626
|
+
const fade1 = 1 - 0.55 * sm(X.guess2[0], X.guess2[0] + 0.3, t);
|
|
627
|
+
if (g1 > 0) {
|
|
628
|
+
const tip = chartLine(ctx, o0[0], o0[1], DOG[0] - 22, DOG[1] + 6, { prog: g1, c: bc, a: 0.95 * fade1, lw: 1.6, glowA: 0.18 });
|
|
629
|
+
if (g1 < 1) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.drawImage(glow(CYAN), tip[0] - 20, tip[1] - 20, 40, 40); ctx.restore(); }
|
|
630
|
+
label(ctx, 'Guess', 600, 404, { size: 36, color: H.mix(CYAN, CREAM, 0.3), a: sm(X.guess[0] + 0.15, X.guess[0] + 0.45, t) * fade1 });
|
|
631
|
+
const dq = sm(X.guess[1] - 0.05, X.guess[1] + 0.15, t);
|
|
632
|
+
label(ctx, 'dog?', DOG[0] + 38, DOG[1] + 14, { italic: true, size: 44, color: H.mix(CREAM, RED, wrong), a: dq });
|
|
633
|
+
}
|
|
634
|
+
// CHECK: red ring marker + cross, line turns red
|
|
635
|
+
if (t > X.check) {
|
|
636
|
+
const k = sm(X.check, X.check + 0.25, t), pulse = 1 + 0.5 * Math.exp(-(t - X.check) * 5);
|
|
637
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = GA * 0.6 * Math.exp(-(t - X.check) * 3); ctx.drawImage(glow(RED), DOG[0] - 90, DOG[1] - 90, 180, 180); ctx.restore();
|
|
638
|
+
marker(ctx, DOG[0], DOG[1], 28 * pulse, RED, k, 1.8);
|
|
639
|
+
ctx.save(); ctx.strokeStyle = rgba(RED, k); ctx.lineWidth = 2.4; ctx.lineCap = 'round'; const cx = DOG[0] + 160, cy = DOG[1] - 2, s = 13 * k;
|
|
640
|
+
ctx.beginPath(); ctx.moveTo(cx - s, cy - s); ctx.lineTo(cx + s, cy + s); ctx.moveTo(cx + s, cy - s); ctx.lineTo(cx - s, cy + s); ctx.stroke(); ctx.restore();
|
|
641
|
+
label(ctx, 'Check', DOG[0] + 40, DOG[1] - 48, { size: 36, color: RED, a: sm(X.check + 0.05, X.check + 0.3, t) });
|
|
642
|
+
label(ctx, 'it’s a cat', EXP[0] + 78, EXP[1] + 14, { italic: true, size: 32, color: GOLDB, a: sm(X.truth, X.truth + 0.25, t) });
|
|
643
|
+
}
|
|
644
|
+
// error signal travels back to the model along a curved return path
|
|
645
|
+
const bk = sm(X.back[0], X.back[1], t);
|
|
646
|
+
if (bk > 0) {
|
|
647
|
+
const tip = chartLine(ctx, DOG[0] - 10, DOG[1] - 30, PB.x + 20, PB.y - PB.R - 14, { prog: E.inOut(bk), q: [700, 70], c: RED, a: 0.8 * fade1, dotted: true, lw: 1.8 });
|
|
648
|
+
if (bk < 1) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.drawImage(glow(RED), tip[0] - 26, tip[1] - 26, 52, 52); ctx.restore(); }
|
|
649
|
+
label(ctx, 'error', 676, 146, { italic: true, size: 34, color: RED, align: 'center', a: sm(X.back[0] + 0.2, X.back[0] + 0.45, t) * fade1 });
|
|
650
|
+
}
|
|
651
|
+
// ADJUST: the weights' orbit swings to a new tilt; curved arrow shows the turn
|
|
652
|
+
const al = sm(X.adjust[0], X.adjust[0] + 0.3, t);
|
|
653
|
+
const ae = ellPt(PB.x, PB.y, 236, 76, rot, 0.0);
|
|
654
|
+
if (al > 0) {
|
|
655
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLDB, al); ctx.lineWidth = 1.6; ctx.beginPath();
|
|
656
|
+
ctx.arc(PB.x, PB.y, 262, rot0 - 0.02, rot0 + (rot - rot0) - 0.02, true); ctx.stroke(); ctx.restore();
|
|
657
|
+
const aa = rot - 0.02; chevron(ctx, PB.x + Math.cos(aa) * 262, PB.y + Math.sin(aa) * 262, aa - Math.PI / 2, 9, GOLDB, al, 1.8);
|
|
658
|
+
label(ctx, 'Adjust', 400, 636, { size: 38, a: al, color: GOLDB });
|
|
659
|
+
label(ctx, 'tilt the weights', 402, 674, { italic: true, size: 30, color: CREAM, a: sm(X.adjust[0] + 0.2, X.adjust[0] + 0.5, t) });
|
|
660
|
+
}
|
|
661
|
+
label(ctx, 'Weights', ae[0] + 30, ae[1] + 12, { size: 32, color: CYAN, a: sm(6.9, 7.2, t) * (1 - al) });
|
|
662
|
+
// GUESS AGAIN: beam re-aimed to the "cat" star
|
|
663
|
+
const g2 = E.inOut(sm(X.guess2[0], X.guess2[1], t));
|
|
664
|
+
if (g2 > 0) {
|
|
665
|
+
const o1 = [PB.x + PB.R * 0.9, PB.y + 10];
|
|
666
|
+
const tip = chartLine(ctx, o1[0], o1[1], CAT[0] - 22, CAT[1] - 4, { prog: g2, c: GOLDB, a: 0.95, lw: 1.8, glowA: 0.22 });
|
|
667
|
+
if (g2 < 1) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.drawImage(glow(GOLDB), tip[0] - 22, tip[1] - 22, 44, 44); ctx.restore(); }
|
|
668
|
+
const ok = sm(X.guess2[1], X.guess2[1] + 0.2, t);
|
|
669
|
+
if (ok > 0) {
|
|
670
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = GA * 0.7 * Math.exp(-(t - X.guess2[1]) * 2.5); ctx.drawImage(glow(GOLDB), CAT[0] - 110, CAT[1] - 110, 220, 220); ctx.restore();
|
|
671
|
+
marker(ctx, CAT[0], CAT[1], 28 * (1 + 0.4 * Math.exp(-(t - X.guess2[1]) * 5)), GOLDB, ok, 1.8);
|
|
672
|
+
label(ctx, 'cat', CAT[0] + 40, CAT[1] + 14, { italic: true, size: 44, color: GOLDB, a: ok });
|
|
673
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLDB, ok); ctx.lineWidth = 2.6; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const cx = CAT[0] + 152, cy = CAT[1] - 2;
|
|
674
|
+
ctx.beginPath(); ctx.moveTo(cx - 12, cy); ctx.lineTo(cx - 3, cy + 10 * ok); ctx.lineTo(cx - 3 + 18 * ok, cy - 14 * ok); ctx.stroke(); ctx.restore();
|
|
675
|
+
label(ctx, 'Guess again', 960, 640, { size: 34, color: GOLDB, a: ok, align: 'right' });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function sceneC(ctx, t) { const GA = ctx.globalAlpha;
|
|
681
|
+
const E = H.ease, x0 = 112, x1 = 742, yT = 150, yB = 600;
|
|
682
|
+
const ya = (v) => yB - (v - 50) / 50 * (yB - yT);
|
|
683
|
+
// atlas grid: curved meridians and parallels (conic projection feel)
|
|
684
|
+
const ga = sm(10.5, 11.1, t);
|
|
685
|
+
ctx.save(); ctx.strokeStyle = rgba(CYAN, 0.16 * ga); ctx.lineWidth = 1;
|
|
686
|
+
for (let k = 0; k <= 6; k++) { const x = x0 + k * (x1 - x0) / 6; ctx.beginPath(); ctx.moveTo(x, yB); ctx.quadraticCurveTo(x + (x - 427) * 0.06, (yT + yB) / 2, x + (x - 427) * 0.1, yT); ctx.stroke(); }
|
|
687
|
+
for (let v = 50; v <= 100; v += 10) { const y = ya(v); ctx.beginPath(); ctx.moveTo(x0, y); ctx.quadraticCurveTo(427, y - 22, x1 + 20, y); ctx.stroke(); }
|
|
688
|
+
ctx.restore();
|
|
689
|
+
// graduated axes
|
|
690
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.85 * ga); ctx.lineWidth = 1.1; ctx.beginPath(); ctx.moveTo(x0, yT - 10); ctx.lineTo(x0, yB); ctx.lineTo(x1 + 30, yB);
|
|
691
|
+
for (let v = 50; v <= 100; v++) { const y = ya(v), l = v % 10 === 0 ? 14 : v % 5 === 0 ? 8 : 4; ctx.moveTo(x0, y); ctx.lineTo(x0 - l, y); }
|
|
692
|
+
for (let k = 0; k <= 60; k++) { const x = x0 + k * (x1 - x0) / 60; ctx.moveTo(x, yB); ctx.lineTo(x, yB + (k % 10 === 0 ? 12 : 4)); }
|
|
693
|
+
ctx.stroke(); ctx.restore();
|
|
694
|
+
for (let v = 50; v <= 100; v += 10) label(ctx, v + '%', x0 - 20, ya(v) + 7, { size: 24, align: 'right', a: ga * 0.9, track: 0.02, color: GOLD });
|
|
695
|
+
label(ctx, 'Training rounds', x0 + 4, yB + 50, { size: 34, a: ga });
|
|
696
|
+
chevron(ctx, x1 + 30, yB, 0, 9, GOLD, ga);
|
|
697
|
+
// the constellation forms, one star per round
|
|
698
|
+
const pts = ACC.map((v, i) => [x0 + 34 + i * (x1 - x0 - 60) / 12, ya(v)]);
|
|
699
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLDB, 0.8); ctx.lineWidth = 1.3; ctx.lineCap = 'round';
|
|
700
|
+
for (let i = 1; i < pts.length; i++) {
|
|
701
|
+
const f = sm(X.pts0 + i * X.ptsDt - 0.1, X.pts0 + i * X.ptsDt, t); if (f <= 0) continue;
|
|
702
|
+
const [ax, ay] = pts[i - 1], [bx, by] = pts[i], L = Math.hypot(bx - ax, by - ay), dx = (bx - ax) / L, dy = (by - ay) / L, g = 7;
|
|
703
|
+
ctx.beginPath(); ctx.moveTo(ax + dx * g, ay + dy * g); ctx.lineTo(ax + dx * (g + (L - 2 * g) * f), ay + dy * (g + (L - 2 * g) * f)); ctx.stroke();
|
|
704
|
+
}
|
|
705
|
+
ctx.restore();
|
|
706
|
+
pts.forEach((p, i) => { const a = sm(X.pts0 + i * X.ptsDt - 0.03, X.pts0 + i * X.ptsDt + 0.05, t); chartStar(ctx, p[0], p[1], 0.2 + 0.6 * i / 12, i === 0 ? CYAN : GOLDB, a); });
|
|
707
|
+
// dotted best-fit arc behind the stars
|
|
708
|
+
const fitA = sm(12.0, 12.5, t);
|
|
709
|
+
if (fitA > 0) { ctx.save(); ctx.setLineDash([1.5, 6]); ctx.strokeStyle = rgba(CYAN, 0.5 * fitA); ctx.lineWidth = 1.2; ctx.beginPath(); pts.forEach((p, i) => { const y = ya(61 + 36 * (1 - Math.exp(-i / 3.0)) / (1 - Math.exp(-12 / 3.0))); i ? ctx.lineTo(p[0], y) : ctx.moveTo(p[0], y); }); ctx.stroke(); ctx.restore(); }
|
|
710
|
+
// first point marker
|
|
711
|
+
marker(ctx, pts[0][0], pts[0][1], 12, CYAN, sm(X.pts0, X.pts0 + 0.2, t));
|
|
712
|
+
label(ctx, '61%', pts[0][0] + 22, pts[0][1] + 40, { size: 32, color: CYAN, a: sm(X.pts0, X.pts0 + 0.2, t), track: 0.04 });
|
|
713
|
+
// stellar flare at 97%
|
|
714
|
+
const hit = t - X.hit, last = pts[12];
|
|
715
|
+
if (hit > -0.05) {
|
|
716
|
+
const fl = hit < 0 ? 0 : Math.exp(-hit * 1.6) * 0.8 + 0.35;
|
|
717
|
+
spikeStar(ctx, last[0], last[1], 9 + 16 * Math.exp(-hit * 2.2) * sm(-0.05, 0.05, hit), H.clamp(fl), [255, 228, 180], 0.0);
|
|
718
|
+
marker(ctx, last[0], last[1], 22 + 30 * (1 - Math.exp(-Math.max(0, hit) * 4)), GOLDB, sm(0, 0.15, hit) * (0.5 + 0.5 * Math.exp(-hit * 2)), 1.6);
|
|
719
|
+
if (hit > 0) { ctx.save(); ctx.strokeStyle = rgba(GOLDB, 0.6 * Math.exp(-hit * 2.5)); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(last[0], last[1], 30 + hit * 260, 0, TAU); ctx.stroke(); ctx.restore(); }
|
|
720
|
+
}
|
|
721
|
+
// the number
|
|
722
|
+
const fi = H.clamp((t - X.pts0) / X.ptsDt, 0, 12), i0 = Math.floor(fi), v = i0 >= 12 ? 97 : ACC[i0] + (ACC[i0 + 1] - ACC[i0]) * (fi - i0);
|
|
723
|
+
const na = sm(10.95, 11.25, t);
|
|
724
|
+
label(ctx, 'Accuracy', 826, 262, { size: 40, a: na });
|
|
725
|
+
ctx.save(); ctx.globalAlpha = GA * na; ctx.font = `800 116px ${SYNE}`; ctx.shadowColor = 'rgba(2,3,14,0.9)'; ctx.shadowBlur = 20;
|
|
726
|
+
const fl2 = hit > 0 ? Math.exp(-hit * 2) : 0;
|
|
727
|
+
ctx.fillStyle = rgba(H.mix(CREAM, GOLDB, sm(-0.1, 0.1, hit) * 0.85 + 0), 1); ctx.fillText(Math.round(v) + '%', 822, 404);
|
|
728
|
+
if (fl2 > 0) { ctx.globalCompositeOperation = 'lighter'; ctx.shadowBlur = 40; ctx.shadowColor = rgba(GOLDB, fl2); ctx.fillStyle = rgba(GOLDB, 0.35 * fl2); ctx.fillText(Math.round(v) + '%', 822, 404); }
|
|
729
|
+
ctx.restore();
|
|
730
|
+
const ra = sm(X.hit, X.hit + 0.3, t);
|
|
731
|
+
label(ctx, 'from 61%', 830, 474, { italic: true, size: 34, color: CYAN, a: Math.max(sm(11.3, 11.6, t) * 0.8, ra) });
|
|
732
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.7 * na); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(830, 290); ctx.lineTo(1190, 290); for (let x = 0; x <= 360; x += 12) { ctx.moveTo(830 + x, 290); ctx.lineTo(830 + x, 290 + (x % 60 === 0 ? 8 : 4)); } ctx.stroke(); ctx.restore();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function takeaway(ctx, W, t) {
|
|
736
|
+
const [a, b, c] = X.take, E = H.ease;
|
|
737
|
+
const ra = sm(a, a + 0.3, t);
|
|
738
|
+
if (ra <= 0) return;
|
|
739
|
+
const cx = W / 2, y1 = 300, y2 = 520;
|
|
740
|
+
label(ctx, 'The takeaway', cx, 190, { size: 32, align: 'center', a: ra, track: 0.3 });
|
|
741
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.8 * ra); ctx.lineWidth = 1; const hw = 150 * E.out(ra);
|
|
742
|
+
ctx.beginPath(); ctx.moveTo(cx - 140 - hw, 178); ctx.lineTo(cx - 140, 178); ctx.moveTo(cx + 140, 178); ctx.lineTo(cx + 140 + hw, 178); ctx.stroke(); ctx.restore();
|
|
743
|
+
ctx.save(); ctx.font = `800 64px ${SYNE}`; ctx.textAlign = 'center';
|
|
744
|
+
ctx.shadowColor = 'rgba(2,3,14,0.95)'; ctx.shadowBlur = 20;
|
|
745
|
+
ctx.globalAlpha = ra; ctx.fillStyle = rgba(CREAM, 1); ctx.fillText('More examples', cx, y1 - 14 * (1 - E.out(sm(a, a + 0.35, t))));
|
|
746
|
+
ctx.globalAlpha = sm(c, c + 0.3, t); ctx.fillStyle = rgba(GOLDB, 1); ctx.fillText('better guesses', cx, y2 - 14 * (1 - E.out(sm(c, c + 0.35, t))));
|
|
747
|
+
const w2 = ctx.measureText('better guesses').width;
|
|
748
|
+
ctx.restore();
|
|
749
|
+
// the arrow is a dotted orbit arc with an arrowhead, swinging down from line one to line two
|
|
750
|
+
const ap = E.inOut(sm(b, c, t));
|
|
751
|
+
chartLine(ctx, cx, y1 + 34, cx, y2 - 70, { prog: ap, q: [cx + 90, (y1 + y2) / 2 - 20], c: GOLDB, a: 1, lw: 2, dotted: true });
|
|
752
|
+
marker(ctx, cx, y1 + 34, 5, GOLD, sm(b - 0.1, b, t));
|
|
753
|
+
// degree scale underline
|
|
754
|
+
ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.75 * sm(c, c + 0.4, t)); ctx.lineWidth = 1; const ul = w2 * E.out(sm(c, c + 0.5, t)), x0 = cx - w2 / 2;
|
|
755
|
+
ctx.beginPath(); ctx.moveTo(x0, y2 + 26); ctx.lineTo(x0 + ul, y2 + 26); for (let x = 0; x <= ul; x += 9) { ctx.moveTo(x0 + x, y2 + 26); ctx.lineTo(x0 + x, y2 + 26 + (x % 45 === 0 ? 9 : 4)); } ctx.stroke(); ctx.restore();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function explainer(ctx, W, Hh, t) {
|
|
759
|
+
const E = H.ease;
|
|
760
|
+
const [w0, wm, w1] = X.warp;
|
|
761
|
+
const warpIn = sm(w0, wm, t), warpOut = sm(wm, w1, t);
|
|
762
|
+
const warp = t < wm ? E.in(warpIn) : 1 - E.out(warpOut);
|
|
763
|
+
const pan = E.inOut(sm(X.pan[0], X.pan[1], t));
|
|
764
|
+
const zA = H.lerp(0.62, 1, E.inOut(sm(X.push[0], X.push[1], t))) + 0.05 * sm(X.push[1], w0, t);
|
|
765
|
+
const camZ = t < wm ? zA + 0.6 * warpIn : 1 + 0.6 * (1 - E.out(warpOut)) + 0.03 * sm(w1, X.pan[0], t);
|
|
766
|
+
const tk = sm(X.take[0] - 0.1, X.take[0] + 0.45, t);
|
|
767
|
+
sky(ctx, W, Hh, t, { x: t * 8, y: -720 * pan, z: camZ - 0.08 * E.inOut(tk), warp, bright: 0.35 + 0.65 * sm(0, 0.8, t), nx: 60, ny: 30, gal: [1440, 350, 1.0] });
|
|
768
|
+
|
|
769
|
+
// ---- scene A: the model planet + data comets
|
|
770
|
+
if (t < wm + 0.05) {
|
|
771
|
+
ctx.save();
|
|
772
|
+
ctx.globalAlpha = 1 - warpIn;
|
|
773
|
+
ctx.translate(W / 2, Hh / 2); ctx.scale(zA, zA); ctx.translate(-W / 2, -Hh / 2);
|
|
774
|
+
const ws = 1 + 3.2 * E.in(warpIn); ctx.translate(PA.x, PA.y); ctx.scale(ws, ws); ctx.translate(-PA.x, -PA.y);
|
|
775
|
+
sceneA(ctx, t);
|
|
776
|
+
ctx.restore();
|
|
777
|
+
ctx.save(); ctx.globalAlpha = 1 - sm(w0, w0 + 0.25, t); counterA(ctx, t); ctx.restore();
|
|
778
|
+
}
|
|
779
|
+
// ---- scene B: guess / check / adjust
|
|
780
|
+
if (t > wm - 0.05 && t < X.pan[1] + 0.05) {
|
|
781
|
+
ctx.save();
|
|
782
|
+
ctx.globalAlpha = sm(wm - 0.05, wm + 0.3, t);
|
|
783
|
+
ctx.translate(0, 720 * pan);
|
|
784
|
+
const sb = 0.55 + 0.45 * E.out(sm(wm - 0.05, w1 + 0.05, t));
|
|
785
|
+
ctx.translate(W / 2, Hh / 2); ctx.scale(sb, sb); ctx.translate(-W / 2, -Hh / 2);
|
|
786
|
+
const punch = 0.05 * E.inOut(sm(X.check - 0.05, X.check + 0.3, t)) * (1 - E.inOut(sm(X.back[0], X.adjust[0], t)));
|
|
787
|
+
ctx.translate(DOG[0], DOG[1]); ctx.scale(1 + punch, 1 + punch); ctx.translate(-DOG[0], -DOG[1]);
|
|
788
|
+
sceneB(ctx, t);
|
|
789
|
+
ctx.restore();
|
|
790
|
+
}
|
|
791
|
+
// ---- scene C: the accuracy constellation (above scene B; camera tilts up), then the takeaway
|
|
792
|
+
const rec = E.inOut(tk);
|
|
793
|
+
if (t > X.pan[0] - 0.05) {
|
|
794
|
+
if (tk < 1) {
|
|
795
|
+
ctx.save();
|
|
796
|
+
ctx.translate(0, -720 * (1 - pan));
|
|
797
|
+
ctx.globalAlpha = 1 - rec;
|
|
798
|
+
ctx.translate(W / 2, 330); ctx.scale(1 - 0.1 * rec, 1 - 0.1 * rec); ctx.translate(-W / 2, -330 - 50 * rec);
|
|
799
|
+
sceneC(ctx, t);
|
|
800
|
+
ctx.restore();
|
|
801
|
+
}
|
|
802
|
+
if (tk > 0) { ctx.save(); const g = ctx.createRadialGradient(W / 2, 360, 40, W / 2, 360, 520); g.addColorStop(0, rgba([3, 4, 12], 0.6 * rec)); g.addColorStop(1, rgba([3, 4, 12], 0)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, Hh); ctx.restore(); }
|
|
803
|
+
takeaway(ctx, W, t);
|
|
804
|
+
}
|
|
805
|
+
// ---- header: hook title shrinks into the corner
|
|
806
|
+
{
|
|
807
|
+
const sh = E.inOut(sm(X.shrink[0], X.shrink[1], t));
|
|
808
|
+
ctx.save(); ctx.font = `800 70px ${SYNE}`; const s = 'How does AI learn?', tw = ctx.measureText(s).width;
|
|
809
|
+
const bx = W / 2 - tw / 2, by = 250, sx = 40, sy = 62, sc = 1 + (36 / 70 - 1) * sh;
|
|
810
|
+
const x = bx + (sx - bx) * sh, y = by + (sy - by) * sh;
|
|
811
|
+
ctx.translate(x, y); ctx.scale(sc, sc);
|
|
812
|
+
const n = s.length, rev = sm(X.hookIn[0], X.hookIn[1], t) * n;
|
|
813
|
+
ctx.shadowColor = 'rgba(2,3,14,0.95)'; ctx.shadowBlur = 20; ctx.fillStyle = rgba(CREAM, 1);
|
|
814
|
+
let cx = 0;
|
|
815
|
+
for (let i = 0; i < n; i++) { const ch = s[i], cw = ctx.measureText(ch).width, a = H.clamp(rev - i); if (a > 0) { ctx.globalAlpha = a; ctx.fillText(ch, cx, -10 * (1 - a)); } cx += cw; }
|
|
816
|
+
ctx.globalAlpha = 1; ctx.shadowBlur = 0;
|
|
817
|
+
// fine graduated rule + subtitle under the big title (fades as it shrinks)
|
|
818
|
+
const rp = E.out(sm(X.ruleIn[0], X.ruleIn[1], t)), ra = 1 - sm(X.shrink[0], X.shrink[0] + 0.4, t);
|
|
819
|
+
ctx.strokeStyle = rgba(GOLD, 0.85 * ra); ctx.lineWidth = 1.2 / sc; ctx.beginPath(); ctx.moveTo(0, 28); ctx.lineTo(tw * rp, 28);
|
|
820
|
+
for (let k = 0; k <= tw * rp; k += 12) { ctx.moveTo(k, 28); ctx.lineTo(k, 28 + (k % 60 === 0 ? 12 : 5)); } ctx.stroke();
|
|
821
|
+
ctx.restore();
|
|
822
|
+
label(ctx, 'a star chart of machine learning', W / 2, by - 84, { italic: true, size: 30, color: GOLD, align: 'center', a: sm(0.8, 1.2, t) * (1 - sm(X.shrink[0], X.shrink[0] + 0.3, t)) });
|
|
823
|
+
if (sh > 0.9) { ctx.save(); ctx.strokeStyle = rgba(GOLD, 0.6 * sm(0.9, 1, sh)); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(40, 78); ctx.lineTo(40 + tw * 36 / 70, 78); ctx.stroke(); ctx.restore(); }
|
|
824
|
+
}
|
|
825
|
+
// ---- warp flash
|
|
826
|
+
const fl = Math.exp(-Math.pow((t - wm) / 0.09, 2));
|
|
827
|
+
if (fl > 0.01) { ctx.save(); ctx.globalCompositeOperation = 'lighter'; const g = ctx.createRadialGradient(W / 2, Hh / 2, 0, W / 2, Hh / 2, 700); g.addColorStop(0, rgba([210, 235, 255], 0.75 * fl)); g.addColorStop(0.5, rgba([120, 90, 220], 0.3 * fl)); g.addColorStop(1, rgba([40, 20, 80], 0)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, Hh); ctx.restore(); }
|
|
828
|
+
H.vignette(ctx, W, Hh, 0.55, '1,2,8');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ================================================================= SOUND
|
|
832
|
+
function explainerSound(k) {
|
|
833
|
+
const D = X.D, E = H.ease;
|
|
834
|
+
// deep space pad: detuned low sines/triangles with slow swells, lowpassed
|
|
835
|
+
const pad = (t, notes, dur, g = 1, o = {}) => notes.forEach((n, i) => {
|
|
836
|
+
for (const det of [-6, 6]) k.tone(t, { f: n, type: i ? 'triangle' : 'sine', detune: det, dur, gain: 0.05 * g, a: o.a || dur * 0.4, hold: dur * 0.3, filter: 'lowpass', f2: o.cut || 900, q: 0.5, pan: (i - (notes.length - 1) / 2) * 0.35, verb: 0.5, minor: o.minor });
|
|
837
|
+
});
|
|
838
|
+
// soft low radar ping (sine + quiet fifth, long reverb tail, all under 1.5 kHz)
|
|
839
|
+
const ping = (t, f = 880, g = 1, pan = 0, minor = true) => {
|
|
840
|
+
k.tone(t, { f, dur: 0.9, gain: 0.12 * g, a: 0.004, verb: 0.6, pan, minor });
|
|
841
|
+
k.tone(t, { f: k.hz(f) * 1.5, dur: 0.35, gain: 0.03 * g, a: 0.004, verb: 0.5, pan, minor });
|
|
842
|
+
};
|
|
843
|
+
// airy comet whoosh: pink noise band sweeping low -> mid -> low
|
|
844
|
+
const airy = (t, dur, g = 1, pan = 0, top = 1300) => k.noise(t, { dur, color: 'pink', filter: 'bandpass', f: [[0, 220], [dur * 0.6, top], [dur, 380]], q: 0.7, gain: 0.13 * g, a: dur * 0.6, verb: 0.35, pan, minor: true });
|
|
845
|
+
// throttled counter ticks: soft low tock under 1.5 kHz
|
|
846
|
+
const counter = (val, a, b, gap, f, g = 1) => { let last = -1, prev = val(a); for (let t = a; t <= b; t += 0.002) { const v = val(t); if (v !== prev && t - last >= gap) { k.noise(t, { dur: 0.02, filter: 'bandpass', f, q: 4, gain: 0.22 * g, a: 0.001, pan: -0.35, minor: true }); k.tone(t, { f: f * 0.5, dur: 0.05, gain: 0.04 * g, pan: -0.35, minor: true }); last = t; } prev = v; } };
|
|
847
|
+
|
|
848
|
+
// ---- bed: sub drone + slow evolving pad (dropped in voiceover mode via music/bed... pads are tones, mark minor)
|
|
849
|
+
k.bed('room', 0, D, { gain: 0.5 });
|
|
850
|
+
k.withMinor(() => {
|
|
851
|
+
k.tone(0, { f: 'A1', dur: X.warp[0] + 0.3, gain: 0.07, a: 1.5, hold: X.warp[0] - 1.8, filter: 'lowpass', f2: 300, bus: 'amb' });
|
|
852
|
+
pad(0.0, ['A2', 'E3', 'B3'], 3.2, 0.9, { a: 1.6 });
|
|
853
|
+
pad(2.8, ['F2', 'C3', 'A3', 'E4'], 3.4, 0.8, { a: 1.2 });
|
|
854
|
+
k.tone(X.warp[2], { f: 'D2', dur: X.pan[0] - X.warp[2] + 0.5, gain: 0.06, a: 0.8, hold: 2.6, filter: 'lowpass', f2: 300, bus: 'amb' });
|
|
855
|
+
pad(6.7, ['D3', 'A3', 'E4'], 1.4, 0.65, { a: 0.6 });
|
|
856
|
+
k.tone(X.pan[0], { f: 'C2', dur: D - X.pan[0], gain: 0.06, a: 1.0, hold: 2.5, filter: 'lowpass', f2: 300, bus: 'amb' });
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// ---- hook: title letters reveal; graduated rule draws; title slides to the corner
|
|
860
|
+
k.noise(0.1, { dur: 1.1, color: 'pink', filter: 'bandpass', f: [[0, 200], [1.1, 700]], q: 0.8, gain: 0.08, a: 0.8, verb: 0.4 });
|
|
861
|
+
ping(0.95, 'A4', 0.9, 0);
|
|
862
|
+
k.noise(X.shrink[0], { dur: 0.8, color: 'pink', filter: 'lowpass', f: [[0, 260], [0.45, 700], [0.8, 260]], gain: 0.12, a: 0.35, minor: true });
|
|
863
|
+
|
|
864
|
+
// ---- degree ring + orbits draw on (0.6-2.5): faint scanning sweep
|
|
865
|
+
k.noise(0.6, { dur: 1.8, color: 'pink', filter: 'bandpass', f: [[0, 400], [1.8, 900]], q: 3, gain: 0.04, a: 0.9, minor: true });
|
|
866
|
+
|
|
867
|
+
// ---- labels land: radar pings
|
|
868
|
+
ping(X.model, 'E5', 1.7, 0.5, false);
|
|
869
|
+
ping(X.weights, 'B4', 0.9, 0.4);
|
|
870
|
+
|
|
871
|
+
// ---- comets stream in: airy whoosh on approach, soft ping on arrival at the planet
|
|
872
|
+
X.comets.forEach((c, i) => {
|
|
873
|
+
airy(c.t0 + 0.1, c.dur * 0.9, 0.9, -0.6 + 0.1 * (i % 4));
|
|
874
|
+
ping(c.t0 + c.dur, ['A4', 'C5', 'E5', 'A4', 'G4', 'E5', 'C5', 'A5'][i], 0.55, 0.35);
|
|
875
|
+
});
|
|
876
|
+
// examples counter 10^(6p): ticks, throttled, then a lock ping
|
|
877
|
+
counter((t) => Math.round(Math.pow(10, 6 * E.inOut(sm(X.count[0], X.count[1], t)))), X.count[0], X.count[1] - 0.02, 0.1, 1100, 0.8);
|
|
878
|
+
ping(X.count[1], 'A4', 1.1, -0.4, false);
|
|
879
|
+
|
|
880
|
+
// ---- warp: deep sub rumble + rising airy streak, a soft bloom at the midpoint
|
|
881
|
+
const [w0, wm, w1] = X.warp;
|
|
882
|
+
k.tone(w0 - 0.1, { f: [[0, 55], [wm - w0, 34], [w1 - w0 + 0.4, 28]], dur: w1 - w0 + 0.6, gain: 0.42, a: wm - w0, hold: 0.05 });
|
|
883
|
+
k.noise(w0, { dur: w1 - w0 + 0.3, color: 'brown', filter: 'lowpass', f: [[0, 120], [wm - w0, 600], [w1 - w0 + 0.3, 90]], gain: 0.4, a: wm - w0 });
|
|
884
|
+
k.noise(w0, { dur: w1 - w0, color: 'pink', filter: 'bandpass', f: [[0, 300], [wm - w0, 1600], [w1 - w0, 300]], q: 0.8, gain: 0.12, a: wm - w0 - 0.05, verb: 0.4 });
|
|
885
|
+
k.tone(wm, { f: 'A2', dur: 1.2, gain: 0.08, a: 0.01, verb: 0.6 });
|
|
886
|
+
|
|
887
|
+
// ---- scene B
|
|
888
|
+
airy(X.exIn[0], X.exIn[1] - X.exIn[0] + 0.15, 1, -0.6); // new example comet arrives
|
|
889
|
+
ping(X.exIn[1], 'C5', 0.8, -0.6);
|
|
890
|
+
ping(X.stars, 'E4', 0.6, 0.5); ping(X.stars + 0.06, 'G4', 0.5, 0.6); // two target stars appear
|
|
891
|
+
// guess beam: a soft rising glide while the line draws
|
|
892
|
+
k.tone(X.guess[0], { f: [[0, 'A3'], [X.guess[1] - X.guess[0], 'E4']], type: 'sine', dur: X.guess[1] - X.guess[0] + 0.1, gain: 0.06, a: 0.2, filter: 'lowpass', f2: 1200, verb: 0.3, pan: 0.2, minor: true });
|
|
893
|
+
ping(X.guess[1], 'E5', 0.8, 0.5, false); // "dog?" lands
|
|
894
|
+
// CHECK: wrong. Low dissonant swell (minor second + tritone, detuned saws, dark lowpass)
|
|
895
|
+
['Bb2', 'B2', 'F3'].forEach((n, i) => k.tone(X.check, { f: n, type: 'sawtooth', detune: (i - 1) * 9, dur: 1.1, gain: 0.07, a: 0.05, hold: 0.4, filter: 'lowpass', f2: [[0, 300], [0.15, 900], [1.1, 300]], q: 1, pan: 0.5, verb: 0.4 }));
|
|
896
|
+
k.tone(X.check, { f: [[0, 90], [0.4, 60]], dur: 0.6, gain: 0.3, a: 0.01 });
|
|
897
|
+
// error signal travels back: a falling dark glide + red pulse
|
|
898
|
+
k.tone(X.back[0], { f: [[0, 'E4'], [X.back[1] - X.back[0], 'A2']], type: 'triangle', dur: X.back[1] - X.back[0] + 0.1, gain: 0.07, a: 0.15, filter: 'lowpass', f2: 900, pan: -0.2, verb: 0.3 });
|
|
899
|
+
ping(X.back[1], 'A3', 0.8, -0.5);
|
|
900
|
+
// ADJUST: the orbit swings; a slow low whoosh and a settling ping
|
|
901
|
+
k.noise(X.adjust[0], { dur: X.adjust[1] - X.adjust[0] + 0.2, color: 'pink', filter: 'bandpass', f: [[0, 250], [0.4, 800], [0.85, 300]], q: 1.2, gain: 0.12, a: 0.35, pan: -0.4, verb: 0.3 });
|
|
902
|
+
k.tone(X.adjust[0], { f: [[0, 'D3'], [0.65, 'A3']], type: 'triangle', dur: 0.75, gain: 0.05, a: 0.3, filter: 'lowpass', f2: 900, verb: 0.3, pan: -0.4, minor: true });
|
|
903
|
+
ping(X.adjust[1], 'D5', 0.7, -0.4);
|
|
904
|
+
// guess again -> "cat" correct: warm consonant swell
|
|
905
|
+
k.tone(X.guess2[0], { f: [[0, 'A3'], [X.guess2[1] - X.guess2[0], 'E4']], dur: X.guess2[1] - X.guess2[0] + 0.1, gain: 0.06, a: 0.2, filter: 'lowpass', f2: 1200, verb: 0.3, pan: 0.2, minor: true });
|
|
906
|
+
pad(X.guess2[1], ['D3', 'A3', 'F#4', 'A4'], 1.5, 1.6, { a: 0.06, cut: 1400 });
|
|
907
|
+
ping(X.guess2[1], 'A5', 0.9, 0.5, false);
|
|
908
|
+
|
|
909
|
+
// ---- tilt-up pan to the accuracy chart: smooth low air
|
|
910
|
+
k.noise(X.pan[0], { dur: X.pan[1] - X.pan[0] + 0.2, color: 'pink', filter: 'bandpass', f: [[0, 200], [0.4, 700], [0.85, 220]], q: 0.8, gain: 0.14, a: 0.35, verb: 0.3 });
|
|
911
|
+
// constellation stars appear one per round: soft pings rising in pitch
|
|
912
|
+
const scale = ['A3', 'C4', 'D4', 'E4', 'G4', 'A4', 'C5', 'D5', 'E5', 'G5', 'A5', 'C6', 'D6'];
|
|
913
|
+
for (let i = 0; i < 13; i++) ping(X.pts0 + i * X.ptsDt, scale[i] === 'C6' ? 'A5' : scale[i] === 'D6' ? 'C6' : scale[i], 0.6, -0.3 + i * 0.05);
|
|
914
|
+
// big low bloom with shimmer at 97%
|
|
915
|
+
k.tone(X.hit, { f: 'C2', dur: 2.6, gain: 0.32, a: 0.03, hold: 0.4, verb: 0.4 });
|
|
916
|
+
k.tone(X.hit, { f: [[0, 70], [0.3, 45]], dur: 0.6, gain: 0.35, a: 0.005 });
|
|
917
|
+
pad(X.hit, ['C3', 'G3', 'E4', 'B4'], 2.2, 2.0, { a: 0.04, cut: 1600 });
|
|
918
|
+
k.noise(X.hit, { dur: 1.6, color: 'pink', filter: 'bandpass', f: [[0, 2200], [1.6, 900]], q: 2, gain: 0.07, a: 0.1, verb: 0.6 });
|
|
919
|
+
['E5', 'G5', 'B5', 'C6', 'E6'].forEach((n, i) => k.tone(X.hit + 0.08 + i * 0.07, { f: n, dur: 1.4, gain: 0.025, a: 0.05, verb: 0.6, pan: -0.5 + i * 0.25 }));
|
|
920
|
+
|
|
921
|
+
// ---- takeaway: recede, then a resolving warm pad as the line lands
|
|
922
|
+
k.noise(X.take[0] - 0.1, { dur: 0.7, color: 'pink', filter: 'lowpass', f: [[0, 240], [0.4, 600], [0.7, 240]], gain: 0.1, a: 0.3, minor: true });
|
|
923
|
+
k.tone(X.take[1], { f: [[0, 'C4'], [0.3, 'G4']], dur: 0.4, gain: 0.04, a: 0.15, filter: 'lowpass', f2: 1200, verb: 0.3, minor: true });
|
|
924
|
+
pad(X.take[2], ['F2', 'C3', 'A3', 'E4'], D - X.take[2], 1.4, { a: 0.25, cut: 1200 });
|
|
925
|
+
ping(X.take[2], 'C5', 1.0, 0, false); ping(X.take[2] + 0.12, 'G5', 0.6, 0.3, false);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
registerStyle({
|
|
929
|
+
id: 'cosmic',
|
|
930
|
+
name: 'Cosmic Star Chart',
|
|
931
|
+
category: 'Cosmic & Psychedelic',
|
|
932
|
+
description: 'Deep-space illustration crossed with an engraved celestial atlas: fbm nebulae in teal, violet, rose and gold, a lit ringed gas giant with cratered moons, comets and a diffraction-spiked star, overlaid with hairline gold and pale-cyan chart work (dotted orbits with arrowheads, degree rings, ring markers, small-caps serif labels).',
|
|
933
|
+
motion: 'The sky fades up star by star while the camera drifts in, the key star flares, the planet lights up, then the chart engraves itself over it: degree ring sweeping round, orbits dotting on, markers and labels landing, a comet streaking past.',
|
|
934
|
+
fonts: [`600 26px ${CORM}`, `italic 400 26px ${CORM}`, `800 64px ${SYNE}`, `italic 400 26px ${BASK}`, `400 26px ${BASK}`],
|
|
935
|
+
frameT: 5.8,
|
|
936
|
+
duration: 6,
|
|
937
|
+
readsPixels: false,
|
|
938
|
+
draw(ctx, W, Hh, t) {
|
|
939
|
+
const B = {
|
|
940
|
+
sky: sm(0, 1.0, t), cam: sm(0, 6, t), star: sm(0.4, 1.2, t), planet: sm(0.8, 1.8, t),
|
|
941
|
+
ring: H.ease.inOut(sm(1.5, 3.2, t)), orbits: H.ease.inOut(sm(1.8, 3.3, t)), labels: sm(2.8, 3.6, t),
|
|
942
|
+
comet: sm(2.0, 5.2, t), const: sm(3.0, 4.4, t), title: sm(3.3, 4.6, t),
|
|
943
|
+
};
|
|
944
|
+
stillScene(ctx, W, Hh, t, B);
|
|
945
|
+
},
|
|
946
|
+
explainerDuration: X.D,
|
|
947
|
+
explainerNote: 'Told as one celestial chart overlaid on deep space: the model is a ringed planet whose moons are its weights, cat/dog examples arrive as constellation-headed comets, a warp streak changes scene, the guess is a chart beam to a labelled star that turns red with a red ring marker, the weights’ orbit visibly tilts, the camera tilts up to an atlas chart where the accuracy constellation forms and flares at 97%; numbers are big Syne figures over graduated sextant scales.',
|
|
948
|
+
explainer,
|
|
949
|
+
explainerSound,
|
|
950
|
+
});
|
|
951
|
+
}
|