@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,903 @@
|
|
|
1
|
+
{
|
|
2
|
+
// ---------------------------------------------------------------
|
|
3
|
+
// Sumi-e — ink wash on rice paper
|
|
4
|
+
// ---------------------------------------------------------------
|
|
5
|
+
const INK = [22, 20, 19];
|
|
6
|
+
|
|
7
|
+
// resample a polyline to a fixed spacing; pts = [[x,y,p], ...]
|
|
8
|
+
function resample(pts, step) {
|
|
9
|
+
const out = [pts[0]];
|
|
10
|
+
let acc = 0;
|
|
11
|
+
for (let i = 1; i < pts.length; i++) {
|
|
12
|
+
const a = pts[i - 1], b = pts[i], d = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
13
|
+
let tt = step - acc;
|
|
14
|
+
while (tt <= d) { const u = tt / d; out.push([a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u]); tt += step; }
|
|
15
|
+
acc = (acc + d) % step;
|
|
16
|
+
}
|
|
17
|
+
out.push(pts[pts.length - 1]);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
// smooth Catmull-Rom through control points [[x,y,p]...]
|
|
21
|
+
function spline(cp, n = 12) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (let i = 0; i < cp.length - 1; i++) {
|
|
24
|
+
const p0 = cp[Math.max(0, i - 1)], p1 = cp[i], p2 = cp[i + 1], p3 = cp[Math.min(cp.length - 1, i + 2)];
|
|
25
|
+
for (let k = 0; k < n; k++) {
|
|
26
|
+
const t = k / n, t2 = t * t, t3 = t2 * t;
|
|
27
|
+
const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
|
|
28
|
+
out.push([f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1]), f(p0[2], p1[2], p2[2], p3[2])]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
out.push(cp[cp.length - 1]);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// One loaded-brush stroke: a solid wet body with ragged edges, then "flying white" — the dry-brush streaks
|
|
36
|
+
// where individual bristles ran out of ink — erased out of it, more and more toward the tail.
|
|
37
|
+
let TMP = null;
|
|
38
|
+
function brush(c, cp, o, nz) {
|
|
39
|
+
const { w = 10, ink = 1, dry = 0.9, seed = 1, tone = 1, pool = 0.35, rim = 0.3, p: prog = 1 } = o;
|
|
40
|
+
if (prog <= 0) return;
|
|
41
|
+
const r = H.rng(seed);
|
|
42
|
+
const pts = resample(spline(cp), 2);
|
|
43
|
+
const N = pts.length;
|
|
44
|
+
if (N < 2) return;
|
|
45
|
+
const Np = prog >= 1 ? N : Math.floor(N * prog); // the brush is still travelling: only lay what it has covered
|
|
46
|
+
if (Np < 2) return;
|
|
47
|
+
const nrm = pts.map((p, i) => {
|
|
48
|
+
const a = pts[Math.max(0, i - 1)], b = pts[Math.min(N - 1, i + 1)];
|
|
49
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1;
|
|
50
|
+
return [-dy / l, dx / l];
|
|
51
|
+
});
|
|
52
|
+
if (!TMP) TMP = H.layer(1280, 720);
|
|
53
|
+
const tc = TMP.getContext('2d');
|
|
54
|
+
let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
|
|
55
|
+
pts.forEach((p) => { x0 = Math.min(x0, p[0]); y0 = Math.min(y0, p[1]); x1 = Math.max(x1, p[0]); y1 = Math.max(y1, p[1]); });
|
|
56
|
+
const m = w + 4; x0 = Math.max(0, Math.floor(x0 - m)); y0 = Math.max(0, Math.floor(y0 - m)); x1 = Math.min(1280, Math.ceil(x1 + m)); y1 = Math.min(720, Math.ceil(y1 + m));
|
|
57
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
58
|
+
tc.clearRect(x0, y0, x1 - x0, y1 - y0);
|
|
59
|
+
const ph = r() * 100;
|
|
60
|
+
const L = [], Rr = [];
|
|
61
|
+
for (let i = 0; i < Np; i++) {
|
|
62
|
+
const head = prog >= 1 ? 1 : Math.min(1, (Np - i) / 5);
|
|
63
|
+
const p = pts[i], hw = Math.max(0.3, w * p[2] * 0.5 * head * (1 + nz.n2(ph + i * 0.06, 3.3) * 0.16));
|
|
64
|
+
L.push([p[0] + nrm[i][0] * hw, p[1] + nrm[i][1] * hw]); Rr.push([p[0] - nrm[i][0] * hw, p[1] - nrm[i][1] * hw]);
|
|
65
|
+
}
|
|
66
|
+
tc.save();
|
|
67
|
+
tc.fillStyle = `rgb(${INK})`; tc.strokeStyle = `rgb(${INK})`;
|
|
68
|
+
tc.globalAlpha = Math.min(1, ink) * 0.9 * tone;
|
|
69
|
+
tc.beginPath(); tc.moveTo(L[0][0], L[0][1]); for (let i = 1; i < Np; i++) tc.lineTo(L[i][0], L[i][1]);
|
|
70
|
+
for (let i = Np - 1; i >= 0; i--) tc.lineTo(Rr[i][0], Rr[i][1]); tc.closePath(); tc.fill();
|
|
71
|
+
if (rim > 0) { tc.globalAlpha = rim * tone; tc.lineWidth = 1.1; tc.stroke(); }
|
|
72
|
+
if (pool > 0) {
|
|
73
|
+
const p0 = pts[0], p1 = pts[Math.min(N - 1, 3)];
|
|
74
|
+
tc.globalAlpha = pool * tone;
|
|
75
|
+
tc.beginPath(); tc.ellipse((p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, w * p0[2] * 0.48, w * p0[2] * 0.4, Math.atan2(p1[1] - p0[1], p1[0] - p0[0]), 0, 7); tc.fill();
|
|
76
|
+
}
|
|
77
|
+
// flying white
|
|
78
|
+
tc.globalCompositeOperation = 'destination-out'; tc.lineCap = 'butt'; tc.lineJoin = 'round';
|
|
79
|
+
const nb = Math.max(4, Math.min(64, Math.round(w * 1.4)));
|
|
80
|
+
const step = 2;
|
|
81
|
+
for (let b = 0; b < nb; b++) {
|
|
82
|
+
const off = ((b + r()) / nb) * 2 - 1, edge = Math.abs(off);
|
|
83
|
+
const start = r() * 0.25 - (ink - 1) * 0.6, rate = dry * (0.5 + edge * edge * 1.1 + r() * 0.9);
|
|
84
|
+
const lw = Math.max(0.5, (w / nb) * (0.5 + r() * 0.9));
|
|
85
|
+
const ph2 = r() * 100;
|
|
86
|
+
let run = null, sumA = 0;
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (run && run.length > 1) { tc.globalAlpha = H.clamp(sumA / run.length); tc.lineWidth = lw; tc.beginPath(); tc.moveTo(run[0][0], run[0][1]); for (let k = 1; k < run.length; k++) tc.lineTo(run[k][0], run[k][1]); tc.stroke(); }
|
|
89
|
+
run = null; sumA = 0;
|
|
90
|
+
};
|
|
91
|
+
for (let i = 0; i < Np; i += step) {
|
|
92
|
+
const s = i / N;
|
|
93
|
+
const dness = start + s * rate + nz.n2(ph2 + i * 0.05, b * 0.53) * 0.4;
|
|
94
|
+
if (dness < 0.5) { flush(); continue; }
|
|
95
|
+
const p = pts[i], h = w * p[2] * 0.5 * off;
|
|
96
|
+
const pos = [p[0] + nrm[i][0] * h, p[1] + nrm[i][1] * h];
|
|
97
|
+
if (!run) run = [];
|
|
98
|
+
run.push(pos); sumA += (dness - 0.5) * 3;
|
|
99
|
+
if (run.length >= 8) { flush(); run = [pos]; sumA = (dness - 0.5) * 3; }
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
}
|
|
103
|
+
tc.restore();
|
|
104
|
+
c.drawImage(TMP, x0, y0, x1 - x0, y1 - y0, x0, y0, x1 - x0, y1 - y0);
|
|
105
|
+
return [x0, y0, x1, y1];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Kozo rice paper, shared by the still and the scroll: cloudy fibre formation (mottle), a fine pulp grain,
|
|
109
|
+
// faint old sizing stains, and hair-like fibres -- the light ones kept subtle so they read as fibre, not scratches.
|
|
110
|
+
function makePaper(w, h, nz, R, nFib) {
|
|
111
|
+
const pw = H.layer(w, h), p = pw.getContext('2d');
|
|
112
|
+
p.fillStyle = '#efe7d6'; p.fillRect(0, 0, w, h);
|
|
113
|
+
const cw = Math.ceil(w / 20), ch = Math.ceil(h / 20);
|
|
114
|
+
const big = H.layer(cw, ch), bx = big.getContext('2d');
|
|
115
|
+
for (let j = 0; j < ch; j++) for (let i = 0; i < cw; i++) { const v = nz.fbm2(i * 0.12, j * 0.12, 3); bx.fillStyle = v > 0 ? `rgba(255,252,242,${Math.min(1, v * 0.8)})` : `rgba(176,160,128,${Math.min(1, -v * 0.45)})`; bx.fillRect(i, j, 1, 1); }
|
|
116
|
+
p.imageSmoothingEnabled = true; p.drawImage(big, 0, 0, w, h);
|
|
117
|
+
// flocculation: finer clouds of denser pulp (the formation you see holding washi up to light)
|
|
118
|
+
const fw = Math.ceil(w / 6), fh = Math.ceil(h / 6), fl = H.layer(fw, fh), fx = fl.getContext('2d'), fi = fx.createImageData(fw, fh), fd = fi.data;
|
|
119
|
+
for (let j = 0; j < fh; j++) for (let i = 0; i < fw; i++) { const v = nz.fbm2(i * 0.11 + 40, j * 0.11 + 17, 3), k = (j * fw + i) * 4; if (v > 0) { fd[k] = 255; fd[k + 1] = 253; fd[k + 2] = 246; fd[k + 3] = Math.min(255, v * 90); } else { fd[k] = 170; fd[k + 1] = 152; fd[k + 2] = 120; fd[k + 3] = Math.min(255, -v * 40); } }
|
|
120
|
+
fx.putImageData(fi, 0, 0); p.drawImage(fl, 0, 0, w, h);
|
|
121
|
+
// pulp grain
|
|
122
|
+
const gi = p.getImageData(0, 0, w, h), gd = gi.data, gr = H.rng(31337);
|
|
123
|
+
for (let i = 0; i < gd.length; i += 4) { const v = (gr() - 0.5) * 9 + (gr() < 0.004 ? -26 * gr() : 0); gd[i] += v; gd[i + 1] += v; gd[i + 2] += v * 0.9; }
|
|
124
|
+
p.putImageData(gi, 0, 0);
|
|
125
|
+
p.lineCap = 'round';
|
|
126
|
+
for (let i = 0; i < nFib; i++) {
|
|
127
|
+
const x = R() * w, y = R() * h, len = R.range(10, 70), a = R() * Math.PI * 2, bend = R.gauss() * 0.6, dark = R.chance(0.45);
|
|
128
|
+
p.strokeStyle = dark ? `rgba(150,130,100,${R.range(0.07, 0.2)})` : `rgba(255,253,246,${R.range(0.12, 0.3)})`; p.lineWidth = R.range(0.4, 1.3);
|
|
129
|
+
p.beginPath(); p.moveTo(x, y); p.quadraticCurveTo(x + Math.cos(a + bend) * len * 0.5, y + Math.sin(a + bend) * len * 0.5, x + Math.cos(a) * len, y + Math.sin(a) * len); p.stroke();
|
|
130
|
+
}
|
|
131
|
+
return pw;
|
|
132
|
+
}
|
|
133
|
+
let HERO_PAPER = null, HERO_PAPER_KEY = '';
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// =================================================================
|
|
137
|
+
// EXPLAINER — "How does AI learn?" told along a handscroll: the camera unrolls it right-to-left,
|
|
138
|
+
// a drift of mist passes between scenes, everything is brush-written in ink with vermilion accents.
|
|
139
|
+
// =================================================================
|
|
140
|
+
const SXW = 3400;
|
|
141
|
+
let SXC = null;
|
|
142
|
+
const lerp = (a, b, u) => a + (b - a) * u;
|
|
143
|
+
const cl = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
144
|
+
const ez = (u) => H.ease.inOut(cl(u));
|
|
145
|
+
const win = (t, a, b) => cl((t - a) / (b - a));
|
|
146
|
+
const VERM = [184, 50, 42];
|
|
147
|
+
const fmt = (n) => Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
148
|
+
|
|
149
|
+
function sumiCache() {
|
|
150
|
+
if (SXC) return SXC;
|
|
151
|
+
const C = {}, nz = H.makeNoise(808), R = H.rng(4242);
|
|
152
|
+
C.nz = nz;
|
|
153
|
+
// --- the scroll: rice paper with fibres, mottled, long enough to pan across ---
|
|
154
|
+
const pw = makePaper(SXW, 720, nz, R, 2600);
|
|
155
|
+
// --- a far landscape painted faintly along the scroll: pale graded washes of distant peaks dissolving
|
|
156
|
+
// into mist, the same treatment as the still's far range, kept light so the story reads over it ---
|
|
157
|
+
{
|
|
158
|
+
const pk = (x, c, h, sd) => h * Math.exp(-Math.pow((x - c) / sd, 2));
|
|
159
|
+
const range = (x0, x1, ridge, tone, fade, mistY, mistR, seed) => {
|
|
160
|
+
const w = x1 - x0, tops = new Float32Array(w); let ymin = 720;
|
|
161
|
+
for (let i = 0; i < w; i++) { tops[i] = ridge(x0 + i); ymin = Math.min(ymin, tops[i]); }
|
|
162
|
+
ymin = Math.max(0, Math.floor(ymin) - 4); const h = 720 - ymin;
|
|
163
|
+
const L = H.layer(w, h), lc = L.getContext('2d'), img = lc.createImageData(w, h), d = img.data;
|
|
164
|
+
for (let i = 0; i < w; i++) {
|
|
165
|
+
const x = x0 + i, top = tops[i], m = mistY + nz.n2(x * 0.006, seed) * mistR * 0.8, hf = H.smooth(x0, x0 + 160, x) * H.smooth(x1, x1 - 160, x);
|
|
166
|
+
for (let y = Math.max(ymin, Math.floor(top)); y < 720; y++) {
|
|
167
|
+
const dy = y - top;
|
|
168
|
+
let al = (0.3 + 0.7 * Math.exp(-dy / fade) + 0.18 * Math.exp(-dy / 4)) * H.smooth(m + mistR, m - mistR * 0.4, y) * hf;
|
|
169
|
+
al *= 0.75 + 0.5 * H.clamp(nz.fbm2(x * 0.012, y * 0.02 + seed, 3) + 0.5);
|
|
170
|
+
if (dy < 2) al *= Math.max(0, dy / 2);
|
|
171
|
+
if (al < 0.003 && dy > 4 && y > m) break;
|
|
172
|
+
const k = ((y - ymin) * w + i) * 4; d[k] = INK[0]; d[k + 1] = INK[1]; d[k + 2] = INK[2]; d[k + 3] = Math.min(255, al * tone * 255);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lc.putImageData(img, 0, 0);
|
|
176
|
+
p.save(); p.globalCompositeOperation = 'multiply'; p.filter = 'blur(1.6px)'; p.drawImage(L, x0, ymin); p.restore();
|
|
177
|
+
};
|
|
178
|
+
const p = pw.getContext('2d');
|
|
179
|
+
// low hills under the river scene and the check
|
|
180
|
+
range(420, 1560, (x) => 640 - pk(x, 760, 120, 170) - pk(x, 900, 70, 90) - pk(x, 1240, 95, 160) - pk(x, 1380, 60, 70) + nz.fbm2(x * 0.016, 2.2, 4) * 26 + Math.abs(nz.n2(x * 0.05, 4)) * 10, 0.16, 70, 610, 55, 3);
|
|
181
|
+
range(1820, 2300, (x) => 690 - pk(x, 2000, 120, 110) - pk(x, 2120, 80, 70) + nz.fbm2(x * 0.018, 5.5, 4) * 20, 0.14, 60, 650, 45, 7);
|
|
182
|
+
// a tall far peak beside the summit of the chart
|
|
183
|
+
range(3010, 3400, (x) => 620 - pk(x, 3220, 300, 120) - pk(x, 3150, 170, 60) - pk(x, 3300, 210, 50) + nz.fbm2(x * 0.016, 8.8, 4) * 30 + Math.abs(nz.n2(x * 0.05, 9)) * 14, 0.2, 130, 560, 70, 11);
|
|
184
|
+
}
|
|
185
|
+
C.paper = pw;
|
|
186
|
+
// --- small ink sketches of a cat and a dog (a handful of brush strokes each) ---
|
|
187
|
+
const sketch = (dog) => {
|
|
188
|
+
const cv = H.layer(200, 200), c = cv.getContext('2d');
|
|
189
|
+
const S = (cp, o) => brush(c, cp, Object.assign({ pool: 0.2 }, o), nz);
|
|
190
|
+
if (dog) {
|
|
191
|
+
S([[64, 70, 0.5], [100, 52, 1], [140, 72, 0.8], [146, 120, 0.7], [100, 150, 0.9], [58, 122, 0.6], [62, 76, 0.3]], { w: 9, ink: 1.05, dry: 0.9, seed: 3 });
|
|
192
|
+
S([[66, 70, 1], [46, 104, 0.9], [52, 132, 0.4]], { w: 16, ink: 1.2, dry: 0.5, seed: 4 });
|
|
193
|
+
S([[138, 70, 1], [158, 104, 0.9], [152, 132, 0.4]], { w: 16, ink: 1.2, dry: 0.5, seed: 5 });
|
|
194
|
+
S([[96, 118, 1], [104, 120, 0.9]], { w: 12, ink: 1.4, dry: 0.1, seed: 6, pool: 0.6 });
|
|
195
|
+
} else {
|
|
196
|
+
S([[62, 92, 0.5], [70, 60, 0.8], [100, 56, 1], [134, 62, 0.8], [144, 100, 0.8], [124, 140, 0.9], [84, 142, 0.8], [60, 104, 0.4]], { w: 9, ink: 1.05, dry: 0.9, seed: 13 });
|
|
197
|
+
S([[66, 70, 0.9], [64, 34, 0.5], [90, 58, 0.3]], { w: 8, ink: 1.1, dry: 0.8, seed: 14 });
|
|
198
|
+
S([[136, 70, 0.9], [140, 34, 0.5], [112, 58, 0.3]], { w: 8, ink: 1.1, dry: 0.8, seed: 15 });
|
|
199
|
+
for (const s of [-1, 1]) for (let k = 0; k < 2; k++) S([[100 + s * 18, 116 + k * 6, 0.6], [100 + s * 50, 108 + k * 12, 0.2]], { w: 2.2, ink: 1, dry: 0.6, seed: 16 + k + (s > 0 ? 2 : 0), pool: 0 });
|
|
200
|
+
}
|
|
201
|
+
S([[84, 98, 1], [86, 101, 0.9]], { w: 8, ink: 1.4, dry: 0.1, seed: 21, pool: 0.6 });
|
|
202
|
+
S([[116, 98, 1], [118, 101, 0.9]], { w: 8, ink: 1.4, dry: 0.1, seed: 22, pool: 0.6 });
|
|
203
|
+
return cv;
|
|
204
|
+
};
|
|
205
|
+
C.cat = sketch(false); C.dog = sketch(true);
|
|
206
|
+
// --- slips of washi for the pictures: deckled torn edges, the paper's own fibres, a soft lift shadow ---
|
|
207
|
+
const slip = (img, sz, seed) => {
|
|
208
|
+
const pad = 12, cv = H.layer(sz + pad * 2, sz + pad * 2), c = cv.getContext('2d'), sr = H.rng(seed);
|
|
209
|
+
const path = new Path2D(), cs = [[pad, pad], [pad + sz, pad], [pad + sz, pad + sz], [pad, pad + sz]], nseg = Math.round(sz / 4);
|
|
210
|
+
for (let e = 0; e < 4; e++) { const [ax, ay] = cs[e], [bx2, by2] = cs[(e + 1) % 4], nx = (by2 - ay) / sz, ny = -(bx2 - ax) / sz;
|
|
211
|
+
for (let k = 0; k < nseg; k++) { const u = k / nseg, j = (nz.n2(seed + e * 3.1, k * 0.35) * 1.1 + sr.gauss() * 0.35) * Math.min(1, u * 8, (1 - u) * 8 + 0.15); const x = ax + (bx2 - ax) * u + nx * j, y = ay + (by2 - ay) * u + ny * j; e === 0 && k === 0 ? path.moveTo(x, y) : path.lineTo(x, y); } }
|
|
212
|
+
path.closePath();
|
|
213
|
+
c.save(); c.shadowColor = 'rgba(60,50,30,0.26)'; c.shadowBlur = 7; c.shadowOffsetX = 1.5; c.shadowOffsetY = 3; c.fillStyle = 'rgb(248,244,233)'; c.fill(path); c.restore();
|
|
214
|
+
c.save(); c.clip(path);
|
|
215
|
+
c.globalAlpha = 0.5; c.drawImage(C.paper, 300 + sr() * 2600, sr() * 300, sz, sz, pad, pad, sz, sz); c.globalAlpha = 1;
|
|
216
|
+
const vg = c.createRadialGradient(pad + sz / 2, pad + sz / 2, sz * 0.3, pad + sz / 2, pad + sz / 2, sz * 0.75); vg.addColorStop(0, 'rgba(255,255,255,0)'); vg.addColorStop(1, 'rgba(150,130,100,0.10)');
|
|
217
|
+
c.fillStyle = vg; c.fillRect(0, 0, cv.width, cv.height);
|
|
218
|
+
c.globalCompositeOperation = 'multiply'; c.drawImage(img, pad, pad, sz, sz);
|
|
219
|
+
c.restore();
|
|
220
|
+
// torn edge: the long fibres feather out lighter, with a faint grey where the slip curls up from the scroll
|
|
221
|
+
c.save(); c.strokeStyle = 'rgba(255,253,246,0.7)'; c.lineWidth = 1.4; c.stroke(path);
|
|
222
|
+
c.strokeStyle = 'rgba(120,104,80,0.16)'; c.lineWidth = 0.7; c.translate(0.4, 0.6); c.stroke(path); c.restore();
|
|
223
|
+
c.lineCap = 'round';
|
|
224
|
+
for (let k = 0; k < sz * 0.5; k++) { const e = sr() * 4 | 0, u = sr(), [ax, ay] = cs[e], [bx2, by2] = cs[(e + 1) % 4], nx = (by2 - ay) / sz, ny = -(bx2 - ax) / sz, x = ax + (bx2 - ax) * u, y = ay + (by2 - ay) * u, l = sr.range(1.5, 4.5), a = Math.atan2(ny, nx) + sr.gauss() * 0.6;
|
|
225
|
+
c.strokeStyle = `rgba(250,246,236,${sr.range(0.4, 0.8)})`; c.lineWidth = sr.range(0.4, 0.8); c.beginPath(); c.moveTo(x - nx, y - ny); c.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke(); }
|
|
226
|
+
cv.pad = pad; cv.sz = sz;
|
|
227
|
+
return cv;
|
|
228
|
+
};
|
|
229
|
+
C.slipCat = slip(C.cat, 92, 61); C.slipDog = slip(C.dog, 92, 62); C.slipBig = slip(C.cat, 210, 63);
|
|
230
|
+
// --- the river of examples: a pale ink wash laid wet between two banks (revealed as it flows).
|
|
231
|
+
// Pigment pools into a darker tide-line along each bank, the current leaves lighter streaks, the wash is
|
|
232
|
+
// mottled where the water sat unevenly, settles into the paper tooth, and dissolves inside the ensō.
|
|
233
|
+
const rv = H.layer(1300, 400), r = rv.getContext('2d');
|
|
234
|
+
const bank = (x, o) => 200 + Math.sin(x * 0.006 + 0.4) * 50 - x * 0.08 + o + nz.n2(x * 0.02, o) * 6;
|
|
235
|
+
{
|
|
236
|
+
const img = r.createImageData(1300, 400), d = img.data;
|
|
237
|
+
for (let x = 0; x < 1300; x++) {
|
|
238
|
+
const top = bank(x, -40), bot = bank(x, 60), fadeR = H.smooth(1080, 920, x);
|
|
239
|
+
if (fadeR <= 0) continue;
|
|
240
|
+
const y0 = Math.max(0, Math.floor(top - 8)), y1 = Math.min(400, Math.ceil(bot + 8));
|
|
241
|
+
for (let y = y0; y < y1; y++) {
|
|
242
|
+
const e = Math.min(y - top, bot - y); // distance inside the nearest bank
|
|
243
|
+
let al = e < 0 ? Math.exp(e / 1.6) : 1; // a hair of bleed past the bank
|
|
244
|
+
const ee = Math.max(0, e);
|
|
245
|
+
al *= 0.075 + 0.13 * Math.exp(-ee / 4.5) + 0.05 * Math.exp(-ee / 16); // tide-line pooled along the banks
|
|
246
|
+
const flow = nz.n2(x * 0.005, y * 0.07) + 0.5 * nz.n2(x * 0.012 + 9, y * 0.16);
|
|
247
|
+
al *= 0.62 + 0.55 * H.clamp(flow + 0.55); // lighter streaks where the current ran
|
|
248
|
+
al *= 0.75 + 0.5 * H.clamp(nz.fbm2(x * 0.01 + 3, y * 0.025, 3) + 0.5); // uneven water
|
|
249
|
+
const gr = nz.n2(x * 0.45, y * 0.45);
|
|
250
|
+
al *= 0.86 + 0.32 * Math.max(0, gr); // pigment settling into the tooth
|
|
251
|
+
al *= fadeR;
|
|
252
|
+
const k = (y * 1300 + x) * 4; d[k] = INK[0]; d[k + 1] = INK[1]; d[k + 2] = INK[2]; d[k + 3] = Math.min(255, al * 255);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const tmp = H.layer(1300, 400); tmp.getContext('2d').putImageData(img, 0, 0);
|
|
256
|
+
r.filter = 'blur(1.2px)'; r.drawImage(tmp, 0, 0); r.filter = 'none';
|
|
257
|
+
}
|
|
258
|
+
C.river = rv; C.bank = bank;
|
|
259
|
+
// --- a seal stamp ---
|
|
260
|
+
C.seal = (txt, size) => {
|
|
261
|
+
const cv = H.layer(size + 8, size + 8), c = cv.getContext('2d'), sr = H.rng(txt.length * 31 + size);
|
|
262
|
+
c.fillStyle = `rgb(${VERM})`; c.beginPath();
|
|
263
|
+
const s = size, corners = [[4, 4], [4 + s, 4], [4 + s, 4 + s], [4, 4 + s]];
|
|
264
|
+
for (let e = 0; e < 4; e++) { const [ax, ay] = corners[e], [bx2, by2] = corners[(e + 1) % 4]; for (let k = 0; k < 8; k++) { const u = k / 8, x = ax + (bx2 - ax) * u + sr.gauss() * 0.7, y = ay + (by2 - ay) * u + sr.gauss() * 0.7; e === 0 && k === 0 ? c.moveTo(x, y) : c.lineTo(x, y); } }
|
|
265
|
+
c.closePath(); c.fill();
|
|
266
|
+
c.globalCompositeOperation = 'destination-out'; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
267
|
+
c.font = `600 ${Math.round(s * (txt.length > 2 ? 0.5 : 0.62))}px ${/[a-z]/i.test(txt) ? '"Cormorant Garamond"' : '"Noto Serif JP"'}`;
|
|
268
|
+
c.fillText(txt, 4 + s / 2, 4 + s / 2 + 1);
|
|
269
|
+
for (let k = 0; k < s * 2; k++) { c.globalAlpha = sr.range(0.3, 1); c.beginPath(); c.arc(4 + sr() * s, 4 + sr() * s, sr.range(0.3, 1.2), 0, 7); c.fill(); }
|
|
270
|
+
return cv;
|
|
271
|
+
};
|
|
272
|
+
C.sealCat = C.seal('cat', 52); C.sealDog = C.seal('dog', 52); C.sealKnow = C.seal('知', 46);
|
|
273
|
+
SXC = C;
|
|
274
|
+
return C;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ink lettering (Cormorant), written left to right, with a little bleed into the fibres
|
|
278
|
+
function inkText(c, str, x, y, size, { align = 'left', reveal = 1, italic = true, col = '22,20,19', alpha = 0.92, weight = italic ? 400 : 600 } = {}) {
|
|
279
|
+
if (reveal <= 0) return;
|
|
280
|
+
c.save();
|
|
281
|
+
c.font = `${italic ? 'italic ' : ''}${weight} ${size}px "Cormorant Garamond"`; c.textAlign = 'left'; c.textBaseline = 'alphabetic';
|
|
282
|
+
const w = c.measureText(str).width, x0 = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
|
|
283
|
+
if (reveal < 1) { c.beginPath(); c.rect(x0 - 10, y - size * 1.2, (w + 20) * reveal, size * 1.7); c.clip(); }
|
|
284
|
+
c.fillStyle = `rgba(${col},${alpha * 0.3})`; c.filter = 'blur(1.4px)'; c.fillText(str, x0, y); c.filter = 'none';
|
|
285
|
+
c.fillStyle = `rgba(${col},${alpha})`; c.fillText(str, x0, y);
|
|
286
|
+
c.restore();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Ink is laid into a screen-space layer, then pressed into the paper: a soft bleed copy (the ink wicking along
|
|
290
|
+
// the fibres) under the sharp stroke, both multiplied, exactly like the still's strokes(). Only the dirty box is touched.
|
|
291
|
+
let INKL = null, VERL = null, SCR = null;
|
|
292
|
+
const BLEED = [[1.4, 0.5], [-1.2, 1.1], [0.4, -1.5], [-0.9, -0.7], [1.6, -1.2]];
|
|
293
|
+
const grow = (A, b) => (b ? (A ? [Math.min(A[0], b[0]), Math.min(A[1], b[1]), Math.max(A[2], b[2]), Math.max(A[3], b[3])] : b.slice()) : A);
|
|
294
|
+
function pressInk(ctx, L, bb, col) {
|
|
295
|
+
if (!bb) return;
|
|
296
|
+
const x0 = Math.max(0, bb[0] - 6), y0 = Math.max(0, bb[1] - 6), x1 = Math.min(L.width, bb[2] + 6), y1 = Math.min(L.height, bb[3] + 6), w = x1 - x0, h = y1 - y0;
|
|
297
|
+
if (w <= 0 || h <= 0) return;
|
|
298
|
+
const lc = L.getContext('2d');
|
|
299
|
+
if (col) { lc.save(); lc.globalCompositeOperation = 'source-in'; lc.fillStyle = col; lc.fillRect(x0, y0, w, h); lc.restore(); }
|
|
300
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply';
|
|
301
|
+
// bleed: a few faint sub-pixel offset copies (a cheap 1.5px blur -- the canvas blur filter is slow here)
|
|
302
|
+
ctx.globalAlpha = 0.17; for (const [dx, dy] of BLEED) ctx.drawImage(L, x0, y0, w, h, x0 + dx, y0 + dy, w, h);
|
|
303
|
+
ctx.globalAlpha = 1; ctx.drawImage(L, x0, y0, w, h, x0, y0, w, h); ctx.restore();
|
|
304
|
+
lc.clearRect(x0, y0, w, h);
|
|
305
|
+
}
|
|
306
|
+
// chart geometry (world coordinates); shared by the wash cache and the ridge stroke
|
|
307
|
+
const CX0 = 2240, CY0 = 560, CW = 780, CH = 360, CN = 24;
|
|
308
|
+
function chartPts() {
|
|
309
|
+
const pts = [];
|
|
310
|
+
for (let i = 0; i <= CN; i++) { const acc = 61 + 36 * (1 - Math.exp(-i / 5.5)) / (1 - Math.exp(-CN / 5.5)) + (i > 0 && i < CN ? Math.sin(i * 2.7) * 1.6 : 0); pts.push([CX0 + i / CN * CW, CY0 - (acc - 50) / 50 * CH, 0.6 + 0.4 * Math.sin(i * 1.3) ** 2]); }
|
|
311
|
+
return pts;
|
|
312
|
+
}
|
|
313
|
+
// the wash under the ridge: dense and pooled just under the stroke, mottled, settling into the tooth, dissolving
|
|
314
|
+
// toward the baseline and at the right end -- an ink wash rather than a clipped gradient
|
|
315
|
+
let CWASH = null;
|
|
316
|
+
function chartWash(nz) {
|
|
317
|
+
if (CWASH) return CWASH;
|
|
318
|
+
const pts = chartPts(), sp = spline(pts, 16), wx0 = CX0 - 4, wy0 = CY0 - CH - 30, ww = CW + 40, wh = CH + 46;
|
|
319
|
+
const top = new Float32Array(ww).fill(1e9);
|
|
320
|
+
for (let i = 0; i < sp.length - 1; i++) { const a = sp[i], b = sp[i + 1]; for (let x = Math.ceil(a[0]); x <= b[0]; x++) { const u = (x - a[0]) / ((b[0] - a[0]) || 1), xi = x - wx0; if (xi >= 0 && xi < ww) top[xi] = a[1] + (b[1] - a[1]) * u + 3; } }
|
|
321
|
+
const cv = H.layer(ww, wh), c = cv.getContext('2d'), img = c.createImageData(ww, wh), d = img.data;
|
|
322
|
+
for (let xi = 0; xi < ww; xi++) {
|
|
323
|
+
const x = xi + wx0, tp = top[xi]; if (tp > 1e8) continue;
|
|
324
|
+
const hf = H.smooth(CX0 - 2, CX0 + 14, x) * H.smooth(CX0 + CW + 6, CX0 + CW - 50, x);
|
|
325
|
+
for (let yi = Math.max(0, Math.floor(tp - wy0)); yi < wh; yi++) {
|
|
326
|
+
const y = yi + wy0, dy = y - tp;
|
|
327
|
+
let al = 0.05 + 0.2 * Math.exp(-dy / 75) + 0.13 * Math.exp(-dy / 5); // wet edge pooled under the ridge
|
|
328
|
+
al *= H.smooth(CY0 + 2, CY0 - 150, y) * 0.85 + 0.15 * H.smooth(CY0 + 2, CY0 - 20, y);
|
|
329
|
+
al *= 0.7 + 0.6 * H.clamp(nz.fbm2(x * 0.009, y * 0.014 + 50, 3) + 0.5);
|
|
330
|
+
al *= 0.84 + 0.36 * Math.max(0, nz.n2(x * 0.42 + 7, y * 0.42));
|
|
331
|
+
if (dy < 1) al *= Math.max(0, dy);
|
|
332
|
+
al *= hf;
|
|
333
|
+
const k = (yi * ww + xi) * 4; d[k] = INK[0]; d[k + 1] = INK[1]; d[k + 2] = INK[2]; d[k + 3] = Math.min(255, al * 255);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
c.putImageData(img, 0, 0);
|
|
337
|
+
// lighter blooms where the water sat unevenly
|
|
338
|
+
c.globalCompositeOperation = 'destination-out'; const rr = H.rng(173);
|
|
339
|
+
for (let k = 0; k < 14; k++) { const x = rr.range(30, ww - 60), y = rr.range(CH * 0.25, wh - 40), rx = rr.range(30, 80), ry = rr.range(10, 28);
|
|
340
|
+
const g = c.createRadialGradient(x, y, 0, x, y, rx); g.addColorStop(0, `rgba(0,0,0,${rr.range(0.15, 0.35)})`); g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
341
|
+
c.save(); c.translate(x, y); c.scale(1, ry / rx); c.translate(-x, -y); c.fillStyle = g; c.beginPath(); c.arc(x, y, rx, 0, 7); c.fill(); c.restore(); }
|
|
342
|
+
CWASH = { cv, x: wx0, y: wy0 };
|
|
343
|
+
return CWASH;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function explainSumi(ctx, W, H_, t) {
|
|
347
|
+
const C = sumiCache(), nz = C.nz;
|
|
348
|
+
if (!INKL || INKL.width !== W || INKL.height !== H_) { INKL = H.layer(W, H_); VERL = H.layer(W, H_); SCR = H.layer(W, H_); }
|
|
349
|
+
const ic = INKL.getContext('2d'), vc = VERL.getContext('2d');
|
|
350
|
+
let IB = null;
|
|
351
|
+
const flush = () => { pressInk(ctx, INKL, IB, null); IB = null; };
|
|
352
|
+
// vermilion strokes: brushed into their own layer, tinted, pressed in with the same bleed
|
|
353
|
+
// (multiply commutes, so all the vermilion of a frame is pressed in once at the end)
|
|
354
|
+
let VBB = null;
|
|
355
|
+
const verm = (fn) => { const VB = (cp, o) => { VBB = grow(VBB, brush(vc, cp.map(([x, y, p]) => [x - cam, y, p]), o, nz)); }; fn(VB); };
|
|
356
|
+
const cam = Math.round(780 * ez((t - 6.0) / 0.95) + 1300 * ez((t - 10.45) / 0.95));
|
|
357
|
+
ctx.drawImage(C.paper, cam, 0, W, H_, 0, 0, W, H_);
|
|
358
|
+
const X = (x) => x - cam;
|
|
359
|
+
// brush in world coordinates (the brush engine works in screen space)
|
|
360
|
+
const B = (cp, o) => { const pts = cp.map(([x, y, p]) => [x - cam, y, p]); let mn = 1e9, mx = -1e9; pts.forEach((q) => { mn = Math.min(mn, q[0]); mx = Math.max(mx, q[0]); }); if (mx < -60 || mn > W + 60) return; IB = grow(IB, brush(ic, pts, o, nz)); };
|
|
361
|
+
|
|
362
|
+
// ---------- beat 2 scenery: the river of examples flowing into the ensō (the model) ----------
|
|
363
|
+
const ex0 = 1100, ey0 = 330, er = 128; // ensō centre / radius
|
|
364
|
+
if (t > 1.0) {
|
|
365
|
+
const rp = win(t, 1.6, 3.0);
|
|
366
|
+
if (rp > 0) { // the wash spreads along the river with a soft wet front
|
|
367
|
+
const rx0 = Math.max(0, X(-40)), rx1 = Math.min(W, X(-40) + 1300);
|
|
368
|
+
if (rx1 > rx0) {
|
|
369
|
+
const lc = SCR.getContext('2d'); lc.clearRect(rx0, 130, rx1 - rx0, 400); lc.drawImage(C.river, X(-40), 130);
|
|
370
|
+
if (rp < 1) { lc.globalCompositeOperation = 'destination-in'; const f = X(-40) + 1400 * rp, g = lc.createLinearGradient(f - 160, 0, f, 0); g.addColorStop(0, '#000'); g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
371
|
+
lc.fillStyle = g; lc.fillRect(rx0, 130, rx1 - rx0, 400); lc.globalCompositeOperation = 'source-over'; }
|
|
372
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.drawImage(SCR, rx0, 130, rx1 - rx0, 400, rx0, 130, rx1 - rx0, 400); ctx.restore();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const bankL = [], bankR = [];
|
|
376
|
+
for (let x = -40; x <= 1010; x += 70) { bankL.push([x, 130 + C.bank(x + 40, -40), 0.5 + 0.4 * Math.sin(x * 0.01)]); bankR.push([x, 130 + C.bank(x + 40, 60), 0.6 + 0.3 * Math.cos(x * 0.012)]); }
|
|
377
|
+
B(bankL, { w: 7, ink: 1.0, dry: 1.0, seed: 71, tone: 0.8, p: ez((t - 1.6) / 1.2) });
|
|
378
|
+
B(bankR, { w: 9, ink: 1.1, dry: 0.9, seed: 72, tone: 0.85, p: ez((t - 1.8) / 1.2) });
|
|
379
|
+
}
|
|
380
|
+
// the ensō: one breath, one stroke, dry at the tail
|
|
381
|
+
{
|
|
382
|
+
const cp = []; for (let i = 0; i <= 28; i++) { const a = -Math.PI * 0.62 + i / 28 * Math.PI * 1.86; cp.push([ex0 + Math.cos(a) * er * (1 + 0.04 * Math.sin(i)), ey0 + Math.sin(a) * er, i < 3 ? 0.7 + i * 0.1 : 1 - i / 28 * 0.5]); }
|
|
383
|
+
B(cp, { w: 26, ink: 1.3, dry: 0.5, seed: 81, p: ez((t - 1.2) / 0.9) });
|
|
384
|
+
if (t > 2.4) { B([[ex0 - 70, ey0 + 150, 0.4], [ex0 - 100, ey0 + 205, 0.3]], { w: 2, ink: 1, dry: 0.5, seed: 82, pool: 0, p: win(t, 2.4, 2.6) }); inkText(ctx, 'the model', X(ex0 - 100), ey0 + 240, 34, { align: 'center', reveal: win(t, 2.5, 3.0) }); }
|
|
385
|
+
// three reeds inside the circle: the model's "weights"; they swing when it adjusts
|
|
386
|
+
const adj = ez((t - 8.7) / 0.7);
|
|
387
|
+
for (let k = 0; k < 3; k++) {
|
|
388
|
+
if (t < 2.2) break;
|
|
389
|
+
const bx = ex0 - 46 + k * 46, a = -Math.PI / 2 + (k - 1) * 0.5 + adj * (k === 1 ? 0.7 : -0.55) * (k === 0 ? -1 : 1);
|
|
390
|
+
B([[bx, ey0 + 52, 1], [bx + Math.cos(a) * 46, ey0 + 52 + Math.sin(a) * 46, 0.7], [bx + Math.cos(a) * 82, ey0 + 52 + Math.sin(a) * 82, 0.2]], { w: 7, ink: 1.1, dry: 1.1, seed: 90 + k + (adj > 0.5 ? 10 : 0), p: win(t, 2.2 + k * 0.15, 2.5 + k * 0.15) });
|
|
391
|
+
}
|
|
392
|
+
if (t > 8.6 && t < 9.7) { // a ripple of ink spreading from the circle as it adjusts
|
|
393
|
+
const k = win(t, 8.6, 9.7);
|
|
394
|
+
// a soft wet ring of dilute ink with a darker tide-line at its outer edge, thinning as it spreads
|
|
395
|
+
const rr = er + 20 + k * 90, cx = X(ex0), fa = 1 - k, g = ctx.createRadialGradient(cx, ey0, Math.max(0, rr - 12), cx, ey0, rr + 3);
|
|
396
|
+
g.addColorStop(0, 'rgba(22,20,19,0)'); g.addColorStop(0.5, `rgba(22,20,19,${0.06 * fa})`); g.addColorStop(0.8, `rgba(22,20,19,${0.3 * fa})`); g.addColorStop(0.9, `rgba(22,20,19,${0.2 * fa})`); g.addColorStop(1, 'rgba(22,20,19,0)');
|
|
397
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, ey0, rr + 5, 0, 7); ctx.fill(); ctx.restore();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
flush();
|
|
401
|
+
// slips of paper float down the river, each with a vermilion seal saying what it is
|
|
402
|
+
const at = (u) => { const x = lerp(-80, ex0 - 90, u); return [x, 130 + (C.bank(x + 40, -40) + C.bank(x + 40, 60)) / 2 - 6 + Math.sin(u * 9) * 6]; };
|
|
403
|
+
for (let i = 0; i < 22; i++) {
|
|
404
|
+
const s0 = 2.6 + i * 0.16, u = (t - s0) / 1.9;
|
|
405
|
+
if (u <= 0 || u >= 1) continue;
|
|
406
|
+
const [x, y] = at(u), dog = H.rng(i * 23 + 5)() < 0.5, sc = u > 0.82 ? 1 - (u - 0.82) / 0.18 * 0.9 : 1, sz = 92 * sc;
|
|
407
|
+
ctx.save(); ctx.translate(X(x), y - 40); ctx.rotate(Math.sin(i * 1.7 + t) * 0.08); ctx.globalAlpha = sc;
|
|
408
|
+
{ const sl = dog ? C.slipDog : C.slipCat, q = sz / sl.sz, e = (sl.sz / 2 + sl.pad) * q; ctx.drawImage(sl, -e, -e, 2 * e, 2 * e); }
|
|
409
|
+
if (sc > 0.6) { ctx.globalCompositeOperation = 'multiply'; ctx.drawImage(dog ? C.sealDog : C.sealCat, sz / 2 - 30, sz / 2 - 26, 56, 56); }
|
|
410
|
+
ctx.restore();
|
|
411
|
+
}
|
|
412
|
+
if (t > 2.9) {
|
|
413
|
+
const n = t > 5.6 ? 1e6 : Math.round(1e6 * H.ease.in(win(t, 3.0, 5.6)) / 1000) * 1000;
|
|
414
|
+
inkText(ctx, fmt(n), X(120), 646, 68, { italic: false, weight: 600, reveal: win(t, 2.9, 3.2) });
|
|
415
|
+
ctx.save(); ctx.font = '600 68px "Cormorant Garamond"'; const nw = ctx.measureText(fmt(n)).width; ctx.restore();
|
|
416
|
+
inkText(ctx, 'examples', X(120 + 16) + nw, 640, 44, { reveal: win(t, 3.0, 3.4) });
|
|
417
|
+
if (t > 5.6) B([[100, 662, 0.5], [300, 668, 1], [520, 660, 0.4]], { w: 8, ink: 1, dry: 1.4, seed: 99, p: ez((t - 5.6) / 0.35) });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------- beat 3: guess -> check -> adjust ----------
|
|
421
|
+
const PX = 1720, PY = 250;
|
|
422
|
+
if (t > 6.5) {
|
|
423
|
+
const k = win(t, 6.5, 6.9);
|
|
424
|
+
ctx.save(); ctx.globalAlpha = k; ctx.drawImage(C.slipBig, X(PX) - C.slipBig.pad, PY - C.slipBig.pad); ctx.restore();
|
|
425
|
+
inkText(ctx, 'a new picture', X(PX + 105), PY + 250, 32, { align: 'center', reveal: win(t, 6.8, 7.2) });
|
|
426
|
+
}
|
|
427
|
+
const GX = 1440, GY = 140; // where the guess is written
|
|
428
|
+
B([[ex0 + 110, ey0 - 100, 0.8], [ex0 + 170, GY + 40, 1], [GX - 20, GY - 10, 0.5]], { w: 6, ink: 1.1, dry: 1.0, seed: 111, p: ez((t - 6.8) / 0.4) });
|
|
429
|
+
if (t > 7.2) { const a = Math.atan2(-50, 90); B([[GX - 20, GY - 10, 0.6], [GX - 42, GY - 26, 0.2]], { w: 5, ink: 1.1, dry: 0.8, seed: 112, pool: 0 }); B([[GX - 20, GY - 10, 0.6], [GX - 44, GY - 2, 0.2]], { w: 5, ink: 1.1, dry: 0.8, seed: 113, pool: 0 }); }
|
|
430
|
+
inkText(ctx, 'Guess', X(ex0 + 70), GY + 30, 40, { reveal: win(t, 6.9, 7.3) });
|
|
431
|
+
const second = t > 9.4;
|
|
432
|
+
if (!second) inkText(ctx, 'dog?', X(GX + 10), GY + 30, 76, { reveal: win(t, 7.1, 7.5) });
|
|
433
|
+
// check against the label on the picture
|
|
434
|
+
B([[PX + 30, PY - 10, 0.7], [PX - 10, PY - 70, 1], [GX + 210, GY - 10, 0.5]], { w: 5, ink: 1.05, dry: 1.1, seed: 121, p: ez((t - 7.5) / 0.4) });
|
|
435
|
+
inkText(ctx, 'Check', X(PX + 20), PY - 60, 40, { reveal: win(t, 7.6, 8.0) });
|
|
436
|
+
if (t > 7.9 && !second) {
|
|
437
|
+
// vermilion cross and an emphatic red ensō around the wrong answer
|
|
438
|
+
const red = (cp, seed, a, b, w = 11) => verm((VB) => VB(cp, { w, ink: 1.1, dry: 0.9, seed, p: ez((t - a) / (b - a)) }));
|
|
439
|
+
red([[GX + 20, GY - 40, 1], [GX + 140, GY + 40, 0.6]], 131, 7.9, 8.05);
|
|
440
|
+
red([[GX + 140, GY - 40, 1], [GX + 20, GY + 40, 0.6]], 132, 8.05, 8.2);
|
|
441
|
+
const ring = []; for (let i = 0; i <= 24; i++) { const a = -2.2 + i / 24 * Math.PI * 1.9; ring.push([GX + 82 + Math.cos(a) * 120, GY + 4 + Math.sin(a) * 74, 1 - i / 24 * 0.6]); }
|
|
442
|
+
red(ring, 133, 8.0, 8.45, 9);
|
|
443
|
+
inkText(ctx, "it's a cat", X(PX + 105), PY + 292, 36, { align: 'center', col: VERM.join(','), reveal: win(t, 8.0, 8.4) });
|
|
444
|
+
}
|
|
445
|
+
// the error flows back: a returning stroke, and the reeds inside the circle swing
|
|
446
|
+
if (t > 8.2) {
|
|
447
|
+
const cp = [[GX + 70, GY + 60, 0.9], [GX + 40, GY + 230, 1], [ex0 + 240, ey0 + 170, 0.8], [ex0 + 170, ey0 + 110, 0.6], [ex0 + 140, ey0 + 60, 0.4]];
|
|
448
|
+
verm((VB) => {
|
|
449
|
+
VB(cp, { w: 9, ink: 1.1, dry: 0.8, seed: 141, p: ez((t - 8.25) / 0.5) });
|
|
450
|
+
if (t > 8.75) { const e = [ex0 + 140, ey0 + 60]; VB([[e[0], e[1], 0.6], [e[0] + 6, e[1] + 26, 0.2]], { w: 6, ink: 1.1, dry: 0.7, seed: 142, pool: 0 }); VB([[e[0], e[1], 0.6], [e[0] + 26, e[1] + 10, 0.2]], { w: 6, ink: 1.1, dry: 0.7, seed: 143, pool: 0 }); }
|
|
451
|
+
});
|
|
452
|
+
inkText(ctx, 'Adjust', X(GX - 140), GY + 250, 40, { col: VERM.join(','), reveal: win(t, 8.5, 8.9) });
|
|
453
|
+
}
|
|
454
|
+
if (second) {
|
|
455
|
+
inkText(ctx, 'cat', X(GX + 20), GY + 30, 76, { reveal: win(t, 9.4, 9.7) });
|
|
456
|
+
B([[GX + 140, GY - 4, 0.6], [GX + 156, GY + 16, 1], [GX + 200, GY - 40, 0.3]], { w: 8, ink: 1.2, dry: 0.7, seed: 151, p: ez((t - 9.7) / 0.25) });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---------- beat 4: progress drawn as a mountain ridge climbing over training rounds ----------
|
|
460
|
+
const N = CN;
|
|
461
|
+
if (t > 10.6) {
|
|
462
|
+
B([[CX0, CY0 - CH, 0.4], [CX0, CY0, 0.7]], { w: 3, ink: 0.8, dry: 1.4, seed: 161, tone: 0.6, pool: 0, p: ez((t - 10.7) / 0.3) });
|
|
463
|
+
B([[CX0, CY0, 0.7], [CX0 + CW + 20, CY0, 0.4]], { w: 3, ink: 0.8, dry: 1.4, seed: 162, tone: 0.6, pool: 0, p: ez((t - 10.9) / 0.4) });
|
|
464
|
+
inkText(ctx, 'training rounds', X(CX0 + CW / 2), CY0 + 44, 32, { align: 'center', reveal: win(t, 11.0, 11.4) });
|
|
465
|
+
const pts = chartPts();
|
|
466
|
+
const pr = win(t, 11.2, 12.7);
|
|
467
|
+
// pale wash below the ridge, revealed as the ridge is drawn
|
|
468
|
+
if (pr > 0) {
|
|
469
|
+
const cw = chartWash(nz), wx = X(cw.x), wy = cw.y, ww = cw.cv.width, wh = cw.cv.height;
|
|
470
|
+
const lc = SCR.getContext('2d'), cx0 = Math.max(0, wx), cx1 = Math.min(W, wx + ww);
|
|
471
|
+
if (cx1 > cx0) {
|
|
472
|
+
lc.clearRect(cx0, wy, cx1 - cx0, wh); lc.drawImage(cw.cv, wx, wy);
|
|
473
|
+
if (pr < 1) { // the wash soaks in just behind the travelling brush, with a soft wet front
|
|
474
|
+
const f = X(CX0) + CW * pr + 6, g = lc.createLinearGradient(f - 70, 0, f, 0); g.addColorStop(0, '#000'); g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
475
|
+
lc.globalCompositeOperation = 'destination-in'; lc.fillStyle = g; lc.fillRect(cx0, wy, cx1 - cx0, wh); lc.globalCompositeOperation = 'source-over';
|
|
476
|
+
}
|
|
477
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.drawImage(SCR, cx0, wy, cx1 - cx0, wh, cx0, wy, cx1 - cx0, wh); ctx.restore();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
B(pts, { w: 10, ink: 1.25, dry: 0.6, seed: 171, p: pr });
|
|
481
|
+
// the little traveller climbs the ridge with the line
|
|
482
|
+
if (pr > 0) {
|
|
483
|
+
const f = pr * N, i = Math.min(N - 1, Math.floor(f)), u = f - i, x = lerp(pts[i][0], pts[i + 1][0], u), y = lerp(pts[i][1], pts[i + 1][1], u);
|
|
484
|
+
B([[x, y - 22, 0.8], [x - 1, y - 10, 1], [x - 2, y - 2, 1.1]], { w: 7, ink: 1.4, dry: 0.2, seed: 181, pool: 0.3 });
|
|
485
|
+
B([[x + 0.5, y - 28, 1], [x + 1, y - 26, 0.9]], { w: 6, ink: 1.4, dry: 0.1, seed: 182, pool: 0.6 });
|
|
486
|
+
}
|
|
487
|
+
const acc = Math.round(t < 11.2 ? 61 : t > 12.7 ? 97 : lerp(61, 97, H.ease.out(pr)));
|
|
488
|
+
inkText(ctx, 'Accuracy', X(CX0 + 10), CY0 - CH - 40, 48, { reveal: win(t, 10.9, 11.3) });
|
|
489
|
+
inkText(ctx, acc + '%', X(CX0 + 230), CY0 - CH - 40, 72, { italic: false, weight: 600, reveal: win(t, 11.1, 11.3), col: acc >= 97 ? VERM.join(',') : '22,20,19' });
|
|
490
|
+
inkText(ctx, '61%', X(CX0 - 70), pts[0][1] + 10, 30, { italic: false, weight: 600, reveal: win(t, 11.2, 11.5) });
|
|
491
|
+
if (t > 12.75) { // the seal is pressed beside the summit
|
|
492
|
+
const st = win(t, 12.75, 12.9), sc = 1 + (1 - H.ease.out(st)) * 0.3;
|
|
493
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.92 * Math.min(1, st * 1.5);
|
|
494
|
+
ctx.translate(X(CX0 + 400), CY0 - CH - 62); ctx.scale(sc, sc); ctx.drawImage(C.sealKnow, -27, -27, 54, 54); ctx.restore();
|
|
495
|
+
const ring = []; for (let i = 0; i <= 20; i++) { const a = -2 + i / 20 * Math.PI * 1.9; ring.push([pts[N][0] + Math.cos(a) * 30, pts[N][1] + Math.sin(a) * 30, 1 - i / 20 * 0.6]); }
|
|
496
|
+
verm((VB) => VB(ring, { w: 6, ink: 1.1, dry: 0.9, seed: 191, p: ez((t - 12.8) / 0.35) }));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
flush(); pressInk(ctx, VERL, VBB, `rgb(${VERM})`);
|
|
501
|
+
// ---------- mist drifting across the scroll between scenes (screen space) ----------
|
|
502
|
+
const mist = Math.max(Math.sin(Math.PI * win(t, 5.8, 7.2)), Math.sin(Math.PI * win(t, 10.2, 11.6)));
|
|
503
|
+
if (mist > 0.01) {
|
|
504
|
+
ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.filter = 'blur(24px)';
|
|
505
|
+
const mr = H.rng(55);
|
|
506
|
+
for (let k = 0; k < 7; k++) { ctx.fillStyle = `rgba(239,231,214,${mr.range(0.45, 0.75) * mist})`; ctx.beginPath(); ctx.ellipse(mr.range(0, W) - (t * 120) % 400 + 200, mr.range(120, 620), mr.range(220, 420), mr.range(30, 70), 0, 0, 7); ctx.fill(); }
|
|
507
|
+
ctx.restore();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---------- beat 5: takeaway ----------
|
|
511
|
+
if (t > 13.2) {
|
|
512
|
+
inkText(ctx, 'More examples → better guesses', 640, 680, 58, { align: 'center', reveal: ez((t - 13.2) / 0.7) });
|
|
513
|
+
}
|
|
514
|
+
// ---------- beat 1: hook -> corner header with a small seal ----------
|
|
515
|
+
{
|
|
516
|
+
const k = ez((t - 1.5) / 0.8), s = lerp(1, 0.45, k);
|
|
517
|
+
ctx.save(); ctx.font = 'italic 400 104px "Cormorant Garamond"'; const tw = ctx.measureText('How does AI learn?').width; ctx.restore();
|
|
518
|
+
ctx.save(); ctx.translate(lerp(640 - tw / 2, 44, k), lerp(380, 64, k)); ctx.scale(s, s);
|
|
519
|
+
inkText(ctx, 'How does AI learn?', 0, 0, 104, { reveal: win(t, 0.1, 1.0) });
|
|
520
|
+
if (t > 1.05) { const st = win(t, 1.05, 1.2), sc = 1 + (1 - H.ease.out(st)) * 0.3; ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = Math.min(1, st * 1.5); ctx.translate(tw + 50, -40); ctx.scale(sc, sc); ctx.drawImage(C.sealKnow, -30, -30, 60, 60); }
|
|
521
|
+
ctx.restore();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
registerStyle({
|
|
526
|
+
id: 'sumi-e',
|
|
527
|
+
name: 'Sumi-e Ink Wash',
|
|
528
|
+
category: 'Painterly',
|
|
529
|
+
description: 'Monochrome Japanese ink painting on fibrous rice paper: dilute grey washes that bleed and fade into mist, a few confident loaded-brush strokes that split into dry bristle tracks, vast negative space, a vertical calligraphy column and a vermilion seal.',
|
|
530
|
+
motion: 'Mist drifts sideways across the peaks while each brush stroke is laid down in one sweep, bristles running dry as it travels; the tiny device on the ledge pulses with light.',
|
|
531
|
+
fonts: ['900 58px "Noto Serif JP"', '700 30px "Noto Serif JP"', '600 20px "Cormorant Garamond"', 'italic 400 40px "Cormorant Garamond"', '600 40px "Noto Serif JP"'],
|
|
532
|
+
duration: 6.8,
|
|
533
|
+
frameT: 6.7,
|
|
534
|
+
readsPixels: true,
|
|
535
|
+
explainerDuration: 15,
|
|
536
|
+
explainerNote: 'Told along a handscroll the camera unrolls, with mist drifting across between scenes; arrows and the chart are single loaded brush strokes that run dry, labels and numbers are ink-written in place, and vermilion strokes and seal stamps do the emphasis.',
|
|
537
|
+
explainer(ctx, W, H_, t) { explainSumi(ctx, W, H_, t); },
|
|
538
|
+
explainerSound(k) {
|
|
539
|
+
// all times copied from explainSumi() above (its win()/ez() windows). Calm and sparse: ink, paper, wood, a bowl.
|
|
540
|
+
const D = 15;
|
|
541
|
+
const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
|
|
542
|
+
// a loaded ink brush on rice paper: soft press, a wet body, and dry bristles rasping as it runs out at the tail
|
|
543
|
+
const ink = (a, b, g = 1, pan = 0, minor = true) => {
|
|
544
|
+
const d = b - a, r = H.rng(Math.round(a * 1000) + 7);
|
|
545
|
+
k.noise(a, { dur: 0.06, color: 'brown', filter: 'lowpass', f: 600, gain: 0.3 * g, a: 0.006, pan, minor });
|
|
546
|
+
k.play('stroke', a, { material: 'brush', dur: d * 0.7, gain: 1.4 * g, pan, minor });
|
|
547
|
+
for (let x = d * 0.45; x < d; x += 0.03 + r() * 0.03) // dry-brush tail: sparser, a little scratchier
|
|
548
|
+
k.noise(a + x, { dur: 0.035, filter: 'bandpass', f: 1500 + r() * 700, q: 0.9, gain: 0.07 * g * (0.5 + r() * 0.6), a: 0.008, pan, minor });
|
|
549
|
+
};
|
|
550
|
+
const write = (s, a, b, g = 0.7, pan = 0) => { const re = /\S+/g; let m;
|
|
551
|
+
while ((m = re.exec(s))) ink(a + (b - a) * m.index / s.length, a + (b - a) * (m.index + m[0].length) / s.length, g, pan); };
|
|
552
|
+
// soroban bead: a small, dry wooden click for the counters
|
|
553
|
+
const bead = (t, g = 1, pan = 0) => k.play('tock', t, { f: 1150, gain: 0.32 * g, pan, minor: true });
|
|
554
|
+
const counter = (val, a, b, pan, avoid = []) => { let last = val(a), lastT = -1;
|
|
555
|
+
for (let t = a; t <= b; t += 1 / 240) { const v = val(t); if (v !== last && t - lastT >= 1 / 12 && !avoid.some((x) => t > x - 0.03 && t < x + 0.1)) { bead(t, 1, pan); lastT = t; } last = v; } };
|
|
556
|
+
// singing bowl (rin): low fundamental with inharmonic partials, long ring
|
|
557
|
+
const bowl = (t, f, g = 1, pan = 0) => { const h = k.hz(f);
|
|
558
|
+
k.tone(t, { f: h, dur: 3.2, gain: 0.16 * g, a: 0.01, verb: 0.4, pan });
|
|
559
|
+
k.tone(t, { f: h * 2.71, dur: 2.0, gain: 0.05 * g, a: 0.01, verb: 0.4, pan, detune: 4 });
|
|
560
|
+
k.tone(t, { f: h * 1.003, dur: 3.0, gain: 0.08 * g, a: 0.02, pan: -pan }); };
|
|
561
|
+
// mist drifting across while the scroll unrolls: a slow airy breath and the paper sliding on the table
|
|
562
|
+
const mist = (a, b) => { const d = b - a;
|
|
563
|
+
k.noise(a, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 300], [d * 0.5, 900], [d, 350]], q: 0.5, gain: 0.075, a: d * 0.5, verb: 0.3 });
|
|
564
|
+
k.noise(a + 0.2, { dur: 0.95, color: 'brown', filter: 'lowpass', f: 500, gain: 0.12, a: 0.3, pan: 0.3 }); };
|
|
565
|
+
// a drop falling into still water, kept low
|
|
566
|
+
const plop = (t, g = 1, pan = 0) => { k.tone(t, { f: [[0, 260], [0.07, 470]], dur: 0.11, gain: 0.12 * g, a: 0.003, verb: 0.35, pan }); };
|
|
567
|
+
|
|
568
|
+
k.bed('room', 0, D, { gain: 0.4 });
|
|
569
|
+
k.music('pad', 0, D, { bpm: 64, gain: 0.32, chords: [['D3', 'A3', 'E4', 'G4'], ['C3', 'G3', 'D4', 'E4'], ['Bb2', 'F3', 'C4', 'D4'], ['D3', 'A3', 'D4', 'E4']] });
|
|
570
|
+
|
|
571
|
+
// ---- 1 HOOK: title ink-written 0.1-1.0, the small seal is pressed at 1.05-1.2, header drifts to the corner 1.5-2.3
|
|
572
|
+
write('How does AI learn?', 0.1, 1.0, 0.9);
|
|
573
|
+
k.play('press', 1.1, { gain: 0.3 });
|
|
574
|
+
k.noise(1.5, { dur: 0.8, color: 'pink', filter: 'bandpass', f: [[0, 280], [0.8, 600]], q: 0.7, gain: 0.06, a: 0.3, minor: true });
|
|
575
|
+
|
|
576
|
+
// ---- 2 DATA: the ensō in one breath (1.2-2.1), the river banks and wash, reeds, slips floating down
|
|
577
|
+
ink(1.2, 2.1, 1.3, 0.3, false);
|
|
578
|
+
k.noise(1.6, { dur: 1.4, color: 'pink', filter: 'lowpass', f: [[0, 400], [1.4, 900]], gain: 0.06, a: 0.6, pan: -0.4, minor: true }); // the wash spreading
|
|
579
|
+
ink(1.6, 2.8, 0.55, -0.5); ink(1.8, 3.0, 0.55, -0.3);
|
|
580
|
+
// the river carries the slips: a faint trickle, and a soft plop as one sinks into the circle
|
|
581
|
+
k.noise(2.6, { dur: 3.6, color: 'pink', filter: 'bandpass', f: 600, q: 0.6, gain: 0.04, a: 0.6, hold: 2.4, pan: -0.3, minor: true });
|
|
582
|
+
const plops = [0, 5, 10, 15, 20].map((i) => 2.6 + i * 0.16 + 1.9 * 0.95);
|
|
583
|
+
plops.forEach((t) => plop(t, 0.7, 0.2));
|
|
584
|
+
// counter ink-written: n = 1e6 * ease.in(win(3.0, 5.6)), rounded to thousands; soroban beads as it climbs
|
|
585
|
+
counter((t) => Math.round(1e6 * H.ease.in(win(t, 3.0, 5.6)) / 1000), 3.0, 5.58, -0.5, plops);
|
|
586
|
+
k.play('tock', 5.6, { f: 640, gain: 0.6, pan: -0.5 }); // 1,000,000: beads clear with a firm clack
|
|
587
|
+
ink(5.6, 5.95, 1.0, -0.4, false); // dry underline
|
|
588
|
+
|
|
589
|
+
// ---- mist and scroll 5.8-7.2 (camera 6.0-6.95)
|
|
590
|
+
mist(5.8, 7.2);
|
|
591
|
+
|
|
592
|
+
// ---- 3 GUESS -> CHECK -> ADJUST
|
|
593
|
+
k.noise(6.5, { dur: 0.12, color: 'brown', filter: 'lowpass', f: 900, gain: 0.12, a: 0.03, pan: 0.6, minor: true }); // the new slip laid down
|
|
594
|
+
ink(6.8, 7.2, 1.0, 0.1, false); // Guess arrow
|
|
595
|
+
write('dog?', 7.1, 7.5, 0.85, 0.3);
|
|
596
|
+
ink(7.5, 7.9, 0.9, 0.5, false); // Check arrow
|
|
597
|
+
// the wrong guess: two vermilion slashes, a low wooden knock
|
|
598
|
+
ink(7.9, 8.05, 2.0, 0.3, false); ink(8.05, 8.2, 2.0, 0.3, false);
|
|
599
|
+
k.play('thud', 7.9, { f: 105, gain: 0.55, pan: 0.3 });
|
|
600
|
+
ink(8.25, 8.75, 1.4, 0.0, false); // the error flows back
|
|
601
|
+
plop(8.6, 1.1, -0.2); // a ripple spreads from the circle
|
|
602
|
+
// reeds swing (ez over 8.7-9.4): a dry rustle of stems
|
|
603
|
+
k.noise(8.7, { dur: 0.7, color: 'pink', filter: 'bandpass', f: [[0, 1200], [0.35, 1800], [0.7, 1000]], q: 1.0, gain: 0.07, a: 0.3, pan: -0.2, minor: true });
|
|
604
|
+
write('cat', 9.4, 9.7, 0.9, 0.3);
|
|
605
|
+
ink(9.7, 9.95, 1.1, 0.3, false); // check mark
|
|
606
|
+
bowl(9.95, 'A4', 0.35, 0.3); // correct: a bowl rings
|
|
607
|
+
|
|
608
|
+
// ---- mist and scroll 10.2-11.6 (camera 10.45-11.4)
|
|
609
|
+
mist(10.2, 11.6);
|
|
610
|
+
|
|
611
|
+
// ---- 4 PROGRESS: axes 10.7-11.0 / 10.9-11.3, the ridge 11.2-12.7, the summit seal 12.75
|
|
612
|
+
ink(10.7, 11.0, 0.8, -0.4); ink(10.9, 11.3, 0.8, 0);
|
|
613
|
+
ink(11.2, 12.7, 0.75, 0.1); // one long ridge stroke, running dry
|
|
614
|
+
const acc = (t) => Math.round(61 + 36 * H.ease.out(win(t, 11.2, 12.7)));
|
|
615
|
+
const t97 = when(acc, 97, 11.2, 12.7);
|
|
616
|
+
counter(acc, 11.2, t97 - 0.06, -0.3);
|
|
617
|
+
k.play('tock', t97, { f: 700, gain: 0.7, pan: -0.3 }); // 97% lands in vermilion
|
|
618
|
+
// the seal is pressed beside the summit (scale settles 12.75-12.9): the biggest hit
|
|
619
|
+
k.play('stamp', 12.8, { gain: 1.0, pan: 0.1 });
|
|
620
|
+
k.play('impact', 12.8, { f: 80, gain: 0.8 });
|
|
621
|
+
k.noise(12.8, { dur: 1.2, color: 'pink', filter: 'lowpass', f: [[0, 1400], [1.0, 250]], gain: 0.16, verb: 0.4 });
|
|
622
|
+
bowl(12.82, 'D4', 1.0, 0.1);
|
|
623
|
+
ink(12.8, 13.15, 1.0, 0.6); // small vermilion ring at the summit
|
|
624
|
+
|
|
625
|
+
// ---- 5 TAKEAWAY (13.2-13.9) and the resolve: a low bowl under the last chord
|
|
626
|
+
write('More examples → better guesses', 13.2, 13.9, 1.1);
|
|
627
|
+
bowl(13.95, 'D3', 0.6, 0);
|
|
628
|
+
bowl(14.0, 'A3', 0.3, 0.3);
|
|
629
|
+
},
|
|
630
|
+
draw(ctx, W, H_, t) {
|
|
631
|
+
const nz = H.makeNoise(808);
|
|
632
|
+
const R = H.rng(4242);
|
|
633
|
+
const drift = t * 16;
|
|
634
|
+
// timeline helpers: sp = linear brush travel, ep = eased
|
|
635
|
+
const sp = (a, b) => H.clamp((t - a) / (b - a));
|
|
636
|
+
const ep = (a, b) => H.ease.inOut(sp(a, b));
|
|
637
|
+
|
|
638
|
+
// ---------- 1. rice paper (static: built once per size) ----------
|
|
639
|
+
if (HERO_PAPER_KEY !== W + 'x' + H_) { HERO_PAPER = makePaper(W, H_, nz, R, 900); HERO_PAPER_KEY = W + 'x' + H_; }
|
|
640
|
+
ctx.drawImage(HERO_PAPER, 0, 0);
|
|
641
|
+
|
|
642
|
+
const L = H.layer(W, H_), lx = L.getContext('2d');
|
|
643
|
+
const T = H.layer(W, H_), tx = T.getContext('2d');
|
|
644
|
+
// graded wash: darkest at the ridge, dissolving downward into mist; uneven water; pooled ridge edge; slight bleed.
|
|
645
|
+
// prog (0..1) = the ink spreading down from the ridge into the damp paper. Works on its bounding box only.
|
|
646
|
+
function ridgeWash(ridge, x0, x1, tone, fade, mistY, mistR, seed, edgeFall = true, maxX = null, prog = 1) {
|
|
647
|
+
const ry = (x) => {
|
|
648
|
+
let y = ridge(x);
|
|
649
|
+
if (edgeFall) { if (x0 > 0) y += Math.pow(Math.max(0, (x0 + 260 - x) / 260), 2.2) * 300; y += Math.pow(Math.max(0, (x - (x1 - 260)) / 260), 2.2) * 300; }
|
|
650
|
+
return y;
|
|
651
|
+
};
|
|
652
|
+
if (prog <= 0) return ry;
|
|
653
|
+
const bx0 = Math.max(0, x0), bx1 = Math.min(W, x1);
|
|
654
|
+
const tops = new Float32Array(bx1 - bx0);
|
|
655
|
+
let ymin = H_;
|
|
656
|
+
for (let x = bx0; x < bx1; x++) { tops[x - bx0] = ry(x); ymin = Math.min(ymin, tops[x - bx0]); }
|
|
657
|
+
ymin = Math.max(0, Math.floor(ymin) - 6);
|
|
658
|
+
const bw = bx1 - bx0, bh = H_ - ymin;
|
|
659
|
+
const reach = 30 + prog * prog * 520; // how far the ink has crept below the ridge
|
|
660
|
+
const tn = tone * (0.45 + 0.55 * H.ease.out(prog));
|
|
661
|
+
lx.clearRect(0, 0, W, H_);
|
|
662
|
+
const img = lx.createImageData(bw, bh), a = img.data;
|
|
663
|
+
for (let x = bx0; x < bx1; x++) {
|
|
664
|
+
const top = tops[x - bx0], m = mistY + nz.n2(x * 0.007, seed) * mistR * 0.8;
|
|
665
|
+
const hf = edgeFall ? (x0 > 0 ? H.smooth(x0, x0 + 200, x) : 1) * H.smooth(x1, x1 - 200, x) : (x0 > 0 ? H.smooth(x0, x0 + 40, x) : 1) * (x1 < W ? H.smooth(x1, x1 - 40, x) : 1);
|
|
666
|
+
const rch = reach * (0.8 + 0.4 * (nz.n2(x * 0.02, seed + 3) + 0.5));
|
|
667
|
+
for (let y = Math.max(ymin, Math.floor(top)); y < H_; y++) {
|
|
668
|
+
if (maxX && x > maxX(y)) continue;
|
|
669
|
+
const dy = y - top;
|
|
670
|
+
let al = (0.32 + 0.68 * Math.exp(-dy / fade)) * H.smooth(m + mistR, m - mistR * 0.4, y) * hf;
|
|
671
|
+
if (prog < 1) al *= H.clamp((rch - dy) / 40);
|
|
672
|
+
if (dy < 1) al *= Math.max(0, dy);
|
|
673
|
+
if (al < 0.004 && dy > 2 && (y > m || dy > rch)) break;
|
|
674
|
+
const k = ((y - ymin) * bw + (x - bx0)) * 4; a[k] = INK[0]; a[k + 1] = INK[1]; a[k + 2] = INK[2]; a[k + 3] = al * 255;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
lx.putImageData(img, bx0, ymin);
|
|
678
|
+
// uneven water: lighter patches
|
|
679
|
+
// (soft-edged via a radial gradient instead of a blur filter: much cheaper per frame)
|
|
680
|
+
lx.globalCompositeOperation = 'destination-out';
|
|
681
|
+
const rr = H.rng(seed);
|
|
682
|
+
const soft = lx.createRadialGradient(0, 0, 0, 0, 0, 1.35); soft.addColorStop(0, '#000'); soft.addColorStop(0.55, 'rgba(0,0,0,0.75)'); soft.addColorStop(1, 'rgba(0,0,0,0)');
|
|
683
|
+
for (let k = 0; k < 16; k++) {
|
|
684
|
+
const x = rr.range(x0, x1); lx.globalAlpha = rr.range(0.15, 0.45);
|
|
685
|
+
const ey = ry(x) + rr.range(25, 160), erx = rr.range(30, 90), ery = rr.range(10, 40), ea = rr.range(-0.6, 0.6);
|
|
686
|
+
lx.save(); lx.translate(x, ey); lx.rotate(ea); lx.scale(erx, ery); lx.fillStyle = soft; lx.beginPath(); lx.arc(0, 0, 1.35, 0, 7); lx.fill(); lx.restore();
|
|
687
|
+
}
|
|
688
|
+
for (let k = 0; k < 5; k++) { const x = rr.range(x0 + 60, x1 - 60), y = ry(x) + rr.range(30, 110), rad = rr.range(14, 34); lx.globalAlpha = rr.range(0.1, 0.2); lx.beginPath();
|
|
689
|
+
for (let a = 0; a < 6.3; a += 0.3) { const q = rad * (0.75 + 0.35 * Math.abs(nz.n2(a * 1.3 + k * 7, seed))); a ? lx.lineTo(x + Math.cos(a) * q, y + Math.sin(a) * q * 0.7) : lx.moveTo(x + q, y); } lx.closePath(); lx.fill(); }
|
|
690
|
+
lx.globalCompositeOperation = 'source-over'; lx.globalAlpha = 1;
|
|
691
|
+
tx.clearRect(bx0, ymin, bw, bh); tx.filter = 'blur(4px)'; tx.drawImage(L, bx0, ymin, bw, bh, bx0, ymin, bw, bh); tx.filter = 'none';
|
|
692
|
+
const A = lx.getImageData(bx0, ymin, bw, bh), B = tx.getImageData(bx0, ymin, bw, bh), d = A.data, b = B.data;
|
|
693
|
+
for (let i = 3; i < d.length; i += 4) {
|
|
694
|
+
const rim = d[i] - b[i];
|
|
695
|
+
d[i] = Math.min(255, (Math.max(d[i], b[i] * 0.9) + (rim > 0 ? rim * 0.25 : 0)) * tn);
|
|
696
|
+
}
|
|
697
|
+
lx.putImageData(A, bx0, ymin);
|
|
698
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.drawImage(L, bx0, ymin, bw, bh, bx0, ymin, bw, bh); ctx.restore();
|
|
699
|
+
return ry;
|
|
700
|
+
}
|
|
701
|
+
const S = H.layer(W, H_), sx = S.getContext('2d');
|
|
702
|
+
function strokes(fn, bleed = true, bb = [0, 0, W, H_]) {
|
|
703
|
+
const [x0, y0, x1, y1] = bb, w = x1 - x0, h = y1 - y0;
|
|
704
|
+
sx.clearRect(x0, y0, w, h); fn();
|
|
705
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply';
|
|
706
|
+
if (bleed) { ctx.filter = 'blur(1.4px)'; ctx.globalAlpha = 0.45; ctx.drawImage(S, x0, y0, w, h, x0, y0, w, h); ctx.filter = 'none'; ctx.globalAlpha = 1; }
|
|
707
|
+
ctx.drawImage(S, x0, y0, w, h, x0, y0, w, h); ctx.restore();
|
|
708
|
+
}
|
|
709
|
+
const pk = (x, c, h, s) => h * Math.exp(-Math.pow((x - c) / s, 2));
|
|
710
|
+
|
|
711
|
+
// ---------- 2. far peaks: palest dilution ----------
|
|
712
|
+
const far = (x) => 600 - pk(x, 820, 160, 230) - pk(x, 800, 360, 80) - pk(x, 900, 270, 46) - pk(x, 712, 210, 50) - pk(x, 985, 170, 55) - pk(x, 640, 130, 130) - pk(x, 1050, 70, 110)
|
|
713
|
+
+ nz.fbm2(x * 0.016, 3.1, 4) * 44 + Math.abs(nz.n2(x * 0.05, 9)) * 18 + nz.n2(x * 0.22, 4) * 2.5;
|
|
714
|
+
const farY = ridgeWash(far, 420, 1150, 0.36, 130, 440, 80, 11, false, null, sp(0.1, 1.5));
|
|
715
|
+
|
|
716
|
+
// ---------- 3. middle range, left ----------
|
|
717
|
+
const mid = (x) => 640 - pk(x, 250, 110, 260) - pk(x, 170, 260, 100) - pk(x, 330, 190, 75) - pk(x, 470, 120, 110) - pk(x, 610, 50, 120) - pk(x, 30, 170, 70)
|
|
718
|
+
+ nz.fbm2(x * 0.018, 7.7, 4) * 26 + Math.abs(nz.n2(x * 0.06, 2)) * 10;
|
|
719
|
+
const midY = ridgeWash(mid, 0, 760, 0.55, 110, 500, 60, 22, false, null, sp(0.7, 2.0));
|
|
720
|
+
|
|
721
|
+
// hemp-fibre texture strokes (pi-ma cun) running down the slopes + broken ridge contours
|
|
722
|
+
strokes(() => {
|
|
723
|
+
const rr = H.rng(34);
|
|
724
|
+
const contour = (fy, xa, xb, wv, ink, tone, seed, a0) => {
|
|
725
|
+
const cp = []; for (let x = xa; x <= xb; x += 18) cp.push([x, fy(x) + 1.5, 0.45 + 0.55 * Math.abs(Math.sin((x - xa) / (xb - xa) * Math.PI * 1.3))]);
|
|
726
|
+
brush(sx, cp, { w: wv, ink, dry: 1.2, seed, tone, pool: 0.2, p: ep(a0, a0 + 0.45) }, nz);
|
|
727
|
+
};
|
|
728
|
+
contour(farY, 600, 790, 5, 0.75, 0.55, 31, 1.0); contour(farY, 812, 900, 4, 0.6, 0.45, 32, 1.3); contour(farY, 905, 1000, 4, 0.6, 0.4, 35, 1.5);
|
|
729
|
+
contour(midY, 60, 250, 7, 0.9, 0.7, 33, 1.6); contour(midY, 270, 420, 6, 0.85, 0.6, 36, 1.85);
|
|
730
|
+
const fibres = (fy, xa, xb, n, tone, seedBase, a0, span) => {
|
|
731
|
+
for (let k = 0; k < n; k++) {
|
|
732
|
+
const x = rr.range(xa, xb), y0 = fy(x) + rr.range(4, 20), sl = (fy(x + 8) - fy(x - 8)) / 16;
|
|
733
|
+
const dx = H.clamp(-sl * 0.6, -0.8, 0.8), len = rr.range(35, 85);
|
|
734
|
+
const cp = []; for (let j = 0; j <= 4; j++) { const u = j / 4; cp.push([x + dx * len * u + Math.sin(u * 3 + k) * 7, y0 + len * u, 0.5 + Math.sin(u * Math.PI) * 0.5]); }
|
|
735
|
+
brush(sx, cp, { w: rr.range(2.5, 4), ink: 0.6, dry: 1.3, seed: seedBase + k, tone, pool: 0, p: ep(a0 + span * k / n, a0 + span * k / n + 0.2) }, nz);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
fibres(farY, 640, 1000, 16, 0.26, 400, 1.6, 0.6);
|
|
739
|
+
fibres(midY, 40, 520, 30, 0.42, 500, 2.0, 0.6);
|
|
740
|
+
}, true, [0, 0, 1060, 620]);
|
|
741
|
+
|
|
742
|
+
// ---------- 4. drifting mist (lift ink back to paper) ----------
|
|
743
|
+
ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.filter = 'blur(26px)';
|
|
744
|
+
const mr = H.rng(55);
|
|
745
|
+
for (let k = 0; k < 8; k++) {
|
|
746
|
+
ctx.fillStyle = `rgba(239,231,214,${mr.range(0.3, 0.55)})`;
|
|
747
|
+
ctx.beginPath(); ctx.ellipse(((mr.range(0, W) + drift * (1 + k * 0.2)) % (W + 300)) - 150, mr.range(380, 500), mr.range(160, 320), mr.range(12, 26), 0, 0, 7); ctx.fill();
|
|
748
|
+
}
|
|
749
|
+
ctx.restore();
|
|
750
|
+
|
|
751
|
+
// ---------- 5. foreground cliff ----------
|
|
752
|
+
const cliff = (x) => 470 - (x / 440) * 30 + Math.pow(Math.max(0, 160 - x) / 160, 2) * 30 + nz.n2(x * 0.03, 5) * 9 + Math.sin(x * 0.05) * 3;
|
|
753
|
+
const face = [[436, 452], [450, 490], [432, 540], [436, 600], [470, 650], [520, 720]];
|
|
754
|
+
const faceX = (y) => { for (let k = 0; k < face.length - 1; k++) if (y <= face[k + 1][1]) { const u = H.clamp((y - face[k][1]) / (face[k + 1][1] - face[k][1])); return face[k][0] + (face[k + 1][0] - face[k][0]) * u; } return 520; };
|
|
755
|
+
const cliffY = ridgeWash(cliff, 0, 470, 0.62, 120, 640, 70, 66, false, faceX, sp(2.05, 3.0));
|
|
756
|
+
strokes(() => {
|
|
757
|
+
// the ledge: one decisive loaded stroke, splitting at the end
|
|
758
|
+
const cp = []; for (let x = -10; x <= 440; x += 30) cp.push([x, cliffY(x) - 1, x < 30 ? 0.55 : 1 - (x / 440) * 0.35]);
|
|
759
|
+
brush(sx, cp, { w: 16, ink: 1.25, dry: 0.75, seed: 101, p: ep(1.5, 2.0) }, nz);
|
|
760
|
+
// the face dropping away into mist
|
|
761
|
+
brush(sx, [[438, 452, 0.9], [452, 490, 1], [434, 540, 0.85], [438, 600, 0.6], [472, 650, 0.35]], { w: 20, ink: 1.1, dry: 1.1, seed: 102, p: ep(1.9, 2.25) }, nz);
|
|
762
|
+
// side-brush dry strokes describing the rock planes
|
|
763
|
+
const rr = H.rng(103);
|
|
764
|
+
for (let k = 0; k < 5; k++) {
|
|
765
|
+
const x = rr.range(30, 380), y = cliffY(x) + rr.range(22, 110);
|
|
766
|
+
const len = rr.range(70, 130), a = rr.range(2.3, 2.7);
|
|
767
|
+
const cp = []; for (let j = 0; j <= 5; j++) { const u = j / 5; cp.push([x + Math.cos(a) * len * u + Math.sin(u * 4) * 6, y + Math.sin(a) * len * u * 0.6 + u * u * 20, 0.35 + Math.sin(u * Math.PI) * 0.65]); }
|
|
768
|
+
brush(sx, cp, { w: rr.range(9, 15), ink: 0.75, dry: 1.3, seed: 110 + k, tone: 0.4, pool: 0, rim: 0, p: ep(2.95 + k * 0.09, 3.2 + k * 0.09) }, nz);
|
|
769
|
+
}
|
|
770
|
+
// moss dots (dian) along the ledge
|
|
771
|
+
for (let k = 0; k < 22; k++) {
|
|
772
|
+
const x = rr.range(0, 420), y = cliffY(x) - rr.range(1, 7);
|
|
773
|
+
brush(sx, [[x, y, 1], [x + rr.range(3, 6), y + rr.range(-1, 3), 0.6]], { w: rr.range(4, 8), ink: 1.3, dry: 0.3, seed: 150 + k, pool: 0.6, p: sp(3.3 + k * 0.018, 3.36 + k * 0.018) }, nz);
|
|
774
|
+
}
|
|
775
|
+
}, true, [0, 380, 540, 720]);
|
|
776
|
+
|
|
777
|
+
// ---------- 6. pine ----------
|
|
778
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.filter = 'blur(9px)';
|
|
779
|
+
ctx.fillStyle = `rgba(60,58,55,${0.14 * ep(3.9, 4.4)})`;
|
|
780
|
+
[[250, 268, 110, 28], [400, 278, 70, 22], [120, 196, 80, 30]].forEach(([x, y, a, b]) => { ctx.beginPath(); ctx.ellipse(x, y, a, b, 0, 0, 7); ctx.fill(); });
|
|
781
|
+
ctx.restore();
|
|
782
|
+
strokes(() => {
|
|
783
|
+
// trunk: one confident twisting stroke, bark scales dotted on afterwards
|
|
784
|
+
const trunk = [[100, 448, 1], [120, 382, 0.95], [104, 322, 0.9], [134, 262, 0.75], [128, 196, 0.5]];
|
|
785
|
+
brush(sx, trunk, { w: 17, ink: 1.05, dry: 1.25, seed: 201, p: ep(3.0, 3.4) }, nz);
|
|
786
|
+
const rr = H.rng(204);
|
|
787
|
+
for (let k = 0; k < 6; k++) {
|
|
788
|
+
const u = 0.1 + k / 7.5, i = Math.floor(u * (trunk.length - 1)), f = u * (trunk.length - 1) - i;
|
|
789
|
+
const x = trunk[i][0] + (trunk[i + 1][0] - trunk[i][0]) * f + rr.gauss() * 3, y = trunk[i][1] + (trunk[i + 1][1] - trunk[i][1]) * f;
|
|
790
|
+
brush(sx, [[x - 5, y + 3, 0.5], [x, y - 1, 1], [x + 5, y + 2, 0.4]], { w: 2.4, ink: 1.1, dry: 0.6, seed: 210 + k, pool: 0, rim: 0, p: sp(3.4 + k * 0.04, 3.46 + k * 0.04) }, nz);
|
|
791
|
+
}
|
|
792
|
+
// reaching branch + a counter branch
|
|
793
|
+
brush(sx, [[120, 300, 1], [190, 284, 0.85], [260, 296, 0.7], [340, 282, 0.5], [420, 290, 0.25]], { w: 10, ink: 1.1, dry: 1.0, seed: 205, p: ep(3.35, 3.75) }, nz);
|
|
794
|
+
brush(sx, [[132, 230, 0.8], [84, 212, 0.6], [36, 220, 0.3]], { w: 8, ink: 1.0, dry: 1.2, seed: 206, p: ep(3.55, 3.8) }, nz);
|
|
795
|
+
brush(sx, [[260, 296, 0.5], [300, 268, 0.35], [318, 250, 0.2]], { w: 5, ink: 1.0, dry: 1.2, seed: 207, p: ep(3.7, 3.9) }, nz);
|
|
796
|
+
// needle fans
|
|
797
|
+
const clusters = [[205, 280, 1], [262, 286, 1.1], [318, 248, 0.8], [350, 280, 0.95], [414, 286, 0.8], [140, 192, 1], [96, 208, 0.9], [44, 214, 0.75], [168, 244, 0.85]];
|
|
798
|
+
clusters.forEach(([x, y, sc], ci) => {
|
|
799
|
+
const n = 7 + (ci % 3);
|
|
800
|
+
for (let k = 0; k < n; k++) {
|
|
801
|
+
const a = -Math.PI * 0.88 + (Math.PI * 0.76) * (k / (n - 1)) + rr.gauss() * 0.08, len = rr.range(18, 30) * sc * (0.75 + 0.25 * Math.sin(k / (n - 1) * Math.PI));
|
|
802
|
+
brush(sx, [[x, y, 0.9], [x + Math.cos(a) * len * 0.6, y + Math.sin(a) * len * 0.6, 0.6], [x + Math.cos(a) * len, y + Math.sin(a) * len, 0.15]], { w: 3, ink: 1.15, dry: 0.9, seed: 300 + ci * 20 + k, pool: 0, p: sp(3.8 + ci * 0.06 + k * 0.012, 3.87 + ci * 0.06 + k * 0.012) }, nz);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}, true, [0, 150, 480, 470]);
|
|
806
|
+
|
|
807
|
+
// ---------- 7. the figure + glowing device ----------
|
|
808
|
+
const fx = 396, fy = cliffY(396) - 3;
|
|
809
|
+
const pulse = (0.75 + 0.25 * Math.sin((t - 6.1) * 2.6)) * ep(4.75, 5.2);
|
|
810
|
+
ctx.save(); ctx.globalCompositeOperation = 'screen';
|
|
811
|
+
const gg = ctx.createRadialGradient(fx + 10, fy - 20, 0, fx + 10, fy - 20, 110);
|
|
812
|
+
gg.addColorStop(0, `rgba(205,238,242,${0.95 * pulse})`); gg.addColorStop(0.22, `rgba(160,210,220,${0.35 * pulse})`); gg.addColorStop(1, 'rgba(160,210,220,0)');
|
|
813
|
+
ctx.fillStyle = gg; ctx.fillRect(fx - 90, fy - 110, 200, 180); ctx.restore();
|
|
814
|
+
strokes(() => {
|
|
815
|
+
brush(sx, [[fx, fy - 22, 0.8], [fx - 2, fy - 10, 1], [fx - 4, fy, 1.15]], { w: 9, ink: 1.4, dry: 0.2, seed: 501, pool: 0.3, p: sp(4.45, 4.6) }, nz);
|
|
816
|
+
brush(sx, [[fx + 0.5, fy - 29, 1], [fx + 1, fy - 26.5, 0.9]], { w: 6.5, ink: 1.4, dry: 0.1, seed: 502, pool: 0.6, p: sp(4.6, 4.65) }, nz);
|
|
817
|
+
brush(sx, [[fx + 1, fy - 19, 0.7], [fx + 7, fy - 17, 0.5], [fx + 9.5, fy - 20, 0.4]], { w: 2.6, ink: 1.3, dry: 0.3, seed: 503, pool: 0, p: sp(4.65, 4.72) }, nz);
|
|
818
|
+
}, false, [300, 380, 480, 480]);
|
|
819
|
+
if (t > 4.75) { ctx.fillStyle = `rgba(220,248,252,${0.95 * ep(4.75, 4.9)})`; ctx.fillRect(fx + 8.5, fy - 23.5, 4, 3); }
|
|
820
|
+
ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.filter = 'blur(2.5px)';
|
|
821
|
+
const beam = ep(4.9, 5.4), bgr = ctx.createLinearGradient(fx + 10, fy - 22, fx + 150, fy - 330);
|
|
822
|
+
bgr.addColorStop(0, `rgba(205,238,242,${0.55 * pulse})`); bgr.addColorStop(1, 'rgba(205,238,242,0)');
|
|
823
|
+
ctx.strokeStyle = bgr; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(fx + 10, fy - 22); ctx.lineTo(fx + 10 + 140 * beam, fy - 22 - 308 * beam); ctx.stroke();
|
|
824
|
+
ctx.restore();
|
|
825
|
+
|
|
826
|
+
// ---------- 8. birds ----------
|
|
827
|
+
strokes(() => {
|
|
828
|
+
[[640, 238, 1], [672, 226, 0.8], [700, 246, 0.65]].forEach(([x, y, s], k) => {
|
|
829
|
+
const xx = x + (t - 6.7) * 22 + Math.sin(t * 0.8 + k) * 6, yy = y + Math.sin(t * 1.1 + k * 2) * 4, f = Math.sin(t * 7 + k * 1.7) * 3 * s;
|
|
830
|
+
if (t < 1.8 + k * 0.15) return;
|
|
831
|
+
brush(sx, [[xx - 9 * s, yy - 2 * s + f, 0.4], [xx - 4 * s, yy - 4 * s + f * 0.5, 0.8], [xx, yy, 1]], { w: 3, ink: 1.2, dry: 0.5, seed: 601 + k, pool: 0 }, nz);
|
|
832
|
+
brush(sx, [[xx, yy, 1], [xx + 5 * s, yy - 5 * s + f * 0.5, 0.8], [xx + 11 * s, yy - 3 * s + f, 0.4]], { w: 3, ink: 1.2, dry: 0.5, seed: 611 + k, pool: 0 }, nz);
|
|
833
|
+
});
|
|
834
|
+
}, true, [480, 120, 1000, 320]);
|
|
835
|
+
|
|
836
|
+
// ---------- 9. calligraphy column ----------
|
|
837
|
+
const chars = ['山', 'に', '宿', 'る', '知', '恵'];
|
|
838
|
+
// small offscreen layers (calligraphy strip, seal) keep the destination-out work cheap
|
|
839
|
+
const CL = H.layer(160, 480), tx2 = CL.getContext('2d'); tx2.translate(-1088, -36);
|
|
840
|
+
tx2.fillStyle = `rgb(${INK})`; tx2.textAlign = 'center'; tx2.textBaseline = 'middle';
|
|
841
|
+
const cxT = 1168, cr2 = H.rng(707);
|
|
842
|
+
chars.forEach((ch, i) => {
|
|
843
|
+
tx2.save(); tx2.translate(cxT + cr2.gauss() * 2.5, 92 + i * 62); tx2.rotate(cr2.gauss() * 0.04);
|
|
844
|
+
const fs = 56 + cr2.range(-3, 4), rv = H.ease.out(sp(4.3 + i * 0.12, 4.42 + i * 0.12));
|
|
845
|
+
if (rv > 0) { tx2.beginPath(); tx2.rect(-40, -34, 80, 68 * rv); tx2.clip(); tx2.font = `900 ${fs}px "Noto Serif JP"`; tx2.globalAlpha = 0.92; tx2.fillText(ch, 0, 0); }
|
|
846
|
+
tx2.restore();
|
|
847
|
+
});
|
|
848
|
+
// dry-brush streaks bitten out of the characters + bleed
|
|
849
|
+
tx2.globalCompositeOperation = 'destination-out';
|
|
850
|
+
for (let k = 0; k < 420; k++) {
|
|
851
|
+
const x = cxT + cr2.range(-40, 40), y = cr2.range(56, 450);
|
|
852
|
+
tx2.globalAlpha = cr2.range(0.25, 0.85); tx2.lineWidth = cr2.range(0.5, 1.5);
|
|
853
|
+
tx2.beginPath(); tx2.moveTo(x, y); tx2.lineTo(x + cr2.range(8, 22), y + cr2.range(-3, 6)); tx2.strokeStyle = '#000'; tx2.stroke();
|
|
854
|
+
}
|
|
855
|
+
tx2.globalCompositeOperation = 'source-over'; tx2.globalAlpha = 1;
|
|
856
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.filter = 'blur(1.6px)'; ctx.globalAlpha = 0.35; ctx.drawImage(CL, 1088, 36); ctx.filter = 'none'; ctx.globalAlpha = 1; ctx.drawImage(CL, 1088, 36); ctx.restore();
|
|
857
|
+
|
|
858
|
+
// ---------- 10. hanko seal ----------
|
|
859
|
+
const sxp = 1140, syp = 474, ss = 58;
|
|
860
|
+
const SL = H.layer(90, 90), tx3 = SL.getContext('2d'); tx3.translate(-(sxp - 16), -(syp - 16));
|
|
861
|
+
tx3.fillStyle = '#b8322a';
|
|
862
|
+
tx3.beginPath();
|
|
863
|
+
const sr = H.rng(909);
|
|
864
|
+
const corners = [[sxp, syp], [sxp + ss, syp], [sxp + ss, syp + ss], [sxp, syp + ss]];
|
|
865
|
+
for (let e = 0; e < 4; e++) {
|
|
866
|
+
const [ax, ay] = corners[e], [bx2, by2] = corners[(e + 1) % 4];
|
|
867
|
+
for (let k = 0; k < 10; k++) { const u = k / 10; const x = ax + (bx2 - ax) * u + sr.gauss() * 0.9, y = ay + (by2 - ay) * u + sr.gauss() * 0.9; e === 0 && k === 0 ? tx3.moveTo(x, y) : tx3.lineTo(x, y); }
|
|
868
|
+
}
|
|
869
|
+
tx3.closePath(); tx3.fill();
|
|
870
|
+
// carved characters (white) — 知 光 stacked as seal script stand-in
|
|
871
|
+
tx3.globalCompositeOperation = 'destination-out'; tx3.textAlign = 'center'; tx3.textBaseline = 'middle';
|
|
872
|
+
tx3.font = '700 25px "Noto Serif JP"';
|
|
873
|
+
tx3.fillText('知', sxp + ss * 0.5, syp + ss * 0.29); tx3.fillText('光', sxp + ss * 0.5, syp + ss * 0.73);
|
|
874
|
+
tx3.lineWidth = 2.2; tx3.strokeStyle = '#000'; tx3.strokeRect(sxp + 4, syp + 4, ss - 8, ss - 8);
|
|
875
|
+
// uneven stamp pressure: specks where the paper didn't take the paste
|
|
876
|
+
for (let k = 0; k < 160; k++) { tx3.globalAlpha = sr.range(0.3, 1); tx3.beginPath(); tx3.arc(sxp + sr() * ss, syp + sr() * ss, sr.range(0.3, 1.4), 0, 7); tx3.fill(); }
|
|
877
|
+
tx3.globalAlpha = 0.35; tx3.beginPath(); tx3.ellipse(sxp + ss * 0.8, syp + ss * 0.85, 22, 10, -0.6, 0, 7); tx3.fill();
|
|
878
|
+
tx3.globalCompositeOperation = 'source-over'; tx3.globalAlpha = 1;
|
|
879
|
+
const st = sp(5.15, 5.3);
|
|
880
|
+
if (st > 0) {
|
|
881
|
+
const sc = 1 + (1 - H.ease.out(st)) * 0.25, scx = sxp + ss / 2, scy = syp + ss / 2;
|
|
882
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.92 * Math.min(1, st * 1.5);
|
|
883
|
+
ctx.translate(scx, scy); ctx.scale(sc, sc); ctx.translate(-scx, -scy); ctx.drawImage(SL, sxp - 16, syp - 16); ctx.restore();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// small English caption, quietly set
|
|
887
|
+
ctx.save(); ctx.font = '600 20px "Cormorant Garamond"'; ctx.fillStyle = `rgba(40,36,32,${0.72 * ep(5.3, 5.7)})`; ctx.textAlign = 'right';
|
|
888
|
+
if (ctx.letterSpacing !== undefined) ctx.letterSpacing = '6px';
|
|
889
|
+
ctx.fillText('THE QUIET MACHINE', 1214, 590); ctx.restore();
|
|
890
|
+
|
|
891
|
+
// ---------- 11. paper absorbs the ink: fibres show through, slight bleed texture ----------
|
|
892
|
+
// fibres float above the ink a little (screen of the light strands)
|
|
893
|
+
ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.lineCap = 'round';
|
|
894
|
+
const fr = H.rng(77);
|
|
895
|
+
for (let i = 0; i < 260; i++) {
|
|
896
|
+
const x = fr() * W, y = fr() * H_, len = fr.range(14, 60), a = fr() * 6.28;
|
|
897
|
+
ctx.strokeStyle = `rgba(90,84,74,${fr.range(0.06, 0.16)})`; ctx.lineWidth = fr.range(0.5, 1.1);
|
|
898
|
+
ctx.beginPath(); ctx.moveTo(x, y); ctx.quadraticCurveTo(x + Math.cos(a + 0.5) * len * 0.5, y + Math.sin(a + 0.5) * len * 0.5, x + Math.cos(a) * len, y + Math.sin(a) * len); ctx.stroke();
|
|
899
|
+
}
|
|
900
|
+
ctx.restore();
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|