@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,895 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Vintage survey quadrangle: marching-squares contours, hillshade, water-lined lake, dashed trail, full collar.
|
|
3
|
+
const PAPER = '#EEE5CB', BROWN = '#A2643A', BLUE = '#3F7FA6', WATER = [168, 204, 214], WOOD = [200, 219, 168];
|
|
4
|
+
const MX0 = 36, MY0 = 30, MX1 = 1244, MY1 = 620; // map neatline
|
|
5
|
+
const G = 3; // grid spacing (px)
|
|
6
|
+
const CI = 40; // contour interval (m)
|
|
7
|
+
|
|
8
|
+
function makeTerrain() {
|
|
9
|
+
const n = H.makeNoise(31), n2 = H.makeNoise(77), n3 = H.makeNoise(5);
|
|
10
|
+
return function height(x, y) {
|
|
11
|
+
const wx = x + 80 * n.fbm2(x * 0.003, y * 0.003, 3);
|
|
12
|
+
const wy = y + 80 * n.fbm2(x * 0.003 + 5.2, y * 0.003 + 1.3, 3);
|
|
13
|
+
let h = 560 + 620 * n.fbm2(wx * 0.0024, wy * 0.0024, 5);
|
|
14
|
+
const rdg = 1 - Math.abs(n2.fbm2(wx * 0.0034, wy * 0.0034, 4));
|
|
15
|
+
h += 260 * (rdg - 0.6);
|
|
16
|
+
const g1 = Math.exp(-(((wx - 860) / 250) ** 2 + ((wy - 220) / 190) ** 2));
|
|
17
|
+
const g2 = Math.exp(-(((wx - 1120) / 150) ** 2 + ((wy - 450) / 140) ** 2));
|
|
18
|
+
h += 1300 * g1 * (0.45 + 0.75 * rdg) + 650 * g2 * (0.5 + 0.6 * rdg);
|
|
19
|
+
const b = Math.exp(-(((x - 330) / 230) ** 2 + ((y - 400) / 150) ** 2));
|
|
20
|
+
h -= 640 * b;
|
|
21
|
+
h += 18 * n3.fbm2(x * 0.03, y * 0.03, 2); // fine texture so contours crinkle
|
|
22
|
+
return h;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// marching squares over a grid; calls emit(level, x1,y1,x2,y2) for every crossing
|
|
27
|
+
function contours(v, gw, gh, ox, oy, levels, emit) {
|
|
28
|
+
const L0 = levels[0], step = levels[1] - levels[0], nL = levels.length;
|
|
29
|
+
for (let j = 0; j < gh - 1; j++) for (let i = 0; i < gw - 1; i++) {
|
|
30
|
+
const a = v[j * gw + i], b = v[j * gw + i + 1], c = v[(j + 1) * gw + i + 1], d = v[(j + 1) * gw + i];
|
|
31
|
+
const lo = Math.min(a, b, c, d), hi = Math.max(a, b, c, d);
|
|
32
|
+
let k0 = Math.max(0, Math.ceil((lo - L0) / step)), k1 = Math.min(nL - 1, Math.floor((hi - L0) / step));
|
|
33
|
+
const x = ox + i * G, y = oy + j * G;
|
|
34
|
+
for (let k = k0; k <= k1; k++) {
|
|
35
|
+
const L = levels[k];
|
|
36
|
+
const idx = (a > L ? 8 : 0) | (b > L ? 4 : 0) | (c > L ? 2 : 0) | (d > L ? 1 : 0);
|
|
37
|
+
if (idx === 0 || idx === 15) continue;
|
|
38
|
+
const T = () => [x + G * (L - a) / (b - a), y];
|
|
39
|
+
const R = () => [x + G, y + G * (L - b) / (c - b)];
|
|
40
|
+
const B = () => [x + G * (L - d) / (c - d), y + G];
|
|
41
|
+
const Lf = () => [x, y + G * (L - a) / (d - a)];
|
|
42
|
+
const seg = (p, q) => emit(k, p[0], p[1], q[0], q[1]);
|
|
43
|
+
switch (idx) {
|
|
44
|
+
case 1: case 14: seg(Lf(), B()); break;
|
|
45
|
+
case 2: case 13: seg(B(), R()); break;
|
|
46
|
+
case 3: case 12: seg(Lf(), R()); break;
|
|
47
|
+
case 4: case 11: seg(T(), R()); break;
|
|
48
|
+
case 6: case 9: seg(T(), B()); break;
|
|
49
|
+
case 7: case 8: seg(Lf(), T()); break;
|
|
50
|
+
case 5: seg(Lf(), T()); seg(B(), R()); break;
|
|
51
|
+
case 10: seg(T(), R()); seg(Lf(), B()); break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function spacedText(ctx, str, x, y, track, align = 'left') {
|
|
58
|
+
let w = 0; for (const ch of str) w += ctx.measureText(ch).width + track; w -= track;
|
|
59
|
+
let cx = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
|
|
60
|
+
const sa = ctx.textAlign; ctx.textAlign = 'left';
|
|
61
|
+
for (const ch of str) { ctx.fillText(ch, cx, y); cx += ctx.measureText(ch).width + track; }
|
|
62
|
+
ctx.textAlign = sa; return w;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Everything that does not depend on t is computed once and memoised (output stays a pure function of t).
|
|
66
|
+
let CACHE = null;
|
|
67
|
+
function buildStatic(W, H_) {
|
|
68
|
+
const height = makeTerrain();
|
|
69
|
+
const paperL = H.layer(W, H_);
|
|
70
|
+
H.paper(paperL.getContext('2d'), W, H_, PAPER, 21, 0.03);
|
|
71
|
+
|
|
72
|
+
// ---- heightfield ----
|
|
73
|
+
const gw = Math.ceil((MX1 - MX0) / G) + 2, gh = Math.ceil((MY1 - MY0) / G) + 2;
|
|
74
|
+
const hv = new Float32Array(gw * gh);
|
|
75
|
+
let hmax = -1e9, pk = [0, 0];
|
|
76
|
+
for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) {
|
|
77
|
+
const x = MX0 + i * G, y = MY0 + j * G, h = height(x, y);
|
|
78
|
+
hv[j * gw + i] = h;
|
|
79
|
+
if (h > hmax && x > MX0 + 60 && x < MX1 - 60 && y > MY0 + 60) { hmax = h; pk = [x, y]; }
|
|
80
|
+
}
|
|
81
|
+
const WL = height(330, 400) + 170; // lake level
|
|
82
|
+
const hAt = (x, y) => {
|
|
83
|
+
const fx = H.clamp((x - MX0) / G, 0, gw - 1.001), fy = H.clamp((y - MY0) / G, 0, gh - 1.001);
|
|
84
|
+
const i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j, k = j * gw + i;
|
|
85
|
+
return hv[k] * (1 - u) * (1 - v) + hv[k + 1] * u * (1 - v) + hv[k + gw] * (1 - u) * v + hv[k + gw + 1] * u * v;
|
|
86
|
+
};
|
|
87
|
+
const grad = (x, y) => [(hAt(x + 2, y) - hAt(x - 2, y)) / 4, (hAt(x, y + 2) - hAt(x, y - 2)) / 4];
|
|
88
|
+
|
|
89
|
+
// ---- plates at full res: water (blue), woodland (green), hillshade (grey) ----
|
|
90
|
+
const mw = MX1 - MX0, mh = MY1 - MY0;
|
|
91
|
+
const waterL = H.layer(mw, mh), woodL = H.layer(mw, mh), shadeL = H.layer(mw, mh);
|
|
92
|
+
const wi = waterL.getContext('2d').createImageData(mw, mh), oi = woodL.getContext('2d').createImageData(mw, mh), si = shadeL.getContext('2d').createImageData(mw, mh);
|
|
93
|
+
const sh = new Float32Array(gw * gh);
|
|
94
|
+
const Z = 0.16, lx = -0.55, ly = -0.6, lz = 0.58;
|
|
95
|
+
for (let j = 1; j < gh - 1; j++) for (let i = 1; i < gw - 1; i++) {
|
|
96
|
+
const k = j * gw + i;
|
|
97
|
+
const dx = (hv[k + 1] - hv[k - 1]) / (2 * G) * Z, dy = (hv[k + gw] - hv[k - gw]) / (2 * G) * Z;
|
|
98
|
+
sh[k] = (-dx * lx - dy * ly + lz) / Math.hypot(dx, dy, 1);
|
|
99
|
+
}
|
|
100
|
+
const woodN = H.makeNoise(909);
|
|
101
|
+
const wood = new Float32Array(gw * gh);
|
|
102
|
+
for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) wood[j * gw + i] = woodN.fbm2(i * G * 0.0065, j * G * 0.0065, 4);
|
|
103
|
+
for (let py = 0; py < mh; py++) {
|
|
104
|
+
const fy = py / G, j = Math.min(gh - 2, Math.floor(fy)), v = fy - j;
|
|
105
|
+
for (let px = 0; px < mw; px++) {
|
|
106
|
+
const fx = px / G, i = Math.min(gw - 2, Math.floor(fx)), u = fx - i, k = j * gw + i;
|
|
107
|
+
const w00 = (1 - u) * (1 - v), w10 = u * (1 - v), w01 = (1 - u) * v, w11 = u * v;
|
|
108
|
+
const h = hv[k] * w00 + hv[k + 1] * w10 + hv[k + gw] * w01 + hv[k + gw + 1] * w11;
|
|
109
|
+
const s = sh[k] * w00 + sh[k + 1] * w10 + sh[k + gw] * w01 + sh[k + gw + 1] * w11;
|
|
110
|
+
const wd = wood[k] * w00 + wood[k + 1] * w10 + wood[k + gw] * w01 + wood[k + gw + 1] * w11;
|
|
111
|
+
const o = (py * mw + px) * 4;
|
|
112
|
+
if (h < WL) {
|
|
113
|
+
wi.data[o] = WATER[0]; wi.data[o + 1] = WATER[1]; wi.data[o + 2] = WATER[2]; wi.data[o + 3] = 235;
|
|
114
|
+
} else {
|
|
115
|
+
if (wd > 0.04 && h < 1350) { oi.data[o] = WOOD[0]; oi.data[o + 1] = WOOD[1]; oi.data[o + 2] = WOOD[2]; oi.data[o + 3] = 175; }
|
|
116
|
+
const amt = H.clamp((lz - s) * 1.25, -0.25, 0.7);
|
|
117
|
+
if (amt > 0) { si.data[o] = 255 - 100 * amt; si.data[o + 1] = 255 - 115 * amt; si.data[o + 2] = 255 - 128 * amt; si.data[o + 3] = 255; }
|
|
118
|
+
else { si.data[o] = 255; si.data[o + 1] = 255; si.data[o + 2] = 255; si.data[o + 3] = 255; }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
waterL.getContext('2d').putImageData(wi, 0, 0);
|
|
123
|
+
woodL.getContext('2d').putImageData(oi, 0, 0);
|
|
124
|
+
shadeL.getContext('2d').putImageData(si, 0, 0);
|
|
125
|
+
|
|
126
|
+
// ---- contours ----
|
|
127
|
+
const levels = []; for (let L = Math.ceil(WL / CI) * CI; L < hmax + CI; L += CI) levels.push(L);
|
|
128
|
+
const segs = levels.map(() => []);
|
|
129
|
+
contours(hv, gw, gh, MX0, MY0, levels, (k, x1, y1, x2, y2) => segs[k].push(x1, y1, x2, y2));
|
|
130
|
+
|
|
131
|
+
// ---- trail: switchback descent from near the summit to the lake ----
|
|
132
|
+
const trail = [];
|
|
133
|
+
{
|
|
134
|
+
let x = pk[0] - 26, y = pk[1] + 14, dir = [-0.7, 0.7], side = 1, run = 0;
|
|
135
|
+
const tr = H.rng(17);
|
|
136
|
+
for (let s = 0; s < 1600; s++) {
|
|
137
|
+
const g = grad(x, y), gm = Math.hypot(g[0], g[1]) || 1e-6;
|
|
138
|
+
let dx = -g[0] / gm, dy = -g[1] / gm;
|
|
139
|
+
const steep = H.smooth(1.2, 4.5, gm);
|
|
140
|
+
const th = side * (1.15 * steep + 0.25);
|
|
141
|
+
let rx = dx * Math.cos(th) - dy * Math.sin(th), ry = dx * Math.sin(th) + dy * Math.cos(th);
|
|
142
|
+
const lxv = 330 - x, lyv = 400 - y, lm = Math.hypot(lxv, lyv) || 1;
|
|
143
|
+
rx += 0.45 * lxv / lm; ry += 0.45 * lyv / lm;
|
|
144
|
+
const rm = Math.hypot(rx, ry); rx /= rm; ry /= rm;
|
|
145
|
+
dir = [dir[0] * 0.8 + rx * 0.2, dir[1] * 0.8 + ry * 0.2];
|
|
146
|
+
const dm = Math.hypot(dir[0], dir[1]); dir = [dir[0] / dm, dir[1] / dm];
|
|
147
|
+
x += dir[0] * 2; y += dir[1] * 2; run += 2;
|
|
148
|
+
if (run > 46 + tr() * 40 * (1 - steep) + 30 * (1 - steep)) { side = -side; run = 0; }
|
|
149
|
+
trail.push(x, y);
|
|
150
|
+
if (hAt(x, y) < WL + 2) break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const tm = Math.floor(trail.length / 4) * 2;
|
|
154
|
+
const tlx = trail[tm], tly = trail[tm + 1];
|
|
155
|
+
|
|
156
|
+
// ---- label placement on index contours (uphill-reading numerals) ----
|
|
157
|
+
const isIndex = (L) => L % 200 === 0;
|
|
158
|
+
const rng = H.rng(4242);
|
|
159
|
+
const boxes = [];
|
|
160
|
+
const reserve = [[pk[0], pk[1] - 4, 210, 60], [330, 402, 230, 50], [tlx + 80, tly - 10, 160, 24]];
|
|
161
|
+
const freeOf = (x, y, rad) => boxes.every((b) => Math.hypot(b.x - x, b.y - y) > rad) && reserve.every(([rx, ry, rw, rh]) => Math.abs(x - rx) > rw / 2 + 12 || Math.abs(y - ry) > rh / 2 + 10);
|
|
162
|
+
const labels = [];
|
|
163
|
+
levels.forEach((L, k) => {
|
|
164
|
+
if (!isIndex(L)) return;
|
|
165
|
+
const s = segs[k]; const nSeg = s.length / 4; if (!nSeg) return;
|
|
166
|
+
let placed = 0;
|
|
167
|
+
for (let tries = 0; tries < 140 && placed < 3; tries++) {
|
|
168
|
+
const q = rng.int(0, nSeg - 1) * 4;
|
|
169
|
+
const x = (s[q] + s[q + 2]) / 2, y = (s[q + 1] + s[q + 3]) / 2;
|
|
170
|
+
if (x < MX0 + 40 || x > MX1 - 40 || y < MY0 + 30 || y > MY1 - 30) continue;
|
|
171
|
+
const g = grad(x, y), gm = Math.hypot(g[0], g[1]); if (gm < 0.6 || gm > 9) continue;
|
|
172
|
+
const tx = -g[1] / gm, ty = g[0] / gm;
|
|
173
|
+
const g2a = grad(x + tx * 14, y + ty * 14), g2b = grad(x - tx * 14, y - ty * 14);
|
|
174
|
+
const dA = (g2a[0] * g[0] + g2a[1] * g[1]) / (Math.hypot(...g2a) * gm), dB = (g2b[0] * g[0] + g2b[1] * g[1]) / (Math.hypot(...g2b) * gm);
|
|
175
|
+
if (dA < 0.93 || dB < 0.93) continue;
|
|
176
|
+
if (!freeOf(x, y, 110)) continue;
|
|
177
|
+
let ang = Math.atan2(ty, tx);
|
|
178
|
+
if (Math.sin(ang) * g[0] - Math.cos(ang) * g[1] < 0) ang += Math.PI; // read uphill
|
|
179
|
+
const txt = String(L);
|
|
180
|
+
labels.push({ x, y, ang, txt, L });
|
|
181
|
+
boxes.push({ x, y, w: txt.length * 6.2 + 6, h: 11, ang });
|
|
182
|
+
placed++;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---- water lining + shoreline ----
|
|
187
|
+
const wlv = [WL - 3, WL - 9, WL - 17, WL - 28, WL - 42, WL - 60];
|
|
188
|
+
const ws = wlv.map(() => []);
|
|
189
|
+
wlv.forEach((L, k) => contours(hv, gw, gh, MX0, MY0, [L, L + 1000], (kk, a, b, c, d) => { if (kk === 0) ws[k].push(a, b, c, d); }));
|
|
190
|
+
const shore = [];
|
|
191
|
+
contours(hv, gw, gh, MX0, MY0, [WL, WL + 1000], (kk, a, b, c, d) => { if (kk === 0) shore.push(a, b, c, d); });
|
|
192
|
+
|
|
193
|
+
// ---- streams: steepest descent from springs that reach the lake ----
|
|
194
|
+
const streams = [];
|
|
195
|
+
{
|
|
196
|
+
const gradS = (x, y) => [(hAt(x + 7, y) - hAt(x - 7, y)) / 14, (hAt(x, y + 7) - hAt(x, y - 7)) / 14];
|
|
197
|
+
const sr = H.rng(303);
|
|
198
|
+
for (let tries = 0; tries < 160 && streams.length < 4; tries++) {
|
|
199
|
+
let x = sr.range(MX0 + 30, MX1 - 30), y = sr.range(MY0 + 30, MY1 - 30);
|
|
200
|
+
const h0 = hAt(x, y); if (h0 < WL + 260 || h0 > hmax - 250) continue;
|
|
201
|
+
if (streams.some((f) => Math.hypot(f[0][0] - x, f[0][1] - y) < 220)) continue;
|
|
202
|
+
let d = [0, 0]; const pts = [[x, y]]; let ok = false;
|
|
203
|
+
for (let st = 0; st < 1400; st++) {
|
|
204
|
+
const g = gradS(x, y), gm = Math.hypot(g[0], g[1]) || 1e-6;
|
|
205
|
+
d = [d[0] * 0.75 - g[0] / gm * 0.25, d[1] * 0.75 - g[1] / gm * 0.25];
|
|
206
|
+
const dm = Math.hypot(d[0], d[1]) || 1; x += d[0] / dm * 2; y += d[1] / dm * 2;
|
|
207
|
+
pts.push([x, y]);
|
|
208
|
+
if (hAt(x, y) < WL) { ok = true; break; }
|
|
209
|
+
if (x < MX0 || x > MX1 || y < MY0 || y > MY1) break;
|
|
210
|
+
}
|
|
211
|
+
if (ok && pts.length > 90) streams.push(pts);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const spots = [[1100, 470], [560, 230], [960, 560], [150, 150], [720, 470]].map(([x, y]) => [x, y, hAt(x, y)]).filter((s) => s[2] >= WL);
|
|
215
|
+
return { paperL, waterL, woodL, shadeL, hmax, pk, WL, levels, segs, isIndex, trail, tlx, tly, labels, boxes, ws, shore, streams, spots, mw, mh };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
registerStyle({
|
|
219
|
+
id: 'topographic',
|
|
220
|
+
name: 'Topographic Survey',
|
|
221
|
+
category: 'Data & Information',
|
|
222
|
+
description: 'A vintage survey quadrangle of a "loss landscape": brown contour lines with heavier labelled index contours, soft hillshade and woodland tint, a water-lined lake, a red dashed switchback trail and a full printed collar with scale bar and north arrow.',
|
|
223
|
+
motion: 'Printed plate by plate: the black frame and collar, then the grey hillshade, the green woodland sliding into register, the blue lake, then brown contours rising from the shore like a tide; streams trace downhill, names land, and the red trail walks itself down the switchbacks while the camera eases in.',
|
|
224
|
+
duration: 7,
|
|
225
|
+
fonts: ['400 10px "Inter"', '600 10px "Inter"', '700 13px "Libre Baskerville"', 'italic 400 14px "Libre Baskerville"',
|
|
226
|
+
'italic 400 11px "Libre Baskerville"', '700 22px "Libre Baskerville"', '400 10px "Libre Baskerville"', '400 9px "Inter"', 'italic 700 24px "Libre Baskerville"', '700 66px "Libre Baskerville"', '700 28px "Libre Baskerville"', 'italic 400 28px "Libre Baskerville"', '700 64px "Libre Baskerville"'],
|
|
227
|
+
frameT: 6.9,
|
|
228
|
+
readsPixels: true,
|
|
229
|
+
draw(ctx, W, H_, t) {
|
|
230
|
+
const C = CACHE || (CACHE = buildStatic(W, H_));
|
|
231
|
+
const { pk, WL, hmax, mw, mh, trail, tlx, tly } = C;
|
|
232
|
+
const seg = (a, b) => H.clamp((t - a) / (b - a));
|
|
233
|
+
const INKC = '#1E1A16';
|
|
234
|
+
|
|
235
|
+
// slow camera ease-in toward the trail across the whole shot
|
|
236
|
+
ctx.save();
|
|
237
|
+
const push = 1 + 0.02 * H.ease.inOut(seg(0.8, 7));
|
|
238
|
+
const fxp = 640, fyp = 360;
|
|
239
|
+
ctx.translate(fxp, fyp); ctx.scale(push, push); ctx.translate(-fxp, -fyp);
|
|
240
|
+
|
|
241
|
+
ctx.drawImage(C.paperL, 0, 0);
|
|
242
|
+
|
|
243
|
+
// ---- grey plate: hillshade ----
|
|
244
|
+
const gA = seg(0.3, 1.0);
|
|
245
|
+
if (gA > 0) { ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.85 * gA; ctx.drawImage(C.shadeL, MX0, MY0); ctx.restore(); }
|
|
246
|
+
// ---- green plate: woodland slides into registration ----
|
|
247
|
+
const wA = seg(0.6, 1.3);
|
|
248
|
+
if (wA > 0) { const off = (1 - H.ease.out(wA)) * 7; ctx.save(); ctx.globalAlpha = wA; ctx.drawImage(C.woodL, MX0 + off, MY0 - off * 0.6); ctx.restore(); }
|
|
249
|
+
// ---- blue plate: lake ----
|
|
250
|
+
const bA = seg(0.9, 1.5);
|
|
251
|
+
if (bA > 0) { ctx.save(); ctx.globalAlpha = bA; ctx.drawImage(C.waterL, MX0, MY0); ctx.restore(); }
|
|
252
|
+
|
|
253
|
+
// ---- red section grid ----
|
|
254
|
+
const rA = seg(2.4, 3.0);
|
|
255
|
+
if (rA > 0) {
|
|
256
|
+
ctx.save(); ctx.globalAlpha = rA;
|
|
257
|
+
ctx.strokeStyle = 'rgba(190,70,50,0.28)'; ctx.lineWidth = 0.8; ctx.setLineDash([10, 4]);
|
|
258
|
+
for (let x = MX0 + 201; x < MX1; x += 201) { ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY1); ctx.stroke(); }
|
|
259
|
+
for (let y = MY0 + 196; y < MY1; y += 196) { ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX1, y); ctx.stroke(); }
|
|
260
|
+
ctx.setLineDash([]);
|
|
261
|
+
ctx.fillStyle = 'rgba(180,60,45,0.55)'; ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center';
|
|
262
|
+
let sec = 1;
|
|
263
|
+
for (let r = 0; r < 3; r++) for (let c = 0; c < 6; c++) { if ((r * 6 + c) % 2 === 0) ctx.fillText(String(sec), MX0 + 100 + c * 201, MY0 + 30 + r * 196); sec++; }
|
|
264
|
+
ctx.restore();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- brown plate: contours rise from the shore upward ----
|
|
268
|
+
const rise = seg(1.2, 3.4);
|
|
269
|
+
const visibleL = rise <= 0 ? -1e9 : WL + (hmax - WL + CI) * H.ease.inOut(rise);
|
|
270
|
+
ctx.save();
|
|
271
|
+
ctx.beginPath(); ctx.rect(MX0, MY0, mw, mh);
|
|
272
|
+
for (const b of C.boxes) {
|
|
273
|
+
const c = Math.cos(b.ang), s = Math.sin(b.ang), hw = b.w / 2, hh = b.h / 2;
|
|
274
|
+
const P = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([u, v]) => [b.x + u * c - v * s, b.y + u * s + v * c]);
|
|
275
|
+
ctx.moveTo(...P[0]); P.slice(1).forEach((p) => ctx.lineTo(...p)); ctx.closePath();
|
|
276
|
+
}
|
|
277
|
+
ctx.clip('evenodd');
|
|
278
|
+
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
279
|
+
C.levels.forEach((L, k) => {
|
|
280
|
+
if (L > visibleL) return;
|
|
281
|
+
const s = C.segs[k]; if (!s.length) return;
|
|
282
|
+
const idx = C.isIndex(L);
|
|
283
|
+
// the newest ring inks in over a short band
|
|
284
|
+
const fresh = H.clamp((visibleL - L) / (CI * 2.5));
|
|
285
|
+
ctx.strokeStyle = idx ? `rgba(150,88,48,${0.95 * (0.35 + 0.65 * fresh)})` : `rgba(162,100,58,${0.72 * (0.35 + 0.65 * fresh)})`;
|
|
286
|
+
ctx.lineWidth = idx ? 1.45 : 0.65;
|
|
287
|
+
ctx.beginPath();
|
|
288
|
+
for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); }
|
|
289
|
+
ctx.stroke();
|
|
290
|
+
});
|
|
291
|
+
ctx.restore();
|
|
292
|
+
|
|
293
|
+
// ---- blue linework: shoreline and water lining ripple outward from the shore ----
|
|
294
|
+
if (bA > 0) {
|
|
295
|
+
ctx.lineCap = 'round';
|
|
296
|
+
ctx.strokeStyle = `rgba(52,108,146,${0.95 * bA})`; ctx.lineWidth = 1.3; ctx.beginPath();
|
|
297
|
+
for (let q = 0; q < C.shore.length; q += 4) { ctx.moveTo(C.shore[q], C.shore[q + 1]); ctx.lineTo(C.shore[q + 2], C.shore[q + 3]); } ctx.stroke();
|
|
298
|
+
C.ws.forEach((s, k) => {
|
|
299
|
+
const a = seg(1.2 + k * 0.12, 1.5 + k * 0.12); if (a <= 0) return;
|
|
300
|
+
ctx.strokeStyle = `rgba(63,127,166,${(0.75 - k * 0.1) * a})`; ctx.lineWidth = 0.6; ctx.beginPath();
|
|
301
|
+
for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); } ctx.stroke();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---- streams trace downhill from their springs ----
|
|
306
|
+
const sP = seg(2.6, 3.8);
|
|
307
|
+
if (sP > 0) {
|
|
308
|
+
ctx.strokeStyle = 'rgba(52,108,146,0.9)'; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
309
|
+
C.streams.forEach((pts, si) => {
|
|
310
|
+
const n = Math.floor(pts.length * H.clamp(sP * 1.15 - si * 0.05));
|
|
311
|
+
for (let q = 1; q < n; q++) {
|
|
312
|
+
const f = q / pts.length;
|
|
313
|
+
ctx.lineWidth = 0.55 + 1.0 * f;
|
|
314
|
+
if (f < 0.22 && (q >> 2) % 3 === 0) continue; // intermittent near the spring
|
|
315
|
+
ctx.beginPath(); ctx.moveTo(...pts[q - 1]); ctx.lineTo(...pts[q]); ctx.stroke();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- trail walks down from the summit ----
|
|
321
|
+
const tP = H.ease.inOut(seg(3.5, 5.0));
|
|
322
|
+
const nT = Math.floor((trail.length / 2) * tP);
|
|
323
|
+
if (nT > 2) {
|
|
324
|
+
ctx.save();
|
|
325
|
+
ctx.strokeStyle = '#B5352A'; ctx.lineWidth = 1.7; ctx.setLineDash([6, 4]); ctx.lineCap = 'butt'; ctx.lineJoin = 'round';
|
|
326
|
+
ctx.beginPath(); ctx.moveTo(trail[0], trail[1]);
|
|
327
|
+
for (let q = 2; q < nT * 2; q += 2) ctx.lineTo(trail[q], trail[q + 1]);
|
|
328
|
+
ctx.stroke(); ctx.restore();
|
|
329
|
+
ctx.fillStyle = '#B5352A'; ctx.beginPath(); ctx.arc(trail[0], trail[1], 2.6, 0, 7); ctx.fill();
|
|
330
|
+
// walker: leads the drawing, then keeps hiking the route on a loop
|
|
331
|
+
const hikeS = t < 5.0 ? nT - 1 : Math.floor(((t - 5.0) * 0.18 % 1) * (trail.length / 2 - 1));
|
|
332
|
+
const hx = trail[hikeS * 2], hy = trail[hikeS * 2 + 1];
|
|
333
|
+
ctx.fillStyle = 'rgba(238,229,203,0.9)'; ctx.beginPath(); ctx.arc(hx, hy, 4.4, 0, 7); ctx.fill();
|
|
334
|
+
ctx.fillStyle = '#B5352A'; ctx.beginPath(); ctx.arc(hx, hy, 3, 0, 7); ctx.fill();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- contour numbers ink with their level ----
|
|
338
|
+
ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
339
|
+
C.labels.forEach((lb) => {
|
|
340
|
+
const a = H.clamp((visibleL - lb.L) / (CI * 2)); if (a <= 0) return;
|
|
341
|
+
ctx.fillStyle = `rgba(140,80,42,${a})`;
|
|
342
|
+
ctx.save(); ctx.translate(lb.x, lb.y); ctx.rotate(lb.ang); ctx.fillText(lb.txt, 0, 0.5); ctx.restore();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ---- black plate, part 2: names & symbols land after the relief ----
|
|
346
|
+
ctx.textBaseline = 'alphabetic';
|
|
347
|
+
const nA = seg(3.2, 3.7);
|
|
348
|
+
if (nA > 0) {
|
|
349
|
+
ctx.save(); ctx.globalAlpha = nA;
|
|
350
|
+
ctx.fillStyle = INKC; ctx.strokeStyle = INKC; ctx.lineWidth = 1;
|
|
351
|
+
ctx.beginPath(); ctx.moveTo(pk[0], pk[1] - 5); ctx.lineTo(pk[0] + 5, pk[1] + 4); ctx.lineTo(pk[0] - 5, pk[1] + 4); ctx.closePath(); ctx.stroke();
|
|
352
|
+
ctx.beginPath(); ctx.arc(pk[0], pk[1] + 1, 1, 0, 7); ctx.fill();
|
|
353
|
+
ctx.font = '700 13px "Libre Baskerville"';
|
|
354
|
+
const str = 'MOUNT COMPUTE';
|
|
355
|
+
ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.8)'; ctx.lineWidth = 3;
|
|
356
|
+
let w = 0; for (const ch of str) w += ctx.measureText(ch).width + 3; w -= 3;
|
|
357
|
+
let cx = pk[0] - w / 2; const y0 = pk[1] - 14;
|
|
358
|
+
for (const ch of str) { ctx.strokeText(ch, cx, y0); cx += ctx.measureText(ch).width + 3; }
|
|
359
|
+
ctx.restore();
|
|
360
|
+
ctx.fillStyle = INKC; spacedText(ctx, str, pk[0], pk[1] - 14, 3, 'center');
|
|
361
|
+
ctx.font = '400 10px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillText(String(Math.round(hmax)), pk[0] + 9, pk[1] + 9);
|
|
362
|
+
ctx.font = '400 10px "Libre Baskerville"'; ctx.fillStyle = INKC; ctx.textAlign = 'left';
|
|
363
|
+
C.spots.forEach(([x, y, h]) => { ctx.fillText('×', x - 3, y + 3); ctx.fillText(String(Math.round(h)), x + 6, y + 4); });
|
|
364
|
+
ctx.restore();
|
|
365
|
+
}
|
|
366
|
+
const lA = seg(1.6, 2.1);
|
|
367
|
+
if (lA > 0) {
|
|
368
|
+
ctx.save(); ctx.globalAlpha = lA;
|
|
369
|
+
ctx.font = 'italic 400 14px "Libre Baskerville"'; ctx.fillStyle = '#2C6D96'; ctx.textAlign = 'center';
|
|
370
|
+
spacedText(ctx, 'Local Minimum Lake', 330, 405, 1.5, 'center');
|
|
371
|
+
ctx.font = 'italic 400 11px "Libre Baskerville"';
|
|
372
|
+
ctx.fillText('Elev ' + Math.round(WL), 330, 422);
|
|
373
|
+
ctx.restore();
|
|
374
|
+
}
|
|
375
|
+
const trA = seg(4.4, 4.9);
|
|
376
|
+
if (trA > 0 && tlx) {
|
|
377
|
+
ctx.save(); ctx.globalAlpha = trA; ctx.font = 'italic 400 11px "Libre Baskerville"'; ctx.fillStyle = '#8E2A22'; ctx.textAlign = 'left';
|
|
378
|
+
ctx.strokeStyle = 'rgba(238,229,203,0.85)'; ctx.lineWidth = 3;
|
|
379
|
+
ctx.strokeText('Gradient Descent Trail', tlx + 10, tly - 6); ctx.fillText('Gradient Descent Trail', tlx + 10, tly - 6);
|
|
380
|
+
ctx.restore();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---- black plate, part 1: neatline, graticule and collar (printed first) ----
|
|
384
|
+
const kA = seg(0, 0.6);
|
|
385
|
+
ctx.save(); ctx.globalAlpha = kA;
|
|
386
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1.2; ctx.strokeRect(MX0 + 0.5, MY0 + 0.5, mw, mh);
|
|
387
|
+
ctx.lineWidth = 0.6; ctx.strokeRect(MX0 - 3.5, MY0 - 3.5, mw + 8, mh + 8);
|
|
388
|
+
ctx.font = '400 9px "Inter"'; ctx.fillStyle = INKC; ctx.lineWidth = 1;
|
|
389
|
+
for (let k = 1; k < 4; k++) {
|
|
390
|
+
const x = MX0 + mw * k / 4;
|
|
391
|
+
ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY0 + 8); ctx.moveTo(x, MY1); ctx.lineTo(x, MY1 - 8); ctx.stroke();
|
|
392
|
+
ctx.textAlign = 'center'; ctx.fillText(['122°20′', '122°17′30″', '122°15′'][k - 1], x, MY0 - 7);
|
|
393
|
+
}
|
|
394
|
+
for (let k = 1; k < 3; k++) {
|
|
395
|
+
const y = MY0 + mh * k / 3;
|
|
396
|
+
ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX0 + 8, y); ctx.moveTo(MX1, y); ctx.lineTo(MX1 - 8, y); ctx.stroke();
|
|
397
|
+
}
|
|
398
|
+
ctx.textAlign = 'left'; ctx.fillText('37°52′30″ 122°22′30″', MX0, MY0 - 7);
|
|
399
|
+
ctx.textAlign = 'right'; ctx.fillText('122°12′30″', MX1, MY0 - 7);
|
|
400
|
+
ctx.font = '400 9px "Inter"'; ctx.textAlign = 'left';
|
|
401
|
+
spacedText(ctx, 'THE MODEL SURVEY', 489, 16, 2, 'center');
|
|
402
|
+
spacedText(ctx, 'TRAINING SERIES', 1093, 16, 2, 'center');
|
|
403
|
+
ctx.restore();
|
|
404
|
+
|
|
405
|
+
// collar text sets in shortly after the frame
|
|
406
|
+
const cA = seg(0.25, 0.9);
|
|
407
|
+
const cy0 = MY1 + 10;
|
|
408
|
+
ctx.save(); ctx.globalAlpha = cA;
|
|
409
|
+
ctx.textAlign = 'left'; ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"';
|
|
410
|
+
ctx.fillText('Mapped, edited and published by the Model Survey', MX0, cy0 + 18);
|
|
411
|
+
ctx.fillText('Topography compiled from imagined stereo pairs, 2026', MX0, cy0 + 31);
|
|
412
|
+
ctx.fillText('Red tint: township and section lines', MX0, cy0 + 44);
|
|
413
|
+
ctx.fillText('Illustrative map. Not for navigation', MX0, cy0 + 57);
|
|
414
|
+
{
|
|
415
|
+
const ax = 380, ay = cy0 + 62;
|
|
416
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1;
|
|
417
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax, ay - 50); ctx.stroke();
|
|
418
|
+
ctx.save(); ctx.translate(ax, ay - 55); ctx.beginPath();
|
|
419
|
+
for (let q = 0; q < 10; q++) { const r = q % 2 ? 2.2 : 5.5, a = -Math.PI / 2 + q * Math.PI / 5; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); }
|
|
420
|
+
ctx.closePath(); ctx.fill(); ctx.restore();
|
|
421
|
+
const mxp = ax + Math.sin(0.24) * 46, myp = ay - Math.cos(0.24) * 46;
|
|
422
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(mxp, myp); ctx.stroke();
|
|
423
|
+
ctx.beginPath(); ctx.moveTo(mxp, myp); ctx.lineTo(mxp - 1.5, myp + 10); ctx.lineTo(mxp + 3, myp + 9); ctx.closePath(); ctx.fill();
|
|
424
|
+
ctx.font = '400 9px "Inter"'; ctx.textAlign = 'left';
|
|
425
|
+
ctx.fillText('MN', mxp + 4, myp + 4); ctx.fillText('14°', ax + 5, ay - 6);
|
|
426
|
+
}
|
|
427
|
+
{
|
|
428
|
+
const sx = 520, sy = cy0 + 36, unit = 60;
|
|
429
|
+
ctx.textAlign = 'center'; ctx.fillStyle = INKC;
|
|
430
|
+
ctx.font = '700 13px "Libre Baskerville"'; spacedText(ctx, 'SCALE 1:24 000', sx + unit * 1.5, cy0 + 16, 1.5, 'center');
|
|
431
|
+
ctx.lineWidth = 0.8; ctx.strokeStyle = INKC;
|
|
432
|
+
for (let k = 0; k < 4; k++) { ctx.fillStyle = k % 2 ? INKC : PAPER; ctx.fillRect(sx - unit + k * unit / 4, sy, unit / 4, 5); }
|
|
433
|
+
for (let k = 0; k < 3; k++) { ctx.fillStyle = k % 2 ? PAPER : INKC; ctx.fillRect(sx + k * unit, sy, unit, 5); }
|
|
434
|
+
ctx.strokeRect(sx - unit, sy, unit * 4, 5);
|
|
435
|
+
ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"';
|
|
436
|
+
['1', '0', '1', '2', '3'].forEach((s, k) => ctx.fillText(s, sx - unit + k * unit, sy + 16));
|
|
437
|
+
ctx.fillText('KILOMETERS', sx + unit * 3 + 34, sy + 5);
|
|
438
|
+
ctx.font = '400 10px "Libre Baskerville"';
|
|
439
|
+
spacedText(ctx, 'CONTOUR INTERVAL 40 METERS', sx + unit * 1, sy + 34, 1.2, 'center');
|
|
440
|
+
}
|
|
441
|
+
ctx.textAlign = 'right'; ctx.fillStyle = INKC;
|
|
442
|
+
ctx.font = '700 22px "Libre Baskerville"'; spacedText(ctx, 'MOUNT COMPUTE', MX1, cy0 + 30, 3, 'right');
|
|
443
|
+
ctx.font = '400 10px "Libre Baskerville"';
|
|
444
|
+
spacedText(ctx, 'LOSS LANDSCAPE QUADRANGLE', MX1, cy0 + 47, 1.6, 'right');
|
|
445
|
+
ctx.font = '400 9px "Inter"';
|
|
446
|
+
spacedText(ctx, '7.5-MINUTE SERIES (TOPOGRAPHIC) · 2026', MX1, cy0 + 62, 0.8, 'right');
|
|
447
|
+
ctx.restore();
|
|
448
|
+
|
|
449
|
+
// ---- age: fold crease, foxing ----
|
|
450
|
+
ctx.save();
|
|
451
|
+
const fx = 640;
|
|
452
|
+
const gfold = ctx.createLinearGradient(fx - 10, 0, fx + 10, 0);
|
|
453
|
+
gfold.addColorStop(0, 'rgba(255,255,255,0)'); gfold.addColorStop(0.45, 'rgba(255,255,255,0.18)');
|
|
454
|
+
gfold.addColorStop(0.55, 'rgba(90,70,40,0.10)'); gfold.addColorStop(1, 'rgba(90,70,40,0)');
|
|
455
|
+
ctx.fillStyle = gfold; ctx.fillRect(fx - 10, -20, 20, H_ + 40);
|
|
456
|
+
ctx.globalCompositeOperation = 'multiply';
|
|
457
|
+
const fr = H.rng(77);
|
|
458
|
+
for (let k = 0; k < 7; k++) {
|
|
459
|
+
const x = fr.range(0, W), y = fr.range(0, H_), r = fr.range(30, 140);
|
|
460
|
+
const gg = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
461
|
+
gg.addColorStop(0, 'rgba(180,150,100,0.10)'); gg.addColorStop(0.8, 'rgba(180,150,100,0.04)'); gg.addColorStop(1, 'rgba(180,150,100,0)');
|
|
462
|
+
ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
|
|
463
|
+
}
|
|
464
|
+
ctx.restore();
|
|
465
|
+
ctx.restore(); // camera
|
|
466
|
+
H.vignette(ctx, W, H_, 0.18, '80,60,30');
|
|
467
|
+
H.grain(ctx, W, H_, 0.035, 31 + Math.floor(t * 12));
|
|
468
|
+
},
|
|
469
|
+
// Sound: a surveyor's sheet out in the open. A light wind under everything, a draughting pen ticks each
|
|
470
|
+
// survey × onto the sheet, the terrain rises as a slow low swell, the lake laps in, and the hiker's boots
|
|
471
|
+
// crunch gravel step by step down the trail (steps follow the eased hike). Times mirror explainer() below.
|
|
472
|
+
explainerSound(k) {
|
|
473
|
+
const D = 15, io = H.ease.inOut;
|
|
474
|
+
const seg = (t, a, b) => H.clamp((t - a) / (b - a));
|
|
475
|
+
const when = (t0, t1, ease, u) => { for (let i = 0; i <= 400; i++) { const t = t0 + ((t1 - t0) * i) / 400; if (ease(i / 400) >= u - 1e-6) return t; } return t1; };
|
|
476
|
+
const pan = (px) => H.clamp((px / 1280) * 2 - 1, -1, 1) * 0.6;
|
|
477
|
+
// draughting pen on the sheet
|
|
478
|
+
const pen = (a, b, g = 1, p = 0, minor = true) => { k.noise(a, { dur: 0.016, filter: 'bandpass', f: 1500, q: 1.3, gain: 0.12 * g, a: 0.001, pan: p, minor }); k.play('stroke', a, { material: 'pen', dur: b - a, gain: 2.0 * g, pan: p, minor }); };
|
|
479
|
+
const nib = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.018, filter: 'bandpass', f: 2200, q: 1.5, gain: 0.16 * g, a: 0.001, pan: p }); k.noise(t + 0.03, { dur: 0.016, filter: 'bandpass', f: 1900, q: 1.5, gain: 0.12 * g, a: 0.001, pan: p }); });
|
|
480
|
+
// a boot on gravel
|
|
481
|
+
const step = (t, g = 1, p = 0) => k.withMinor(() => {
|
|
482
|
+
k.noise(t, { dur: 0.08, color: 'pink', filter: 'bandpass', f: 850, q: 0.7, gain: 0.22 * g, a: 0.004, pan: p });
|
|
483
|
+
k.noise(t + 0.012, { dur: 0.06, filter: 'bandpass', f: 1800, q: 1, gain: 0.07 * g, a: 0.003, pan: p });
|
|
484
|
+
k.tone(t, { f: [[0, 95], [0.05, 55]], dur: 0.08, gain: 0.12 * g, pan: p });
|
|
485
|
+
});
|
|
486
|
+
// a soft wooden note for type landing in the collar / cartouches
|
|
487
|
+
const wood = (t, f, g = 1, p = 0, minor = true) => { k.tone(t, { f, dur: 0.5, gain: 0.07 * g, a: 0.006, pan: p, minor }); k.tone(t, { f: k.hz(f) * 4, dur: 0.08, gain: 0.012 * g, a: 0.002, pan: p, minor }); };
|
|
488
|
+
const tick = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.016, filter: 'bandpass', f: 1700, q: 1.8, gain: 0.18 * g, a: 0.001, pan: p }); k.tone(t, { f: 200, dur: 0.025, gain: 0.04 * g, pan: p }); });
|
|
489
|
+
|
|
490
|
+
k.bed('room', 0, D, { gain: 0.25 });
|
|
491
|
+
// light open-air wind, slow gusts
|
|
492
|
+
k.withMinor(() => { for (let t = 0, i = 0; t < D; t += 1.2, i++) k.noise(t, { dur: 2.2, color: 'pink', filter: 'bandpass', f: 380 + 120 * Math.sin(i * 1.7), q: 0.6, gain: 0.035 + 0.015 * Math.sin(i * 2.3), a: 1.0, pan: Math.sin(i * 0.9) * 0.4 }); });
|
|
493
|
+
|
|
494
|
+
// ---- the question in a cartouche, its subtitle, then it docks into the collar (1.5 -> 2.4)
|
|
495
|
+
wood(0.05, 'D3', 1.2, 0, false);
|
|
496
|
+
wood(0.5, 'A3', 0.7);
|
|
497
|
+
k.noise(1.5, { dur: 0.9, color: 'brown', filter: 'bandpass', f: [[0, 260], [0.9, 380]], q: 0.6, gain: 0.08, a: 0.35, pan: 0.4, minor: true });
|
|
498
|
+
|
|
499
|
+
// ---- data: survey points plotted (accelerating), terrain rises, woods, lake
|
|
500
|
+
wood(2.05, 'G3', 0.7, -0.6); // EXAMPLES SURVEYED box
|
|
501
|
+
{ // one pen × per new point, throttled to 12/s; shown = 260 * ease.in(seg(2.0, 5.4))^0.7
|
|
502
|
+
let last = 0, lastT = -1;
|
|
503
|
+
for (let t = 2.0; t <= 5.4; t += 1 / 120) {
|
|
504
|
+
const n = Math.floor(260 * Math.pow(H.ease.in(seg(t, 2.0, 5.4)), 0.7));
|
|
505
|
+
if (n > last && t - lastT >= 1 / 12) { const r = H.rng(515 + n)(); nib(t, 0.7 + 0.3 * seg(t, 2, 5.4), (r - 0.5) * 1.2); last = n; lastT = t; }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
k.noise(2.2, { dur: 3.8, color: 'brown', filter: 'lowpass', f: [[0, 120], [3.0, 380], [3.8, 200]], gain: 0.2, a: 2.6 }); // the land rises
|
|
509
|
+
k.withMinor(() => { for (let t = 4.2, i = 0; t < 5.6; t += 0.35, i++) k.noise(t, { dur: 0.6, color: 'pink', filter: 'bandpass', f: 500 + (i % 3) * 90, q: 1.4, gain: 0.05, a: 0.25, pan: pan(300) }); }); // the lake laps in
|
|
510
|
+
tick(5.6, 1.2, -0.6); // 1,000,000 surveyed
|
|
511
|
+
k.play('tock', 5.6, { f: 300, gain: 0.45, pan: -0.6 });
|
|
512
|
+
wood(5.25, 'E3', 0.9, 0.1); // HIGH ERROR / Low error lettered
|
|
513
|
+
|
|
514
|
+
// ---- camera to the summit; the hiker
|
|
515
|
+
k.noise(6.2, { dur: 0.9, color: 'pink', filter: 'bandpass', f: [[0, 280], [0.45, 560], [0.9, 260]], q: 0.7, gain: 0.09, a: 0.4 });
|
|
516
|
+
step(6.7, 0.8); // the hiker sets down at the summit
|
|
517
|
+
wood(6.85, 'G3', 0.6, -0.4); // GUESS → CHECK → ADJUST legend
|
|
518
|
+
pen(7.0, 7.3, 0.9, 0.2); // guess: "dog?"
|
|
519
|
+
pen(7.25, 7.35, 1.6, 0.3, false); pen(7.35, 7.45, 1.6, 0.3, false); // red ✗
|
|
520
|
+
k.tone(7.25, { f: 'D3', type: 'triangle', dur: 0.3, gain: 0.17, hold: 0.08, filter: 'lowpass', f2: 800, pan: 0.3 });
|
|
521
|
+
k.tone(7.43, { f: 'A2', type: 'triangle', dur: 0.5, gain: 0.17, hold: 0.12, filter: 'lowpass', f2: 600, pan: 0.3 });
|
|
522
|
+
pen(7.45, 7.8, 1.0, 0); // ring round the hiker
|
|
523
|
+
pen(7.6, 8.3, 0.7, -0.1); // check: slope arrow drawn downhill
|
|
524
|
+
// adjust: first leg down 8.4 -> 9.4, five steps
|
|
525
|
+
for (let i = 1; i <= 5; i++) step(when(8.4, 9.4, io, i / 5 - 0.1), 0.9, -0.1 + i * 0.04);
|
|
526
|
+
pen(9.5, 9.8, 0.9, 0.1); // guess: "cat"
|
|
527
|
+
pen(9.75, 9.95, 1.2, 0.2, false); // blue tick
|
|
528
|
+
['G4', 'D5'].forEach((f, i) => k.play('ding', 9.82 + i * 0.08, { f, gain: 0.4, pan: 0.2 })); // right
|
|
529
|
+
|
|
530
|
+
// ---- camera out; accuracy cartouche; the hike down to the lake (10.8 -> 12.8)
|
|
531
|
+
k.noise(10.6, { dur: 1.1, color: 'pink', filter: 'bandpass', f: [[0, 260], [0.5, 520], [1.1, 240]], q: 0.7, gain: 0.09, a: 0.5 });
|
|
532
|
+
wood(10.85, 'G3', 0.7, -0.6);
|
|
533
|
+
{ let lastT = -1; for (let f = 0.06; f < 1; f += 0.06) { const t = when(10.8, 12.8, io, f); if (t - lastT >= 0.11) { step(t, 0.75, pan(H.lerp(560, 330, f))); lastT = t; } } }
|
|
534
|
+
{ let last = -1, lastT = -1; for (let t = 10.9; t <= 12.8 + 1e-6; t += 1 / 120) { const v = Math.round(61 + 36 * io(seg(t, 10.9, 12.8))); if (v !== last && t - lastT >= 1 / 12) { tick(t, 0.9, -0.6); last = v; lastT = t; } } }
|
|
535
|
+
// the hiker reaches the lake as 97% turns red: the big hit; the ring is penned round the lake
|
|
536
|
+
k.play('impact', 12.8, { gain: 0.8, pan: pan(300) });
|
|
537
|
+
k.play('ding', 12.82, { f: 'D5', gain: 0.6, pan: -0.4 });
|
|
538
|
+
k.noise(12.85, { dur: 0.9, color: 'pink', filter: 'bandpass', f: [[0, 700], [0.9, 400]], q: 1.2, gain: 0.07, a: 0.08, pan: pan(300), verb: 0.3 }); // water at the shore
|
|
539
|
+
pen(12.85, 13.3, 1.2, pan(300), false); // red ring round the lake (drawing since 12.6)
|
|
540
|
+
// takeaway cartouche settles, then resolves
|
|
541
|
+
wood(13.3, 'D3', 1.0);
|
|
542
|
+
['D4', 'A4', 'F#5'].forEach((f, i) => k.play('ding', 13.5 + i * 0.09, { f, gain: 0.36 - i * 0.06, pan: (i - 1) * 0.25 }));
|
|
543
|
+
},
|
|
544
|
+
explainerDuration: 15,
|
|
545
|
+
explainerNote: 'The loss landscape is literally the map: survey points (examples) raise the terrain, a red hiker guesses, checks the slope and steps down a dashed switchback trail while the camera pans along it, and accuracy is lettered in a map cartouche like an elevation figure.',
|
|
546
|
+
explainer(ctx, W, H_, t) {
|
|
547
|
+
const C = CACHE || (CACHE = buildStatic(W, H_));
|
|
548
|
+
const { pk, WL, hmax, mw, mh, trail } = C;
|
|
549
|
+
const seg = (a, b) => H.clamp((t - a) / (b - a));
|
|
550
|
+
const INKC = '#1E1A16', RED = '#B5352A';
|
|
551
|
+
const nTrail = trail.length / 2;
|
|
552
|
+
const TP = (f) => { const i = Math.min(nTrail - 1, Math.max(0, Math.round(f * (nTrail - 1)))); return [trail[i * 2], trail[i * 2 + 1]]; };
|
|
553
|
+
// hiker position along the trail (fraction) over time
|
|
554
|
+
const STEP1 = 0.2;
|
|
555
|
+
const hikeF = t < 8.4 ? 0 : t < 9.4 ? STEP1 * H.ease.inOut(seg(8.4, 9.4)) : t < 10.8 ? STEP1 : STEP1 + (1 - STEP1) * H.ease.inOut(seg(10.8, 12.8));
|
|
556
|
+
const hiker = TP(hikeF);
|
|
557
|
+
|
|
558
|
+
// ---- camera keyframes (scale, centre) ----
|
|
559
|
+
const K = [
|
|
560
|
+
[0, 1, 640, 360], [6.2, 1, 640, 360], [7.0, 2.3, pk[0] - 40, pk[1] + 40], [8.3, 2.3, pk[0] - 40, pk[1] + 40],
|
|
561
|
+
[9.4, 2.3, TP(STEP1)[0] + 20, TP(STEP1)[1] - 10], [10.6, 2.3, TP(STEP1)[0] + 20, TP(STEP1)[1] - 10],
|
|
562
|
+
[11.6, 1.2, 600, 330], [13.0, 1, 640, 360], [15, 1, 640, 360],
|
|
563
|
+
];
|
|
564
|
+
let cam = K[K.length - 1];
|
|
565
|
+
for (let i = 0; i < K.length - 1; i++) if (t >= K[i][0] && t <= K[i + 1][0]) {
|
|
566
|
+
const u = H.ease.inOut((t - K[i][0]) / (K[i + 1][0] - K[i][0]));
|
|
567
|
+
cam = [t, H.lerp(K[i][1], K[i + 1][1], u), H.lerp(K[i][2], K[i + 1][2], u), H.lerp(K[i][3], K[i + 1][3], u)];
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
ctx.fillStyle = PAPER; ctx.fillRect(0, 0, W, H_);
|
|
571
|
+
ctx.save();
|
|
572
|
+
ctx.translate(W / 2, H_ / 2); ctx.scale(cam[1], cam[1]); ctx.translate(-cam[2], -cam[3]);
|
|
573
|
+
ctx.drawImage(C.paperL, 0, 0);
|
|
574
|
+
|
|
575
|
+
// ---- data raises the terrain: hillshade, woods, lake and contours emerge as points are surveyed ----
|
|
576
|
+
const dataP = seg(2.2, 5.8);
|
|
577
|
+
if (dataP > 0) {
|
|
578
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.85 * H.ease.inOut(dataP); ctx.drawImage(C.shadeL, MX0, MY0); ctx.restore();
|
|
579
|
+
ctx.save(); ctx.globalAlpha = seg(3.6, 5.0); ctx.drawImage(C.woodL, MX0, MY0); ctx.restore();
|
|
580
|
+
ctx.save(); ctx.globalAlpha = seg(4.2, 5.2); ctx.drawImage(C.waterL, MX0, MY0); ctx.restore();
|
|
581
|
+
}
|
|
582
|
+
const visibleL = dataP <= 0 ? -1e9 : WL + (hmax - WL + CI) * H.ease.inOut(seg(3.0, 6.0));
|
|
583
|
+
ctx.lineCap = 'round';
|
|
584
|
+
// contours break around their index numerals, as on the printed quad (same knockout boxes as the still)
|
|
585
|
+
ctx.save();
|
|
586
|
+
ctx.beginPath(); ctx.rect(MX0, MY0, mw, mh);
|
|
587
|
+
for (const b of C.boxes) {
|
|
588
|
+
const c = Math.cos(b.ang), s = Math.sin(b.ang), hw = b.w / 2, hh = b.h / 2;
|
|
589
|
+
const P = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([u, v]) => [b.x + u * c - v * s, b.y + u * s + v * c]);
|
|
590
|
+
ctx.moveTo(...P[0]); P.slice(1).forEach((p) => ctx.lineTo(...p)); ctx.closePath();
|
|
591
|
+
}
|
|
592
|
+
ctx.clip('evenodd');
|
|
593
|
+
C.levels.forEach((L, k) => {
|
|
594
|
+
if (L > visibleL) return;
|
|
595
|
+
const s = C.segs[k]; if (!s.length) return;
|
|
596
|
+
const idx = C.isIndex(L);
|
|
597
|
+
ctx.strokeStyle = idx ? 'rgba(150,88,48,0.95)' : 'rgba(162,100,58,0.72)';
|
|
598
|
+
ctx.lineWidth = (idx ? 1.45 : 0.65) / Math.sqrt(cam[1]);
|
|
599
|
+
ctx.beginPath();
|
|
600
|
+
for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); }
|
|
601
|
+
ctx.stroke();
|
|
602
|
+
});
|
|
603
|
+
ctx.restore();
|
|
604
|
+
// index contour numerals ink with their level (uphill-reading, from the still's placement)
|
|
605
|
+
if (dataP > 0) {
|
|
606
|
+
ctx.save(); ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
607
|
+
C.labels.forEach((lb) => {
|
|
608
|
+
const a = H.clamp((visibleL - lb.L) / (CI * 2)); if (a <= 0) return;
|
|
609
|
+
ctx.fillStyle = `rgba(140,80,42,${a})`;
|
|
610
|
+
ctx.save(); ctx.translate(lb.x, lb.y); ctx.rotate(lb.ang); ctx.fillText(lb.txt, 0, 0.5); ctx.restore();
|
|
611
|
+
});
|
|
612
|
+
ctx.restore();
|
|
613
|
+
}
|
|
614
|
+
// blue streams run down to the lake once it fills (drawn with the lake's own fade)
|
|
615
|
+
const stA = seg(4.6, 5.4);
|
|
616
|
+
if (stA > 0) {
|
|
617
|
+
ctx.save(); ctx.globalAlpha = stA; ctx.strokeStyle = 'rgba(52,108,146,0.9)'; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
618
|
+
C.streams.forEach((pts) => {
|
|
619
|
+
for (let q = 1; q < pts.length; q++) {
|
|
620
|
+
const f = q / pts.length;
|
|
621
|
+
if (f < 0.22 && (q >> 2) % 3 === 0) continue; // intermittent near the spring
|
|
622
|
+
ctx.lineWidth = (0.55 + 1.0 * f) / Math.sqrt(cam[1]);
|
|
623
|
+
ctx.beginPath(); ctx.moveTo(...pts[q - 1]); ctx.lineTo(...pts[q]); ctx.stroke();
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
ctx.restore();
|
|
627
|
+
}
|
|
628
|
+
if (seg(4.2, 5.2) > 0) {
|
|
629
|
+
ctx.strokeStyle = `rgba(52,108,146,${0.95 * seg(4.2, 5.2)})`; ctx.lineWidth = 1.3; ctx.beginPath();
|
|
630
|
+
for (let q = 0; q < C.shore.length; q += 4) { ctx.moveTo(C.shore[q], C.shore[q + 1]); ctx.lineTo(C.shore[q + 2], C.shore[q + 3]); } ctx.stroke();
|
|
631
|
+
C.ws.forEach((s, k) => {
|
|
632
|
+
ctx.strokeStyle = `rgba(63,127,166,${(0.75 - k * 0.1) * seg(4.4 + k * 0.1, 5.0 + k * 0.1)})`; ctx.lineWidth = 0.6; ctx.beginPath();
|
|
633
|
+
for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); } ctx.stroke();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ---- survey points = labelled examples, plotted rapidly ----
|
|
638
|
+
const NPTS = 260, pr = H.rng(515);
|
|
639
|
+
const shown = Math.floor(NPTS * H.ease.in(seg(2.0, 5.4)) ** 0.7);
|
|
640
|
+
const ptsAlpha = 1 - 0.85 * seg(6.0, 6.8);
|
|
641
|
+
const xCol = H.rgba(H.mix('#1E1A16', '#6b5a48', seg(6.0, 6.8)), 1), xArm = 4 / Math.sqrt(cam[1]);
|
|
642
|
+
ctx.font = 'italic 700 24px "Libre Baskerville"'; ctx.textAlign = 'left';
|
|
643
|
+
for (let i = 0; i < NPTS; i++) {
|
|
644
|
+
const x = pr.range(MX0 + 30, MX1 - 30), y = pr.range(MY0 + 30, MY1 - 30), cat = pr.chance(0.5);
|
|
645
|
+
if (i >= shown) continue;
|
|
646
|
+
const fresh = false;
|
|
647
|
+
ctx.globalAlpha = ptsAlpha;
|
|
648
|
+
ctx.strokeStyle = xCol; ctx.lineCap = 'round';
|
|
649
|
+
ctx.lineWidth = 1.6 / cam[1]; ctx.beginPath(); ctx.moveTo(x - xArm, y - xArm); ctx.lineTo(x + xArm, y + xArm); ctx.moveTo(x + xArm, y - xArm); ctx.lineTo(x - xArm, y + xArm); ctx.stroke();
|
|
650
|
+
if (i < 8 && x > MX0 + 150 && y > MY0 + 140 && t < 6.3) {
|
|
651
|
+
ctx.globalAlpha = 1 - seg(5.8, 6.3);
|
|
652
|
+
ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.95)'; ctx.lineWidth = 5; ctx.lineJoin = 'round'; ctx.strokeText(cat ? 'cat' : 'dog', x + 8, y - 6);
|
|
653
|
+
ctx.restore(); ctx.fillStyle = cat ? '#2C6D96' : '#8E2A22'; ctx.fillText(cat ? 'cat' : 'dog', x + 8, y - 6);
|
|
654
|
+
}
|
|
655
|
+
ctx.globalAlpha = 1;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ---- landscape labels land on the terrain ----
|
|
659
|
+
const labA = seg(5.2, 5.8) * (1 - seg(10.6, 11.0) * 0);
|
|
660
|
+
if (labA > 0) {
|
|
661
|
+
ctx.save(); ctx.globalAlpha = labA; ctx.fillStyle = INKC; ctx.font = `700 ${(30 / Math.pow(cam[1], 0.7)).toFixed(1)}px "Libre Baskerville"`;
|
|
662
|
+
ctx.strokeStyle = 'rgba(238,229,203,0.9)'; ctx.lineWidth = 5;
|
|
663
|
+
const lab = (s, x, y) => { ctx.save(); ctx.lineJoin = 'round'; let w = 0; for (const ch of s) w += ctx.measureText(ch).width + 2.5; let cx = x - w / 2; for (const ch of s) { ctx.strokeText(ch, cx, y); cx += ctx.measureText(ch).width + 2.5; } ctx.restore(); spacedText(ctx, s, x, y, 2.5, 'center'); };
|
|
664
|
+
lab('HIGH ERROR', pk[0], pk[1] - 22);
|
|
665
|
+
// spot heights, in the still's small survey type
|
|
666
|
+
ctx.save(); ctx.fillStyle = INKC;
|
|
667
|
+
ctx.font = '400 10px "Libre Baskerville"'; ctx.textAlign = 'left';
|
|
668
|
+
C.spots.forEach(([x, y, h]) => { ctx.fillText('×', x - 3, y + 3); ctx.fillText(String(Math.round(h)), x + 6, y + 4); });
|
|
669
|
+
ctx.restore();
|
|
670
|
+
ctx.font = 'italic 700 32px "Libre Baskerville"'; ctx.fillStyle = '#2C6D96';
|
|
671
|
+
spacedText(ctx, 'Low error', 300, 410, 1.5, 'center');
|
|
672
|
+
ctx.restore();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ---- the trail: guess, check, adjust ----
|
|
676
|
+
const nDrawn = Math.floor(nTrail * hikeF);
|
|
677
|
+
if (nDrawn > 2) {
|
|
678
|
+
ctx.save(); ctx.strokeStyle = RED; ctx.lineWidth = 1.7 / Math.sqrt(cam[1]) * 1.3; ctx.setLineDash([6, 4]); ctx.lineJoin = 'round';
|
|
679
|
+
ctx.beginPath(); ctx.moveTo(trail[0], trail[1]);
|
|
680
|
+
for (let q = 1; q < nDrawn; q++) ctx.lineTo(trail[q * 2], trail[q * 2 + 1]);
|
|
681
|
+
ctx.stroke(); ctx.restore();
|
|
682
|
+
}
|
|
683
|
+
const hikerA = seg(6.6, 7.0);
|
|
684
|
+
const halo = (str, x, y, col) => { ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.92)'; ctx.lineWidth = 3; ctx.lineJoin = 'round'; ctx.strokeText(str, x, y); ctx.restore(); ctx.fillStyle = col; ctx.fillText(str, x, y); };
|
|
685
|
+
if (hikerA > 0) {
|
|
686
|
+
// guess #1 at the summit
|
|
687
|
+
const [gx, gy] = TP(0);
|
|
688
|
+
const g1 = seg(7.0, 7.3) * (1 - seg(8.3, 8.6));
|
|
689
|
+
if (g1 > 0) {
|
|
690
|
+
ctx.save(); ctx.globalAlpha = g1; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
|
|
691
|
+
halo('guess: “dog?”', gx + 26, gy - 14, INKC);
|
|
692
|
+
// red ✗ and a hand-drawn ring for emphasis
|
|
693
|
+
const xA = seg(7.25, 7.45), xx = gx + 26 + ctx.measureText('guess: “dog?”').width + 8, xy = gy - 24;
|
|
694
|
+
ctx.strokeStyle = RED; ctx.lineWidth = 2.2; ctx.lineCap = 'round';
|
|
695
|
+
ctx.beginPath(); ctx.moveTo(xx, xy); ctx.lineTo(xx + 10 * H.clamp(xA * 2), xy + 10 * H.clamp(xA * 2));
|
|
696
|
+
if (xA > 0.5) { ctx.moveTo(xx + 10, xy); ctx.lineTo(xx + 10 - 10 * (xA * 2 - 1), xy + 10 * (xA * 2 - 1)); } ctx.stroke();
|
|
697
|
+
const rr = seg(7.35, 7.8), r0 = H.rng(9);
|
|
698
|
+
ctx.lineWidth = 1.4; ctx.beginPath();
|
|
699
|
+
for (let k = 0; k <= 40 * rr; k++) { const a = -2.2 + k / 40 * 7.0, r = 20 + Math.sin(k * 0.4) * 1.2 + k * 0.08; const px = gx + Math.cos(a) * r * 1.15, py = gy + Math.sin(a) * r; k ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }
|
|
700
|
+
ctx.stroke();
|
|
701
|
+
ctx.restore();
|
|
702
|
+
}
|
|
703
|
+
// check: slope arrow drawn on, pointing downhill along the first leg
|
|
704
|
+
const ck = seg(7.6, 8.3) * (1 - seg(8.6, 8.9));
|
|
705
|
+
if (ck > 0) {
|
|
706
|
+
const [ax, ay] = TP(0.01), [bx, by] = TP(STEP1 * 0.92);
|
|
707
|
+
const ex = H.lerp(ax, bx, ck), ey = H.lerp(ay, by, ck);
|
|
708
|
+
ctx.save(); ctx.strokeStyle = RED; ctx.fillStyle = RED; ctx.lineWidth = 2; ctx.setLineDash([]);
|
|
709
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ex, ey); ctx.stroke();
|
|
710
|
+
const an = Math.atan2(by - ay, bx - ax);
|
|
711
|
+
ctx.beginPath(); ctx.moveTo(ex + Math.cos(an) * 6, ey + Math.sin(an) * 6); ctx.lineTo(ex + Math.cos(an + 2.5) * 7, ey + Math.sin(an + 2.5) * 7); ctx.lineTo(ex + Math.cos(an - 2.5) * 7, ey + Math.sin(an - 2.5) * 7); ctx.closePath(); ctx.fill();
|
|
712
|
+
ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
|
|
713
|
+
halo('check: downhill', (ax + bx) / 2 + 14, (ay + by) / 2 + 4, RED);
|
|
714
|
+
ctx.restore();
|
|
715
|
+
}
|
|
716
|
+
// adjust label while walking
|
|
717
|
+
const adj = seg(8.4, 8.7) * (1 - seg(9.7, 10.0));
|
|
718
|
+
if (adj > 0) { ctx.save(); ctx.globalAlpha = adj; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left'; halo('adjust: step down', hiker[0] + 12, hiker[1] + 18, INKC); ctx.restore(); }
|
|
719
|
+
// guess #2: lower, better
|
|
720
|
+
const g2 = seg(9.5, 9.8) * (1 - seg(10.9, 11.2));
|
|
721
|
+
if (g2 > 0) {
|
|
722
|
+
const [hx, hy] = TP(STEP1);
|
|
723
|
+
ctx.save(); ctx.globalAlpha = g2; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
|
|
724
|
+
halo('guess: “cat”', hx + 12, hy - 12, INKC);
|
|
725
|
+
ctx.strokeStyle = '#2C6D96'; ctx.lineWidth = 2.4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
726
|
+
const v = seg(9.7, 9.95);
|
|
727
|
+
ctx.beginPath(); ctx.moveTo(hx + 92, hy - 17); ctx.lineTo(hx + 96, hy - 12);
|
|
728
|
+
if (v > 0.4) ctx.lineTo(hx + 96 + 8 * (v - 0.4) / 0.6, hy - 12 - 12 * (v - 0.4) / 0.6);
|
|
729
|
+
ctx.stroke(); ctx.restore();
|
|
730
|
+
}
|
|
731
|
+
// hiker marker
|
|
732
|
+
ctx.save(); ctx.globalAlpha = hikerA;
|
|
733
|
+
ctx.fillStyle = 'rgba(238,229,203,0.95)'; ctx.beginPath(); ctx.arc(hiker[0], hiker[1], 5.5, 0, 7); ctx.fill();
|
|
734
|
+
ctx.fillStyle = RED; ctx.beginPath(); ctx.arc(hiker[0], hiker[1], 3.8, 0, 7); ctx.fill();
|
|
735
|
+
ctx.restore();
|
|
736
|
+
}
|
|
737
|
+
// final emphasis: red ring around the lake = the low-error goal
|
|
738
|
+
const ring = seg(12.6, 13.3);
|
|
739
|
+
if (ring > 0) {
|
|
740
|
+
ctx.save(); ctx.strokeStyle = RED; ctx.lineWidth = 2; ctx.beginPath();
|
|
741
|
+
for (let k = 0; k <= 60 * ring; k++) { const a = -1.4 + k / 60 * 6.6, r = 1 + Math.sin(k * 0.3) * 0.02; const px = 300 + Math.cos(a) * 165 * r, py = 400 + Math.sin(a) * 105 * r; k ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }
|
|
742
|
+
ctx.stroke(); ctx.restore();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ---- neatline + collar (always present: it is the sheet) ----
|
|
746
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1.2; ctx.strokeRect(MX0 + 0.5, MY0 + 0.5, mw, mh);
|
|
747
|
+
ctx.lineWidth = 0.6; ctx.strokeRect(MX0 - 3.5, MY0 - 3.5, mw + 8, mh + 8);
|
|
748
|
+
ctx.font = '400 9px "Inter"'; ctx.fillStyle = INKC; ctx.textAlign = 'center';
|
|
749
|
+
spacedText(ctx, 'THE MODEL SURVEY', 489, 16, 2, 'center');
|
|
750
|
+
spacedText(ctx, 'TRAINING SERIES', 1093, 16, 2, 'center');
|
|
751
|
+
const cy0 = MY1 + 10;
|
|
752
|
+
ctx.textAlign = 'left'; ctx.font = '400 9px "Inter"';
|
|
753
|
+
ctx.fillText('Each × is one labelled example', MX0, cy0 + 18);
|
|
754
|
+
ctx.fillText('Elevation = how wrong the model is', MX0, cy0 + 31);
|
|
755
|
+
ctx.fillText('Red dashes: the path of training', MX0, cy0 + 44);
|
|
756
|
+
ctx.fillText('Illustrative map. Not for navigation', MX0, cy0 + 57);
|
|
757
|
+
// sheet furniture from the still: graticule ticks and corner coordinates, north arrow, scale bar
|
|
758
|
+
ctx.lineWidth = 1;
|
|
759
|
+
for (let k = 1; k < 4; k++) {
|
|
760
|
+
const x = MX0 + mw * k / 4;
|
|
761
|
+
ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY0 + 8); ctx.moveTo(x, MY1); ctx.lineTo(x, MY1 - 8); ctx.stroke();
|
|
762
|
+
}
|
|
763
|
+
for (let k = 1; k < 3; k++) {
|
|
764
|
+
const y = MY0 + mh * k / 3;
|
|
765
|
+
ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX0 + 8, y); ctx.moveTo(MX1, y); ctx.lineTo(MX1 - 8, y); ctx.stroke();
|
|
766
|
+
}
|
|
767
|
+
ctx.textAlign = 'left'; ctx.fillText('37°52′30″ 122°22′30″', MX0, MY0 - 9);
|
|
768
|
+
ctx.textAlign = 'right'; ctx.fillText('122°12′30″', MX1, MY0 - 9);
|
|
769
|
+
{
|
|
770
|
+
const ax = 380, ay = cy0 + 64;
|
|
771
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax, ay - 44); ctx.stroke();
|
|
772
|
+
ctx.save(); ctx.translate(ax, ay - 49); ctx.beginPath();
|
|
773
|
+
for (let q = 0; q < 10; q++) { const r = q % 2 ? 2.2 : 5.5, a = -Math.PI / 2 + q * Math.PI / 5; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); }
|
|
774
|
+
ctx.closePath(); ctx.fill(); ctx.restore();
|
|
775
|
+
const mxp = ax + Math.sin(0.24) * 40, myp = ay - Math.cos(0.24) * 40;
|
|
776
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(mxp, myp); ctx.stroke();
|
|
777
|
+
ctx.beginPath(); ctx.moveTo(mxp, myp); ctx.lineTo(mxp - 1.5, myp + 10); ctx.lineTo(mxp + 3, myp + 9); ctx.closePath(); ctx.fill();
|
|
778
|
+
ctx.textAlign = 'left'; ctx.fillText('MN', mxp + 4, myp + 4); ctx.fillText('14°', ax + 5, ay - 6);
|
|
779
|
+
}
|
|
780
|
+
{
|
|
781
|
+
const sx = 520, sy = cy0 + 39, unit = 60;
|
|
782
|
+
ctx.font = '700 13px "Libre Baskerville"'; spacedText(ctx, 'SCALE 1:24 000', sx + unit * 1.5, cy0 + 21, 1.5, 'center');
|
|
783
|
+
ctx.lineWidth = 0.8;
|
|
784
|
+
for (let k = 0; k < 4; k++) { ctx.fillStyle = k % 2 ? INKC : PAPER; ctx.fillRect(sx - unit + k * unit / 4, sy, unit / 4, 5); }
|
|
785
|
+
for (let k = 0; k < 3; k++) { ctx.fillStyle = k % 2 ? PAPER : INKC; ctx.fillRect(sx + k * unit, sy, unit, 5); }
|
|
786
|
+
ctx.strokeRect(sx - unit, sy, unit * 4, 5);
|
|
787
|
+
ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"'; ctx.textAlign = 'center';
|
|
788
|
+
['1', '0', '1', '2', '3'].forEach((q, k) => ctx.fillText(q, sx - unit + k * unit, sy + 16));
|
|
789
|
+
ctx.fillText('KILOMETERS', sx + unit * 3 + 34, sy + 5);
|
|
790
|
+
ctx.font = '400 10px "Libre Baskerville"';
|
|
791
|
+
spacedText(ctx, 'CONTOUR INTERVAL 40 METERS', sx + unit * 1, sy + 32, 1.2, 'center');
|
|
792
|
+
}
|
|
793
|
+
// ---- age: fold crease, foxing (the still's pass, so every frame shares the aged sheet) ----
|
|
794
|
+
{
|
|
795
|
+
ctx.save();
|
|
796
|
+
const fx = 640;
|
|
797
|
+
const gfold = ctx.createLinearGradient(fx - 10, 0, fx + 10, 0);
|
|
798
|
+
gfold.addColorStop(0, 'rgba(255,255,255,0)'); gfold.addColorStop(0.45, 'rgba(255,255,255,0.18)');
|
|
799
|
+
gfold.addColorStop(0.55, 'rgba(90,70,40,0.10)'); gfold.addColorStop(1, 'rgba(90,70,40,0)');
|
|
800
|
+
ctx.fillStyle = gfold; ctx.fillRect(fx - 10, -20, 20, H_ + 40);
|
|
801
|
+
ctx.globalCompositeOperation = 'multiply';
|
|
802
|
+
const fr = H.rng(77);
|
|
803
|
+
for (let k = 0; k < 7; k++) {
|
|
804
|
+
const x = fr.range(0, W), y = fr.range(0, H_), r = fr.range(30, 140);
|
|
805
|
+
const gg = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
806
|
+
gg.addColorStop(0, 'rgba(180,150,100,0.10)'); gg.addColorStop(0.8, 'rgba(180,150,100,0.04)'); gg.addColorStop(1, 'rgba(180,150,100,0)');
|
|
807
|
+
ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
|
|
808
|
+
}
|
|
809
|
+
ctx.restore();
|
|
810
|
+
}
|
|
811
|
+
ctx.restore(); // camera
|
|
812
|
+
|
|
813
|
+
// ---- the question: a big cartouche title that docks into the collar title block ----
|
|
814
|
+
const dock = H.ease.inOut(seg(1.5, 2.4));
|
|
815
|
+
{
|
|
816
|
+
const x = H.lerp(640, MX1, dock), y = H.lerp(350, MY1 + 40, dock), size = H.lerp(64, 28, dock);
|
|
817
|
+
ctx.save();
|
|
818
|
+
ctx.globalAlpha = seg(0, 0.4) * (1 - seg(1.4, 1.8));
|
|
819
|
+
ctx.fillStyle = 'rgba(238,229,203,0.96)'; ctx.fillRect(170, 236, 940, 190);
|
|
820
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(170.5, 236.5, 940, 190); ctx.lineWidth = 0.8; ctx.strokeRect(177.5, 243.5, 926, 176);
|
|
821
|
+
ctx.globalAlpha = seg(0.1, 0.6);
|
|
822
|
+
ctx.fillStyle = INKC; ctx.font = `700 ${size}px "Libre Baskerville"`;
|
|
823
|
+
spacedText(ctx, 'HOW DOES AI LEARN?', x, y, H.lerp(5, 3, dock), dock > 0.5 ? 'right' : 'center');
|
|
824
|
+
ctx.globalAlpha = seg(0.5, 0.9) * (1 - seg(1.3, 1.6));
|
|
825
|
+
ctx.font = 'italic 400 28px "Libre Baskerville"';
|
|
826
|
+
spacedText(ctx, 'A survey of the loss landscape', 640, 398, 1, 'center');
|
|
827
|
+
ctx.globalAlpha = dock;
|
|
828
|
+
ctx.font = '400 10px "Libre Baskerville"';
|
|
829
|
+
spacedText(ctx, 'LOSS LANDSCAPE QUADRANGLE', MX1, MY1 + 57, 1.6, 'right');
|
|
830
|
+
ctx.restore();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---- cartouches (screen-fixed inset boxes, printed in the map's own type) ----
|
|
834
|
+
const box = (x, y, w, h) => {
|
|
835
|
+
ctx.fillStyle = 'rgba(238,229,203,0.97)'; ctx.fillRect(x, y, w, h);
|
|
836
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(x + 0.5, y + 0.5, w, h); ctx.lineWidth = 0.7; ctx.strokeRect(x + 6.5, y + 6.5, w - 12, h - 12);
|
|
837
|
+
};
|
|
838
|
+
const exA = seg(2.0, 2.3) * (1 - seg(6.2, 6.6));
|
|
839
|
+
if (exA > 0) {
|
|
840
|
+
ctx.save(); ctx.globalAlpha = exA; box(50, 44, 420, 136);
|
|
841
|
+
ctx.fillStyle = INKC; ctx.font = '700 28px "Libre Baskerville"'; spacedText(ctx, 'EXAMPLES SURVEYED', 74, 86, 1, 'left');
|
|
842
|
+
const n = Math.round(1e6 * H.ease.in(seg(2.0, 5.6)) ** 0.8);
|
|
843
|
+
ctx.font = '700 66px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillText(n.toLocaleString('en-US'), 70, 158);
|
|
844
|
+
ctx.restore();
|
|
845
|
+
}
|
|
846
|
+
const lgA = seg(6.8, 7.1) * (1 - seg(10.8, 11.1));
|
|
847
|
+
if (lgA > 0) {
|
|
848
|
+
ctx.save(); ctx.globalAlpha = lgA; box(50, 44, 600, 80);
|
|
849
|
+
const steps = ['GUESS', 'CHECK', 'ADJUST'];
|
|
850
|
+
const active = t < 7.6 ? 0 : t < 8.4 ? 1 : t < 9.5 ? 2 : 0;
|
|
851
|
+
let x = 76;
|
|
852
|
+
steps.forEach((s, i) => {
|
|
853
|
+
ctx.font = '700 28px "Libre Baskerville"';
|
|
854
|
+
ctx.fillStyle = i === active ? RED : 'rgba(30,26,22,0.4)';
|
|
855
|
+
const w = spacedText(ctx, s, x, 96, 3, 'left');
|
|
856
|
+
if (i === active) { ctx.fillRect(x, 104, w, 3); }
|
|
857
|
+
x += w + 16;
|
|
858
|
+
if (i < 2) { ctx.fillStyle = 'rgba(30,26,22,0.6)'; ctx.font = '400 28px "Libre Baskerville"'; ctx.fillText('→', x, 96); x += 46; }
|
|
859
|
+
});
|
|
860
|
+
ctx.restore();
|
|
861
|
+
}
|
|
862
|
+
const acA = seg(10.8, 11.2);
|
|
863
|
+
if (acA > 0) {
|
|
864
|
+
ctx.save(); ctx.globalAlpha = acA; box(50, 44, 470, 176);
|
|
865
|
+
ctx.fillStyle = INKC; ctx.font = '700 28px "Libre Baskerville"'; spacedText(ctx, 'ACCURACY', 74, 84, 2, 'left');
|
|
866
|
+
const p = H.ease.inOut(seg(10.9, 12.8));
|
|
867
|
+
const acc = Math.round(61 + 36 * p);
|
|
868
|
+
ctx.font = '700 80px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillStyle = p >= 1 ? RED : INKC; ctx.fillText(acc + '%', 70, 164);
|
|
869
|
+
const px0 = 300, py0 = 192, pw = 196, ph = 110;
|
|
870
|
+
ctx.strokeStyle = 'rgba(30,26,22,0.4)'; ctx.lineWidth = 1;
|
|
871
|
+
ctx.beginPath(); ctx.moveTo(px0, py0); ctx.lineTo(px0 + pw, py0); ctx.moveTo(px0, py0); ctx.lineTo(px0, py0 - ph); ctx.stroke();
|
|
872
|
+
ctx.strokeStyle = RED; ctx.lineWidth = 3; ctx.beginPath();
|
|
873
|
+
for (let k = 0; k <= 40 * p; k++) { const u = k / 40, v = 1 - Math.exp(-u * 4); const xx = px0 + u * pw, yy = py0 - 8 - v * (ph - 18); k ? ctx.lineTo(xx, yy) : ctx.moveTo(xx, yy); }
|
|
874
|
+
ctx.stroke();
|
|
875
|
+
ctx.font = 'italic 400 24px "Libre Baskerville"'; ctx.fillStyle = INKC; ctx.textAlign = 'right'; ctx.fillText('rounds →', px0 + pw + 4, py0 + 23); ctx.textAlign = 'left';
|
|
876
|
+
ctx.font = 'italic 400 28px "Libre Baskerville"'; ctx.fillText('from 61%', 74, 204);
|
|
877
|
+
if (p >= 1) { const u = seg(12.8, 13.1); ctx.fillStyle = RED; ctx.fillRect(72, 174, 190 * u, 3.5); }
|
|
878
|
+
ctx.restore();
|
|
879
|
+
}
|
|
880
|
+
const tk = seg(13.2, 13.7);
|
|
881
|
+
if (tk > 0) {
|
|
882
|
+
ctx.save(); ctx.globalAlpha = tk;
|
|
883
|
+
const y = 600 + (1 - H.ease.out(tk)) * 12;
|
|
884
|
+
ctx.fillStyle = 'rgba(238,229,203,0.97)'; ctx.fillRect(150, y - 58, 980, 92);
|
|
885
|
+
ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(150.5, y - 57.5, 980, 92); ctx.lineWidth = 0.7; ctx.strokeRect(157.5, y - 50.5, 966, 78);
|
|
886
|
+
ctx.fillStyle = INKC; ctx.font = '700 40px "Libre Baskerville"';
|
|
887
|
+
spacedText(ctx, 'MORE EXAMPLES → BETTER GUESSES', 640, y + 2, 2, 'center');
|
|
888
|
+
ctx.restore();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
H.vignette(ctx, W, H_, 0.18, '80,60,30');
|
|
892
|
+
H.grain(ctx, W, H_, 0.035, 31 + Math.floor(t * 12));
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
}
|