@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,934 @@
|
|
|
1
|
+
{
|
|
2
|
+
// ---------- 19th-century hand-coloured natural-history engraving ----------
|
|
3
|
+
const TAU = Math.PI * 2;
|
|
4
|
+
|
|
5
|
+
// tapered engraving line(s) -> filled ribbons. lines: [[x,y,shade],...]
|
|
6
|
+
function ribbonPath(ctx, run) {
|
|
7
|
+
const n = run.length, L = [], R = [];
|
|
8
|
+
for (let i = 0; i < n; i++) {
|
|
9
|
+
const a = run[Math.max(0, i - 1)], b = run[Math.min(n - 1, i + 1)];
|
|
10
|
+
const tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1, nx = -ty / m, ny = tx / m;
|
|
11
|
+
let w = run[i][2] / 2; if (i === 0 || i === n - 1) w *= 0.35;
|
|
12
|
+
L.push([run[i][0] + nx * w, run[i][1] + ny * w]); R.push([run[i][0] - nx * w, run[i][1] - ny * w]);
|
|
13
|
+
}
|
|
14
|
+
const poly = L.concat(R.reverse());
|
|
15
|
+
let area = 0; for (let i = 0; i < poly.length; i++) { const p = poly[i], q = poly[(i + 1) % poly.length]; area += p[0] * q[1] - q[0] * p[1]; }
|
|
16
|
+
if (area < 0) poly.reverse();
|
|
17
|
+
ctx.moveTo(poly[0][0], poly[0][1]); for (let i = 1; i < poly.length; i++) ctx.lineTo(poly[i][0], poly[i][1]); ctx.closePath();
|
|
18
|
+
}
|
|
19
|
+
function engrave(ctx, lines, maxW = 1.2, thr = 0.3, gamma = 0.8) {
|
|
20
|
+
ctx.beginPath();
|
|
21
|
+
for (const ln of lines) {
|
|
22
|
+
let run = [];
|
|
23
|
+
for (const p of ln) {
|
|
24
|
+
const w = maxW * Math.pow(H.clamp((p[2] - thr) / (1 - thr)), gamma);
|
|
25
|
+
if (w > 0.14) run.push([p[0], p[1], w]); else { if (run.length > 1) ribbonPath(ctx, run); run = []; }
|
|
26
|
+
}
|
|
27
|
+
if (run.length > 1) ribbonPath(ctx, run);
|
|
28
|
+
}
|
|
29
|
+
ctx.fill();
|
|
30
|
+
}
|
|
31
|
+
function surfLines(S, sh, n, m, along = 'u', a0 = -1, a1 = 1) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < n; i++) {
|
|
34
|
+
const line = [];
|
|
35
|
+
if (along === 'u') { const v = a0 + (a1 - a0) * (i + 0.5) / n; for (let k = 0; k <= m; k++) { const u = k / m, p = S(u, v); line.push([p[0], p[1], sh(u, v)]); } }
|
|
36
|
+
else { const u = a0 + (a1 - a0) * (i + 0.5) / n; for (let k = 0; k <= m; k++) { const v = -1 + 2 * k / m, p = S(u, v); line.push([p[0], p[1], sh(u, v)]); } }
|
|
37
|
+
out.push(line);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function outline(S, n = 48) { const o = []; for (let k = 0; k <= n; k++) o.push(S(k / n, -1)); for (let k = n; k >= 0; k--) o.push(S(k / n, 1)); return o; }
|
|
42
|
+
function poly(ctx, pts, close = true) { ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); if (close) ctx.closePath(); }
|
|
43
|
+
function erase(ctx, pts) { ctx.save(); ctx.globalCompositeOperation = 'destination-out'; poly(ctx, pts); ctx.fill(); ctx.restore(); }
|
|
44
|
+
// variable-weight outline (thicker on shadow side = lower-right)
|
|
45
|
+
function contour(ctx, pts, w0 = 0.7, w1 = 1.7, close = true) {
|
|
46
|
+
const run = [], n = pts.length;
|
|
47
|
+
for (let i = 0; i < n; i++) {
|
|
48
|
+
const a = pts[(i - 1 + n) % n], b = pts[(i + 1) % n];
|
|
49
|
+
const tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1;
|
|
50
|
+
// outward normal approx (-ty, tx) ; light from upper-left -> heavier where normal points down-right
|
|
51
|
+
const k = H.clamp(0.5 + 0.5 * ((-ty / m) * 0.6 + (tx / m) * 0.8));
|
|
52
|
+
run.push([pts[i][0], pts[i][1], w0 + (w1 - w0) * k]);
|
|
53
|
+
}
|
|
54
|
+
if (close) run.push(run[0]);
|
|
55
|
+
ctx.beginPath(); ribbonPath(ctx, run); ctx.fill();
|
|
56
|
+
}
|
|
57
|
+
function stipple(ctx, clipPts, n, dens, r, rad = 0.6) {
|
|
58
|
+
let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
|
|
59
|
+
for (const p of clipPts) { x0 = Math.min(x0, p[0]); y0 = Math.min(y0, p[1]); x1 = Math.max(x1, p[0]); y1 = Math.max(y1, p[1]); }
|
|
60
|
+
ctx.save(); poly(ctx, clipPts); ctx.clip(); ctx.beginPath();
|
|
61
|
+
for (let i = 0; i < n; i++) { const x = r.range(x0, x1), y = r.range(y0, y1); if (r() < dens(x, y)) { const rr = rad * r.range(0.6, 1.3); ctx.moveTo(x + rr, y); ctx.arc(x, y, rr, 0, TAU); } }
|
|
62
|
+
ctx.fill(); ctx.restore();
|
|
63
|
+
}
|
|
64
|
+
// watercolour wash on tint layer
|
|
65
|
+
// (explainer plates set BX_WASH: the colourist's tints dry with a darker pooled edge)
|
|
66
|
+
let BX_WASH = false;
|
|
67
|
+
function wash(tc, pts, col, a, mis = [1.5, -1], edge) {
|
|
68
|
+
if (edge === undefined) edge = BX_WASH ? 0.9 : 0.5;
|
|
69
|
+
tc.save(); tc.translate(mis[0], mis[1]);
|
|
70
|
+
poly(tc, pts); tc.fillStyle = H.rgba(col, a); tc.fill();
|
|
71
|
+
tc.strokeStyle = H.rgba(col, a * edge); tc.lineWidth = BX_WASH ? 3 : 2.2; tc.stroke();
|
|
72
|
+
tc.restore();
|
|
73
|
+
}
|
|
74
|
+
function bez(p0, p1, p2, p3, t) { const m = 1 - t; return [m * m * m * p0[0] + 3 * m * m * t * p1[0] + 3 * m * t * t * p2[0] + t * t * t * p3[0], m * m * m * p0[1] + 3 * m * m * t * p1[1] + 3 * m * t * t * p2[1] + t * t * t * p3[1]]; }
|
|
75
|
+
|
|
76
|
+
// petal / leaf surface: base point, direction angle, length, width, bend
|
|
77
|
+
function bladeS(bx, by, ang, len, wid, bend, shapeP = 0.75, tipBlunt = 0) {
|
|
78
|
+
const dx = Math.cos(ang), dy = Math.sin(ang), px = -dy, py = dx;
|
|
79
|
+
const C = (u) => [bx + dx * len * u + px * bend * len * u * u, by + dy * len * u + py * bend * len * u * u];
|
|
80
|
+
return (u, v) => {
|
|
81
|
+
const c = C(u), c2 = C(Math.min(1, u + 0.01)), c1 = C(Math.max(0, u - 0.01));
|
|
82
|
+
let tx = c2[0] - c1[0], ty = c2[1] - c1[1]; const m = Math.hypot(tx, ty) || 1; tx /= m; ty /= m;
|
|
83
|
+
const w = wid * Math.pow(Math.sin(Math.PI * Math.pow(Math.min(u, 0.999), shapeP)), 0.85) * (1 - tipBlunt * 0) ;
|
|
84
|
+
return [c[0] - ty * w * v, c[1] + tx * w * v];
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===== EXPLAINER HELPERS =====
|
|
89
|
+
let BX_PAPER = null, BX_WASHTEX = null;
|
|
90
|
+
function bxPaper(W, H_) {
|
|
91
|
+
if (BX_PAPER) return BX_PAPER;
|
|
92
|
+
const L = H.layer(W, H_), c = L.getContext('2d'), r = H.rng(1847);
|
|
93
|
+
H.paper(c, W, H_, '#efe5cf', 19, 0.04);
|
|
94
|
+
const eg = c.createRadialGradient(W / 2, H_ / 2, 300, W / 2, H_ / 2, 800); eg.addColorStop(0, 'rgba(170,130,70,0)'); eg.addColorStop(1, 'rgba(160,115,50,0.30)');
|
|
95
|
+
c.fillStyle = eg; c.fillRect(0, 0, W, H_);
|
|
96
|
+
const PM = [44, 30, W - 44, H_ - 26];
|
|
97
|
+
c.fillStyle = 'rgba(150,120,80,0.06)'; c.fillRect(PM[0], PM[1], PM[2] - PM[0], PM[3] - PM[1]);
|
|
98
|
+
c.globalCompositeOperation = 'multiply';
|
|
99
|
+
for (let i = 0; i < 110; i++) { let x = r() * W, y = r() * H_; if (r.chance(0.55)) x = r.chance(0.5) ? r() * 200 : W - r() * 200; const rad = r.range(1, r.chance(0.12) ? 12 : 4); const fg = c.createRadialGradient(x, y, 0, x, y, rad); fg.addColorStop(0, `rgba(160,100,45,${r.range(0.2, 0.5)})`); fg.addColorStop(1, 'rgba(170,110,55,0)'); c.fillStyle = fg; c.beginPath(); c.arc(x, y, rad, 0, TAU); c.fill(); }
|
|
100
|
+
c.globalCompositeOperation = 'source-over';
|
|
101
|
+
c.strokeStyle = 'rgba(110,80,40,0.35)'; c.lineWidth = 1.5; c.strokeRect(PM[0], PM[1], PM[2] - PM[0], PM[3] - PM[1]);
|
|
102
|
+
c.strokeStyle = 'rgba(255,250,235,0.55)'; c.lineWidth = 2; c.strokeRect(PM[0] - 2, PM[1] - 2, PM[2] - PM[0] + 4, PM[3] - PM[1] + 4);
|
|
103
|
+
BX_PAPER = L; return L;
|
|
104
|
+
}
|
|
105
|
+
function bxWashTex(W, H_) {
|
|
106
|
+
if (BX_WASHTEX) return BX_WASHTEX;
|
|
107
|
+
// full-res pigment granulation with the still's formula (0.7 + 0.42 fbm(0.018) + 0.14 n2(0.35)); the smooth fbm term is
|
|
108
|
+
// sampled on a 3px grid and interpolated, the granulation is per pixel
|
|
109
|
+
const nz = H.makeNoise(31), B = H.layer(W, H_), b = B.getContext('2d'), img = b.createImageData(W, H_), d = img.data;
|
|
110
|
+
const st = 3, gw = Math.ceil(W / st) + 2, gh = Math.ceil(H_ / st) + 2, G = new Float32Array(gw * gh);
|
|
111
|
+
for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) G[j * gw + i] = nz.fbm2(i * st * 0.018, j * st * 0.018, 3);
|
|
112
|
+
for (let y = 0; y < H_; y++) for (let x = 0; x < W; x++) {
|
|
113
|
+
const fx = x / st, fy = y / st, i = fx | 0, j = fy | 0, u = fx - i, v = fy - j, o = j * gw + i;
|
|
114
|
+
const f = (G[o] * (1 - u) + G[o + 1] * u) * (1 - v) + (G[o + gw] * (1 - u) + G[o + gw + 1] * u) * v;
|
|
115
|
+
d[(y * W + x) * 4 + 3] = H.clamp(0.7 + 0.42 * f + 0.14 * nz.n2(x * 0.35, y * 0.35)) * 255;
|
|
116
|
+
}
|
|
117
|
+
b.putImageData(img, 0, 0); BX_WASHTEX = B; return B;
|
|
118
|
+
}
|
|
119
|
+
const bxEll = (cx, cy, rx, ry, n = 48) => { const o = []; for (let k = 0; k < n; k++) { const a = k / n * TAU; o.push([cx + Math.cos(a) * rx, cy + Math.sin(a) * ry]); } return o; };
|
|
120
|
+
// engraved hatching across a shape: parallel lines whose weight follows a shade function
|
|
121
|
+
function bxHatch(c, shape, ang, gap, shade, maxW = 1.15) {
|
|
122
|
+
let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9; shape.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]); });
|
|
123
|
+
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2, R = Math.hypot(x1 - x0, y1 - y0) / 2 + 2, dx = Math.cos(ang), dy = Math.sin(ang), lines = [];
|
|
124
|
+
for (let o = -R; o < R; o += gap) { const ln = []; for (let k = 0; k <= 24; k++) { const u = -R + 2 * R * k / 24, x = cx - dy * o + dx * u, y = cy + dx * o + dy * u; ln.push([x, y, shade(x, y)]); } lines.push(ln); }
|
|
125
|
+
c.save(); poly(c, shape); c.clip(); engrave(c, lines, maxW, 0.25); c.restore();
|
|
126
|
+
}
|
|
127
|
+
// engraved specimen vignette: oval frame, animal head, caption
|
|
128
|
+
function bxSpecimen(c, tc, x, y, isCat, s = 1, fig = '', prog = 1) {
|
|
129
|
+
if (prog <= 0) return;
|
|
130
|
+
c.save(); tc.save(); [c, tc].forEach((k) => { k.translate(x, y); k.scale(s, s); });
|
|
131
|
+
const oval = bxEll(0, 0, 54, 64);
|
|
132
|
+
erase(c, oval); contour(c, oval, 0.6, 1.4); c.lineWidth = 0.6; poly(c, bxEll(0, 0, 50, 60)); c.stroke();
|
|
133
|
+
bxHatch(c, bxEll(0, 0, 50, 60), 0, 3.2, (px, py) => 0.3 + 0.35 * ((px + py) / 80 + 0.5) * 0.6);
|
|
134
|
+
let head;
|
|
135
|
+
if (isCat) { // one clean outline: jaw arc from the right ear round the chin to the left ear, then the two ears
|
|
136
|
+
head = []; for (let k = 0; k <= 30; k++) { const a = -0.62 + (Math.PI + 1.24) * k / 30; head.push([Math.cos(a) * 30, 6 + Math.sin(a) * 26]); }
|
|
137
|
+
head.push([-22, -36], [-6, -18], [0, -19.5], [6, -18], [22, -36]);
|
|
138
|
+
}
|
|
139
|
+
else head = bxEll(0, 4, 30, 30, 36);
|
|
140
|
+
if (!isCat) [-1, 1].forEach((sg) => { const ear = bxEll(sg * 30, 10, 11, 24, 24); erase(c, ear); wash(tc, ear, '#5e3f28', 0.65, [1.5, -1]); bxHatch(c, ear, Math.PI / 2, 2.4, () => 0.75); contour(c, ear, 0.5, 1.3); });
|
|
141
|
+
erase(c, head); wash(tc, head, isCat ? '#c99a4e' : '#a8784e', 0.6, [1.5, -1]);
|
|
142
|
+
const hsh = (px, py) => H.clamp(0.22 + 0.5 * ((px * 0.6 + py) / 40) + 0.25 * Math.pow(Math.hypot(px / 30, (py - 5) / 28), 4));
|
|
143
|
+
bxHatch(c, head, 0.15, 2.4, (px, py) => hsh(px, py) - 0.04, 0.9);
|
|
144
|
+
{ // form-following contour lines round the skull (engravers model a sphere with rings that swell on the shadow side)
|
|
145
|
+
const cy0 = isCat ? 6 : 4, ry0 = isCat ? 26 : 30, rings = [];
|
|
146
|
+
for (let k = 0; k < 7; k++) { const r = 0.92 - k * 0.11, ln = []; for (let q = 0; q <= 40; q++) { const a = q / 40 * TAU; const px = Math.cos(a) * 30 * r, py = cy0 + Math.sin(a) * ry0 * r; ln.push([px, py, H.clamp(hsh(px, py) + 0.12 - 0.25 * (1 - r))]); } rings.push(ln); }
|
|
147
|
+
c.save(); poly(c, head); c.clip(); engrave(c, rings, 0.95, 0.3); c.restore();
|
|
148
|
+
}
|
|
149
|
+
bxHatch(c, head, 1.05, 2.4, (px, py) => hsh(px, py) - 0.3, 0.85); // engraver's cross-hatch in the shadowed half
|
|
150
|
+
contour(c, head, 0.6, 1.6);
|
|
151
|
+
c.fillStyle = '#000';
|
|
152
|
+
[-1, 1].forEach((sg) => { c.beginPath(); c.ellipse(sg * 11, 0, 3.4, isCat ? 4.2 : 3.4, 0, 0, TAU); c.fill(); });
|
|
153
|
+
if (isCat) { c.beginPath(); c.moveTo(-3, 10); c.lineTo(3, 10); c.lineTo(0, 14); c.closePath(); c.fill(); c.lineWidth = 0.7; c.beginPath(); [-1, 1].forEach((sg) => { c.moveTo(sg * 6, 15); c.lineTo(sg * 30, 12); c.moveTo(sg * 6, 17); c.lineTo(sg * 28, 22); }); c.stroke(); }
|
|
154
|
+
else { const sn = bxEll(0, 16, 12, 9, 24); erase(c, sn); contour(c, sn, 0.5, 1.2); c.beginPath(); c.ellipse(0, 12, 5, 3.4, 0, 0, TAU); c.fill(); }
|
|
155
|
+
c.textAlign = 'center'; c.textBaseline = 'alphabetic'; c.font = 'italic 600 32px "Cormorant Garamond"'; c.fillText(isCat ? 'cat.' : 'dog.', 0, 96);
|
|
156
|
+
if (fig) { c.font = 'italic 400 15px "Libre Baskerville"'; c.fillText(fig, -46, -56); }
|
|
157
|
+
c.restore(); tc.restore();
|
|
158
|
+
}
|
|
159
|
+
// engraved arrow (tapered shaft, barbed head, fletched tail)
|
|
160
|
+
function bxArrow(c, pts, p, w = 2.4, label, lx, ly) {
|
|
161
|
+
if (p <= 0) return; const n = Math.max(2, Math.round(pts.length * Math.min(1, p))), P = pts.slice(0, n);
|
|
162
|
+
c.beginPath(); ribbonPath(c, P.map((q, i) => [q[0], q[1], w * (0.5 + 0.5 * i / (P.length - 1))])); c.fill();
|
|
163
|
+
const a = pts[0], b = pts[2]; const an0 = Math.atan2(b[1] - a[1], b[0] - a[0]);
|
|
164
|
+
c.lineWidth = 1; c.beginPath(); [0, 6].forEach((o) => [-1, 1].forEach((sg) => { const bx = a[0] + Math.cos(an0) * o, by = a[1] + Math.sin(an0) * o; c.moveTo(bx, by); c.lineTo(bx - Math.cos(an0 + sg * 0.6) * 9, by - Math.sin(an0 + sg * 0.6) * 9); })); c.stroke();
|
|
165
|
+
if (p >= 1) { const q = pts[pts.length - 1], o = pts[pts.length - 3], an = Math.atan2(q[1] - o[1], q[0] - o[0]); c.beginPath(); c.moveTo(q[0] + Math.cos(an) * 6, q[1] + Math.sin(an) * 6); c.lineTo(q[0] + Math.cos(an + 2.7) * 18, q[1] + Math.sin(an + 2.7) * 18); c.lineTo(q[0] - Math.cos(an) * 4, q[1] - Math.sin(an) * 4); c.lineTo(q[0] + Math.cos(an - 2.7) * 18, q[1] + Math.sin(an - 2.7) * 18); c.closePath(); c.fill(); }
|
|
166
|
+
if (label && p > 0.5) { c.font = 'italic 600 32px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText(label, lx, ly); }
|
|
167
|
+
}
|
|
168
|
+
const bxQ = (a, m, b, n = 32) => { const o = []; for (let k = 0; k <= n; k++) { const t = k / n, u = 1 - t; o.push([u * u * a[0] + 2 * u * t * m[0] + t * t * b[0], u * u * a[1] + 2 * u * t * m[1] + t * t * b[1]]); } return o; };
|
|
169
|
+
// the circuit-lily (the "model"), base of stem at (x,y)
|
|
170
|
+
function bxLily(c, tc, x, y, s, tilt = 0, grow = 1) {
|
|
171
|
+
const H0 = 230 * s * grow, hx = x + Math.sin(tilt) * 40 * s, hy = y - H0;
|
|
172
|
+
const SP = (u) => [H.lerp(x, hx, u) + Math.sin(u * Math.PI) * 14 * s, H.lerp(y, hy, u)];
|
|
173
|
+
const S = (u, v) => { const p = SP(u), w = (5 - 1.5 * u) * s; return [p[0] + w * v, p[1]]; };
|
|
174
|
+
[[0.42, -2.5, 1], [0.6, -0.55, -1]].forEach(([u0, an, sd], i) => {
|
|
175
|
+
const b = SP(u0), L = bladeS(b[0], b[1], an, 120 * s * Math.max(0.5, grow), 24 * s, an < -1.5 ? -0.15 : 0.15, 0.62), o = outline(L, 40);
|
|
176
|
+
erase(c, o); wash(tc, o, '#6d8c45', 0.55, [2, -1.5]);
|
|
177
|
+
const lsh = (u, v) => H.clamp(0.33 + 0.4 * (v * sd > 0 ? 1 : 0) + 0.3 * Math.pow(Math.abs(v), 3) + 0.2 * Math.pow(1 - u, 4));
|
|
178
|
+
engrave(c, surfLines(L, lsh, Math.max(9, Math.round(9 * s * 1.4)), 30, 'u'), 0.95, 0.3);
|
|
179
|
+
if (s > 0.7) engrave(c, surfLines(L, (u, v) => lsh(u, v) - 0.35, Math.round(16 * s), 12, 'v', 0.04, 0.96), 0.7, 0.3);
|
|
180
|
+
stipple(c, o, Math.round(260 * s * s), (px, py) => H.clamp(0.75 - Math.hypot(px - b[0], py - b[1]) / (60 * s)), H.rng(700 + i), 0.5 + 0.15 * s);
|
|
181
|
+
const mr = []; for (let k = 0; k <= 20; k++) { const q = L(k / 20 * 0.95, 0); mr.push([q[0], q[1], 1.6 * (1 - k / 20) + 0.4]); } c.beginPath(); ribbonPath(c, mr); c.fill();
|
|
182
|
+
contour(c, o, 0.5, 1.5);
|
|
183
|
+
});
|
|
184
|
+
const so = outline(S, 40); erase(c, so); wash(tc, so, '#6a8a42', 0.6, [2, -1]); engrave(c, surfLines(S, (u, v) => H.clamp(0.3 + 0.6 * v), 6, 40, 'u'), 0.8, 0.35);
|
|
185
|
+
[0.3, 0.55].forEach((u) => { if (u > grow) return; const a = S(u, -1.2), b = S(u, 1.2); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineWidth = 0.6 + 0.5 * s; c.stroke(); }); // nodes
|
|
186
|
+
contour(c, so, 0.5, 1.3);
|
|
187
|
+
const offs = [-1.05, 1.05, -0.52, 0.52, 0], base = -Math.PI / 2 + tilt;
|
|
188
|
+
offs.forEach((d, i) => {
|
|
189
|
+
const P = bladeS(hx, hy, base + d, (i === 4 ? 78 : 96) * s, 30 * s, d * 0.14, 0.72), o = outline(P, 40);
|
|
190
|
+
erase(c, o); wash(tc, o, i < 2 ? '#a83a52' : '#c94f66', 0.55, [2.5, -1.5]);
|
|
191
|
+
// the still's darker tint pooled at the petal base
|
|
192
|
+
const pb = []; for (let k = 0; k <= 16; k++) pb.push(P(k / 16 * 0.45, -1)); for (let k = 16; k >= 0; k--) pb.push(P(k / 16 * 0.45, 1));
|
|
193
|
+
wash(tc, pb, '#7a2a48', 0.26, [2.5, -1.5], 0.1);
|
|
194
|
+
// engraving density follows the plate scale, so the big lilies get the still's close-set swelling lines
|
|
195
|
+
const sh = (u, v) => H.clamp(0.2 + 0.36 * v * (d > 0 ? -1 : 1) + 0.5 * Math.pow(1 - u, 3.5) + 0.25 * Math.pow(Math.abs(v), 4) + (i < 2 ? 0.22 : 0.16) + 0.06 * Math.sin(u * 9 + i * 2.1 + v * 1.7) - 0.12 * Math.pow(u, 3));
|
|
196
|
+
const nl = Math.max(13, Math.round(20 * s));
|
|
197
|
+
engrave(c, surfLines(P, sh, nl, 30, 'u'), s > 1 ? 1.05 : 1.1, 0.25);
|
|
198
|
+
engrave(c, surfLines(P, (u, v) => sh(u, v) - 0.3, Math.round(nl * 1.1), 18, 'v', 0.02, 0.98), 0.8, 0.3); // cross-hatch in the deepest shadow
|
|
199
|
+
const mr = []; for (let k = 0; k <= 20; k++) { const q = P(k / 20 * 0.9, 0); mr.push([q[0], q[1], (1.1 * (1 - k / 20) + 0.3) * Math.min(1.3, s)]); } c.beginPath(); ribbonPath(c, mr); c.fill();
|
|
200
|
+
stipple(c, o, Math.round(420 * s * s), (px, py) => H.clamp(0.8 - Math.hypot(px - hx, py - hy) / (34 * s)), H.rng(720 + i), 0.45 + 0.15 * s);
|
|
201
|
+
// gold circuit trace
|
|
202
|
+
const a0 = P(0.25, 0.3), a1 = P(0.5, 0.3), a2 = P(0.68, 0.55);
|
|
203
|
+
c.beginPath(); c.moveTo(a0[0], a0[1]); c.lineTo(a1[0], a1[1]); c.lineTo(a2[0], a2[1]); c.lineWidth = 2.4 * s + 0.6; c.stroke(); c.beginPath(); c.arc(a2[0], a2[1], 2.8 * s + 0.6, 0, TAU); c.fill();
|
|
204
|
+
// engraved as a hollow double line (as in the still), so the gilt tint shows through it
|
|
205
|
+
c.save(); c.globalCompositeOperation = 'destination-out'; c.beginPath(); c.moveTo(a0[0], a0[1]); c.lineTo(a1[0], a1[1]); c.lineTo(a2[0], a2[1]); c.lineWidth = (2.4 * s + 0.6) * 0.45; c.stroke(); c.beginPath(); c.arc(a2[0], a2[1], (2.8 * s + 0.6) * 0.6, 0, TAU); c.fill(); c.restore();
|
|
206
|
+
tc.save(); tc.strokeStyle = 'rgba(214,160,70,0.85)'; tc.lineWidth = 3 * s + 1; tc.beginPath(); tc.moveTo(a0[0] + 1, a0[1]); tc.lineTo(a1[0] + 1, a1[1]); tc.lineTo(a2[0] + 1, a2[1]); tc.stroke(); tc.restore();
|
|
207
|
+
contour(c, o, 0.6, 1.7);
|
|
208
|
+
});
|
|
209
|
+
return [hx, hy];
|
|
210
|
+
}
|
|
211
|
+
function bxDial(c, tc, x, y, R, ang) {
|
|
212
|
+
const o = bxEll(x, y, R, R, 40); erase(c, o); wash(tc, o, '#c8a25a', 0.55, [1.5, -1]);
|
|
213
|
+
bxHatch(c, o, Math.PI / 4, 2.6, (px, py) => H.clamp(0.3 + 0.5 * ((px - x) + (py - y)) / R / 1.4 + 0.3));
|
|
214
|
+
const f = bxEll(x, y, R * 0.72, R * 0.72, 40); erase(c, f); contour(c, f, 0.6, 1.1); contour(c, o, 0.8, 2);
|
|
215
|
+
c.lineWidth = 0.9; c.beginPath(); for (let k = 0; k < 12; k++) { const a = k / 12 * TAU; c.moveTo(x + Math.cos(a) * R * 0.58, y + Math.sin(a) * R * 0.58); c.lineTo(x + Math.cos(a) * R * 0.7, y + Math.sin(a) * R * 0.7); } c.stroke();
|
|
216
|
+
c.beginPath(); ribbonPath(c, [[x - Math.cos(ang) * 6, y - Math.sin(ang) * 6, 3], [x, y, 4], [x + Math.cos(ang) * R * 0.6, y + Math.sin(ang) * R * 0.6, 0.6]]); c.fill();
|
|
217
|
+
c.beginPath(); c.arc(x, y, 3, 0, TAU); c.fill();
|
|
218
|
+
}
|
|
219
|
+
function bxCartouche(c, tc, x, y, w, h, lines, sizes) {
|
|
220
|
+
const o = []; for (let k = 0; k < 60; k++) { const a = k / 60 * TAU, sx = Math.cos(a), sy = Math.sin(a); o.push([x + sx * w / 2 * (1 - 0.06 * Math.pow(Math.abs(sy), 6)), y + sy * h / 2 * (1 + 0.05 * Math.cos(a * 4))]); }
|
|
221
|
+
erase(c, o); wash(tc, o, '#e9d9a8', 0.5, [1.5, -1]);
|
|
222
|
+
contour(c, o, 0.8, 2.2); c.lineWidth = 0.6; poly(c, bxEll(x, y, w / 2 - 6, h / 2 - 6, 60)); c.stroke();
|
|
223
|
+
[-1, 1].forEach((sg) => { c.beginPath(); ribbonPath(c, bxQ([x + sg * w / 2, y], [x + sg * (w / 2 + 30), y - 26], [x + sg * (w / 2 + 14), y - 40], 14).map((q, i) => [q[0], q[1], 2.4 * (1 - i / 14) + 0.4])); c.fill(); c.beginPath(); c.arc(x + sg * (w / 2 + 14), y - 40, 3, 0, TAU); c.fill(); });
|
|
224
|
+
c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
225
|
+
const tot = sizes.reduce((a, b) => a + b, 0); let yy = y - tot / 2;
|
|
226
|
+
lines.forEach((l, i) => { yy += sizes[i] / 2; c.font = i === 0 && lines.length > 1 ? `italic 400 ${sizes[i]}px "Cormorant Garamond"` : `600 ${sizes[i]}px "Cormorant Garamond"`; c.fillText(l, x, yy); yy += sizes[i] / 2; });
|
|
227
|
+
c.textBaseline = 'alphabetic';
|
|
228
|
+
}
|
|
229
|
+
function bxCompose(W, H_, fn, T, plateNo) {
|
|
230
|
+
const out = H.layer(W, H_), o = out.getContext('2d');
|
|
231
|
+
o.drawImage(bxPaper(W, H_), 0, 0);
|
|
232
|
+
const ink = H.layer(W, H_), c = ink.getContext('2d'), tint = H.layer(W, H_), tc = tint.getContext('2d');
|
|
233
|
+
c.fillStyle = '#000'; c.strokeStyle = '#000'; c.lineCap = 'round'; c.lineJoin = 'round';
|
|
234
|
+
c.lineWidth = 1.4; c.strokeRect(62, 42, W - 124, H_ - 76); c.lineWidth = 0.6; c.strokeRect(67, 47, W - 134, H_ - 86);
|
|
235
|
+
c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'right'; c.fillText(plateNo, W - 74, 62); c.textAlign = 'left'; c.fillText('Vol. III.', 74, 62);
|
|
236
|
+
fn(c, tc, T);
|
|
237
|
+
tc.globalCompositeOperation = 'destination-in'; tc.drawImage(bxWashTex(W, H_), 0, 0);
|
|
238
|
+
o.globalCompositeOperation = 'multiply'; o.filter = 'blur(0.8px)'; o.drawImage(tint, 0, 0); o.filter = 'none';
|
|
239
|
+
o.globalAlpha = 0.25; o.filter = 'blur(0.9px)'; o.drawImage(ink, 0, 0); o.filter = 'none'; o.globalAlpha = 0.92; o.drawImage(ink, 0, 0);
|
|
240
|
+
o.globalCompositeOperation = 'lighten'; o.globalAlpha = 1; o.fillStyle = 'rgb(38,30,24)'; o.fillRect(0, 0, W, H_);
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
// watercolour bloom (red-ochre emphasis ring painted by the colourist)
|
|
244
|
+
function bxRing(tc, x, y, rx, ry, p, col = '#b8382c') {
|
|
245
|
+
if (p <= 0) return; tc.save(); tc.strokeStyle = H.rgba(col, 0.75); tc.lineCap = 'round';
|
|
246
|
+
[9, 4].forEach((w, k) => { tc.lineWidth = w; tc.beginPath(); const a0 = -0.6; for (let i = 0; i <= 48 * p; i++) { const a = a0 + i / 48 * TAU * 1.08; const wob = 1 + 0.03 * Math.sin(a * 5 + k); const px = x + Math.cos(a) * rx * wob, py = y + Math.sin(a) * ry * wob; i ? tc.lineTo(px, py) : tc.moveTo(px, py); } tc.stroke(); });
|
|
247
|
+
tc.restore();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ===== END EXPLAINER HELPERS =====
|
|
251
|
+
registerStyle({
|
|
252
|
+
id: 'botanical-plate',
|
|
253
|
+
name: 'Botanical Plate',
|
|
254
|
+
category: 'Illustration',
|
|
255
|
+
description: 'A hand-coloured copperplate natural-history plate: swelling engraved hatching that follows every form, loose transparent watercolour tints laid over the print, numbered figures, Latin italic captions and foxed rag paper with a pressed plate mark.',
|
|
256
|
+
motion: 'The circuit-flower sways on its stem while the copper beetle flexes its antennae; tints bleed in a beat after the engraved lines appear.',
|
|
257
|
+
fonts: ['italic 400 30px "Cormorant Garamond"', 'italic 400 16px "Cormorant Garamond"', '600 14px "Cormorant Garamond"', '400 14px "Libre Baskerville"', 'italic 400 15px "Libre Baskerville"'],
|
|
258
|
+
frameT: 5.4,
|
|
259
|
+
duration: 5.5,
|
|
260
|
+
readsPixels: true,
|
|
261
|
+
draw(ctx, W, H_, t) {
|
|
262
|
+
BX_WASH = false;
|
|
263
|
+
const r = H.rng(1847), nz = H.makeNoise(31);
|
|
264
|
+
// ---------- paper ----------
|
|
265
|
+
H.paper(ctx, W, H_, '#efe5cf', 19, 0.04);
|
|
266
|
+
const eg = ctx.createRadialGradient(W / 2, H_ / 2, 300, W / 2, H_ / 2, 800);
|
|
267
|
+
eg.addColorStop(0, 'rgba(170,130,70,0)'); eg.addColorStop(1, 'rgba(160,115,50,0.30)');
|
|
268
|
+
ctx.fillStyle = eg; ctx.fillRect(0, 0, W, H_);
|
|
269
|
+
// plate tone inside plate mark
|
|
270
|
+
const PM = [44, 30, W - 44, H_ - 26];
|
|
271
|
+
ctx.fillStyle = 'rgba(150,120,80,0.06)'; ctx.fillRect(PM[0], PM[1], PM[2] - PM[0], PM[3] - PM[1]);
|
|
272
|
+
|
|
273
|
+
const ink = H.layer(W, H_), c = ink.getContext('2d');
|
|
274
|
+
const tint = H.layer(W, H_), tc = tint.getContext('2d');
|
|
275
|
+
c.fillStyle = '#000'; c.strokeStyle = '#000'; c.lineCap = 'round'; c.lineJoin = 'round';
|
|
276
|
+
const sway = Math.sin(t * 1.1) * 0.025;
|
|
277
|
+
|
|
278
|
+
// ================= FIG. 1 — Silicoflora =================
|
|
279
|
+
c.save(); tc.save(); c.translate(28, 0); tc.translate(28, 0);
|
|
280
|
+
const S0 = [292, 586], S1 = [300, 470], S2 = [356, 340], S3 = [336 + sway * 300, 236];
|
|
281
|
+
const stemAt = (u) => bez(S0, S1, S2, S3, u);
|
|
282
|
+
const F = stemAt(1);
|
|
283
|
+
|
|
284
|
+
// --- leaves (drawn before stem) ---
|
|
285
|
+
const leaf = (u0, ang, len, wid, bend, sd, seed) => {
|
|
286
|
+
const b = stemAt(u0);
|
|
287
|
+
const S = bladeS(b[0], b[1], ang + sway, len, wid, bend, 0.62);
|
|
288
|
+
const out = outline(S, 60);
|
|
289
|
+
erase(c, out);
|
|
290
|
+
wash(tc, out, '#6d8c45', 0.55, [2, -1.5]);
|
|
291
|
+
// darker re-wash on shadow half
|
|
292
|
+
const half = []; for (let k = 0; k <= 40; k++) half.push(S(k / 40, 0)); for (let k = 40; k >= 0; k--) half.push(S(k / 40, sd));
|
|
293
|
+
wash(tc, half, '#3e5a2c', 0.32, [2, -1.5], 0.2);
|
|
294
|
+
// vein-following engraving
|
|
295
|
+
const sh = (u, v) => H.clamp(0.22 + 0.5 * (v * sd > 0 ? 1 : 0) * (0.6 + 0.4 * Math.abs(v)) + 0.3 * Math.pow(Math.abs(v), 3) + 0.25 * Math.pow(1 - u, 4) + 0.08 * nz.n2(u * 9 + seed, v * 3));
|
|
296
|
+
const lines = [];
|
|
297
|
+
const nk = 34;
|
|
298
|
+
for (let k = 0; k < nk; k++) {
|
|
299
|
+
const u0k = k / nk;
|
|
300
|
+
[-1, 1].forEach((sg) => {
|
|
301
|
+
const ln = [];
|
|
302
|
+
for (let j = 0; j <= 10; j++) { const v = sg * j / 10, u = Math.min(0.995, u0k + 0.22 * Math.abs(v) * (1 - u0k * 0.5)); const p = S(u, v); ln.push([p[0], p[1], sh(u, v)]); }
|
|
303
|
+
lines.push(ln);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
engrave(c, lines, 1.05, 0.3);
|
|
307
|
+
// midrib
|
|
308
|
+
const mr = []; for (let k = 0; k <= 30; k++) { const p = S(k / 30 * 0.97, 0); mr.push([p[0], p[1], 2.6 * (1 - k / 30) + 0.5]); }
|
|
309
|
+
c.beginPath(); ribbonPath(c, mr); c.fill();
|
|
310
|
+
// circuit traces as veins
|
|
311
|
+
c.save(); c.lineCap = 'round';
|
|
312
|
+
for (let k = 0; k < 5; k++) {
|
|
313
|
+
const uu = 0.18 + k * 0.15, sg = k % 2 ? 1 : -1;
|
|
314
|
+
const a = S(uu, 0), m1 = S(uu + 0.06, sg * 0.35), e = S(uu + 0.14, sg * 0.78);
|
|
315
|
+
const tr = [a, m1, e];
|
|
316
|
+
c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(m1[0], m1[1]); c.lineTo(e[0], e[1]); c.lineWidth = 3.4; c.stroke();
|
|
317
|
+
c.beginPath(); c.arc(e[0], e[1], 3.6, 0, TAU); c.fill();
|
|
318
|
+
c.save(); c.globalCompositeOperation = 'destination-out';
|
|
319
|
+
c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(m1[0], m1[1]); c.lineTo(e[0], e[1]); c.lineWidth = 1.6; c.stroke();
|
|
320
|
+
c.beginPath(); c.arc(e[0], e[1], 2.3, 0, TAU); c.fill(); c.restore();
|
|
321
|
+
c.beginPath(); c.arc(e[0], e[1], 0.9, 0, TAU); c.fill();
|
|
322
|
+
tc.save(); tc.strokeStyle = 'rgba(206,150,60,0.75)'; tc.lineWidth = 3; tc.beginPath(); tc.moveTo(a[0] + 1, a[1]); tc.lineTo(m1[0] + 1, m1[1]); tc.lineTo(e[0] + 1, e[1]); tc.stroke();
|
|
323
|
+
tc.fillStyle = 'rgba(206,150,60,0.8)'; tc.beginPath(); tc.arc(e[0] + 1, e[1], 3.5, 0, TAU); tc.fill(); tc.restore();
|
|
324
|
+
}
|
|
325
|
+
c.restore();
|
|
326
|
+
contour(c, out, 0.6, 1.7);
|
|
327
|
+
};
|
|
328
|
+
leaf(0.30, -2.55, 205, 40, -0.16, 1, 1);
|
|
329
|
+
leaf(0.50, -0.42, 182, 36, 0.18, -1, 7);
|
|
330
|
+
leaf(0.70, -2.25, 120, 26, -0.2, 1, 3);
|
|
331
|
+
|
|
332
|
+
// --- side bud ---
|
|
333
|
+
{
|
|
334
|
+
const b = stemAt(0.62), tip = [470 + sway * 200, 300];
|
|
335
|
+
const bs = [[b[0], b[1]], [b[0] + 40, b[1] - 30], [tip[0] - 40, tip[1] + 50], [tip[0], tip[1]]];
|
|
336
|
+
const ped = []; for (let k = 0; k <= 30; k++) { const p = bez(bs[0], bs[1], bs[2], bs[3], k / 30); ped.push([p[0], p[1], 3.2 - k / 30 * 1.0]); }
|
|
337
|
+
c.beginPath(); ribbonPath(c, ped); c.fill();
|
|
338
|
+
tc.save(); tc.strokeStyle = 'rgba(100,130,60,0.6)'; tc.lineWidth = 3; poly(tc, ped, false); tc.stroke(); tc.restore();
|
|
339
|
+
const BS = bladeS(tip[0], tip[1], -1.05 + sway, 78, 17, 0.08, 0.55);
|
|
340
|
+
const bo = outline(BS, 40);
|
|
341
|
+
erase(c, bo); wash(tc, bo, '#b8475e', 0.5, [2, -1]);
|
|
342
|
+
engrave(c, surfLines(BS, (u, v) => H.clamp(0.25 + 0.55 * (v + 1) / 2 + 0.3 * Math.pow(1 - u, 3)), 9, 30, 'u'), 1.05, 0.3);
|
|
343
|
+
// sepals
|
|
344
|
+
[-0.5, 0.4].forEach((da, i) => {
|
|
345
|
+
const SS = bladeS(tip[0], tip[1], -1.05 + da + sway, 34, 8, i ? 0.25 : -0.25, 0.6);
|
|
346
|
+
const so = outline(SS, 24); erase(c, so); wash(tc, so, '#55753a', 0.6, [2, -1]);
|
|
347
|
+
engrave(c, surfLines(SS, (u, v) => 0.5 + 0.3 * v, 4, 20, 'u'), 0.9, 0.3); contour(c, so, 0.6, 1.3);
|
|
348
|
+
});
|
|
349
|
+
contour(c, bo, 0.6, 1.5);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --- stem (tube) ---
|
|
353
|
+
{
|
|
354
|
+
const S = (u, v) => { const p = stemAt(u), p2 = stemAt(Math.min(1, u + 0.01)), p1 = stemAt(Math.max(0, u - 0.01)); let tx = p2[0] - p1[0], ty = p2[1] - p1[1]; const m = Math.hypot(tx, ty); const w = 5.5 - u * 1.8; return [p[0] - ty / m * w * v, p[1] + tx / m * w * v]; };
|
|
355
|
+
const o = outline(S, 60); erase(c, o); wash(tc, o, '#6a8a42', 0.6, [2, -1]);
|
|
356
|
+
engrave(c, surfLines(S, (u, v) => H.clamp(0.3 + 0.6 * v), 4, 60, 'u'), 1.0, 0.35);
|
|
357
|
+
// nodes
|
|
358
|
+
[0.3, 0.5, 0.62, 0.7].forEach((u) => { const a = S(u, -1.2), b = S(u, 1.2); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineWidth = 1.2; c.stroke(); });
|
|
359
|
+
contour(c, o, 0.6, 1.5);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- bulb + wire roots ---
|
|
363
|
+
{
|
|
364
|
+
const bc = [S0[0] - 2, S0[1] + 8];
|
|
365
|
+
const roots = H.rng(8);
|
|
366
|
+
c.lineCap = 'round';
|
|
367
|
+
for (let k = 0; k < 13; k++) {
|
|
368
|
+
let x = bc[0] + roots.range(-18, 18), y = bc[1] + 14, a = Math.PI / 2 + roots.range(-1.1, 1.1);
|
|
369
|
+
const ln = [[x, y, 1.6]];
|
|
370
|
+
const steps = roots.int(6, 11);
|
|
371
|
+
for (let j = 0; j < steps; j++) { a += roots.range(-0.35, 0.35); x += Math.cos(a) * 4.2; y += Math.sin(a) * 3.2; ln.push([x, y, 1.6 - j / steps * 1.1]); }
|
|
372
|
+
c.beginPath(); ribbonPath(c, ln); c.fill();
|
|
373
|
+
if (roots.chance(0.6)) { c.beginPath(); c.arc(x, y, 1.5, 0, TAU); c.fill(); }
|
|
374
|
+
}
|
|
375
|
+
const bo = []; for (let k = 0; k <= 50; k++) { const a = k / 50 * TAU; const rr = 1 - 0.18 * Math.pow(Math.max(0, -Math.sin(a)), 3); bo.push([bc[0] + Math.cos(a) * 26 * rr, bc[1] + Math.sin(a) * 19 - Math.max(0, -Math.sin(a)) * 6]); }
|
|
376
|
+
erase(c, bo); wash(tc, bo, '#b88a4a', 0.55, [2, -1]);
|
|
377
|
+
// scale arcs
|
|
378
|
+
const sl = [];
|
|
379
|
+
for (let k = -3; k <= 3; k++) { const ln = []; for (let j = 0; j <= 20; j++) { const v = -1 + j / 10; const x = bc[0] + k * 7 + Math.sin(v * 1.4) * 2, y = bc[1] + v * 18 * Math.cos(k * 0.22); ln.push([x, y, H.clamp(0.35 + 0.08 * k + 0.25 * Math.abs(v))]); } sl.push(ln); }
|
|
380
|
+
c.save(); poly(c, bo); c.clip(); engrave(c, sl, 1.0, 0.3); c.restore();
|
|
381
|
+
// capacitor stripe band
|
|
382
|
+
c.save(); poly(c, bo); c.clip(); c.fillStyle = 'rgba(0,0,0,0)';
|
|
383
|
+
tc.save(); poly(tc, bo); tc.clip(); tc.fillStyle = 'rgba(70,90,120,0.45)'; tc.fillRect(bc[0] + 10, bc[1] - 30, 9, 60); tc.restore();
|
|
384
|
+
c.fillStyle = '#000'; for (let k = 0; k < 3; k++) c.fillRect(bc[0] + 12, bc[1] - 9 + k * 7, 5, 1.2);
|
|
385
|
+
c.restore();
|
|
386
|
+
contour(c, bo, 0.7, 1.7);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- flower head ---
|
|
390
|
+
{
|
|
391
|
+
const petals = [
|
|
392
|
+
{ a: -1.62, len: 128, wid: 44, bend: 0.12, sd: -1, back: 1 },
|
|
393
|
+
{ a: -2.55, len: 150, wid: 46, bend: -0.2, sd: 1, back: 1 },
|
|
394
|
+
{ a: -0.62, len: 152, wid: 46, bend: 0.2, sd: -1, back: 1 },
|
|
395
|
+
{ a: 2.75, len: 130, wid: 44, bend: -0.28, sd: 1, back: 0 },
|
|
396
|
+
{ a: 0.42, len: 136, wid: 44, bend: 0.28, sd: -1, back: 0 },
|
|
397
|
+
{ a: 1.62, len: 72, wid: 40, bend: 0.05, sd: 1, back: 0 },
|
|
398
|
+
];
|
|
399
|
+
petals.forEach((p, i) => {
|
|
400
|
+
const S = bladeS(F[0], F[1], p.a + sway * 0.6, p.len, p.wid, p.bend, 0.72);
|
|
401
|
+
const o = outline(S, 60);
|
|
402
|
+
erase(c, o);
|
|
403
|
+
wash(tc, o, p.back ? '#a83a52' : '#c94f66', 0.52, [2.5, -1.5]);
|
|
404
|
+
const base = []; for (let k = 0; k <= 20; k++) base.push(S(k / 20 * 0.45, -1)); for (let k = 20; k >= 0; k--) base.push(S(k / 20 * 0.45, 1));
|
|
405
|
+
wash(tc, base, '#7a2a48', 0.28, [2.5, -1.5], 0.1);
|
|
406
|
+
const sh = (u, v) => H.clamp(0.2 + 0.36 * v * p.sd + 0.5 * Math.pow(1 - u, 3.5) + 0.25 * Math.pow(Math.abs(v), 4) + p.back * 0.12 + 0.07 * nz.n2(u * 7 + i * 3, v * 2));
|
|
407
|
+
engrave(c, surfLines(S, sh, 22, 46, 'u'), 1.35, 0.25);
|
|
408
|
+
// cross-hatch in deepest shadow
|
|
409
|
+
engrave(c, surfLines(S, (u, v) => sh(u, v) - 0.3, 26, 22, 'v', 0.02, 0.98), 0.9, 0.3);
|
|
410
|
+
// midrib
|
|
411
|
+
const mr = []; for (let k = 0; k <= 24; k++) { const q = S(k / 24 * 0.92, 0.0); mr.push([q[0], q[1], 1.3 * (1 - k / 24) + 0.3]); }
|
|
412
|
+
c.beginPath(); ribbonPath(c, mr); c.fill();
|
|
413
|
+
// circuit traces along the petal
|
|
414
|
+
[-0.42, 0.42].forEach((vv, j) => {
|
|
415
|
+
if (p.len < 100 && j) return;
|
|
416
|
+
const a0 = S(0.18, vv * 0.5), a1 = S(0.42, vv * 0.5), a2 = S(0.55, vv), a3 = S(0.7 + 0.06 * j, vv);
|
|
417
|
+
const tr = [a0, a1, a2, a3];
|
|
418
|
+
c.beginPath(); poly(c, tr, false); c.lineWidth = 3.3; c.stroke();
|
|
419
|
+
c.beginPath(); c.arc(a3[0], a3[1], 3.4, 0, TAU); c.fill();
|
|
420
|
+
c.save(); c.globalCompositeOperation = 'destination-out'; poly(c, tr, false); c.lineWidth = 1.5; c.stroke(); c.beginPath(); c.arc(a3[0], a3[1], 2.2, 0, TAU); c.fill(); c.restore();
|
|
421
|
+
c.beginPath(); c.arc(a3[0], a3[1], 0.8, 0, TAU); c.fill();
|
|
422
|
+
tc.save(); tc.strokeStyle = 'rgba(214,160,70,0.85)'; tc.lineWidth = 3; tc.translate(1.5, -1); poly(tc, tr, false); tc.stroke(); tc.beginPath(); tc.arc(a3[0], a3[1], 3.4, 0, TAU); tc.fillStyle = 'rgba(214,160,70,0.85)'; tc.fill(); tc.restore();
|
|
423
|
+
});
|
|
424
|
+
contour(c, o, 0.6, 1.8);
|
|
425
|
+
});
|
|
426
|
+
// stamens with solder-ball anthers
|
|
427
|
+
const st = [[-1.95, 70], [-1.55, 82], [-1.2, 74], [-2.35, 60], [-0.85, 62], [-1.75, 56]];
|
|
428
|
+
st.forEach(([a, len], i) => {
|
|
429
|
+
const aa = a + sway + Math.sin(t * 2 + i) * 0.03;
|
|
430
|
+
const e = [F[0] + Math.cos(aa) * len, F[1] + Math.sin(aa) * len];
|
|
431
|
+
const m = [F[0] + Math.cos(aa + 0.25) * len * 0.5, F[1] + Math.sin(aa + 0.25) * len * 0.5];
|
|
432
|
+
const fl = []; for (let k = 0; k <= 16; k++) { const tt = k / 16, x = (1 - tt) * (1 - tt) * F[0] + 2 * (1 - tt) * tt * m[0] + tt * tt * e[0], y = (1 - tt) * (1 - tt) * F[1] + 2 * (1 - tt) * tt * m[1] + tt * tt * e[1]; fl.push([x, y, 1.5 - tt * 0.6]); }
|
|
433
|
+
c.beginPath(); ribbonPath(c, fl); c.fill();
|
|
434
|
+
tc.save(); tc.strokeStyle = 'rgba(190,170,60,0.55)'; tc.lineWidth = 2.5; poly(tc, fl, false); tc.stroke(); tc.restore();
|
|
435
|
+
const R = 5.2;
|
|
436
|
+
const ball = []; for (let k = 0; k < 24; k++) ball.push([e[0] + Math.cos(k / 24 * TAU) * R, e[1] + Math.sin(k / 24 * TAU) * R]);
|
|
437
|
+
erase(c, ball);
|
|
438
|
+
wash(tc, ball, '#9aa0a6', 0.6, [1, -0.5]);
|
|
439
|
+
// crescent shading lines
|
|
440
|
+
const cl = []; for (let k = 0; k < 5; k++) { const ln = []; for (let j = 0; j <= 10; j++) { const a2 = -0.6 + j / 10 * 2.6; const rr = R * (0.95 - k * 0.15); ln.push([e[0] + Math.cos(a2) * rr, e[1] + Math.sin(a2) * rr, H.clamp(0.9 - k * 0.12 - Math.abs(a2 - 0.7) * 0.25)]); } cl.push(ln); }
|
|
441
|
+
engrave(c, cl, 0.9, 0.3);
|
|
442
|
+
contour(c, ball, 0.6, 1.4);
|
|
443
|
+
});
|
|
444
|
+
// pistil: tiny die with legs
|
|
445
|
+
{
|
|
446
|
+
const ang = -1.6 + sway, ca = Math.cos(ang), sa = Math.sin(ang);
|
|
447
|
+
const T = (x, y) => [F[0] + x * -sa + y * ca, F[1] + x * ca + y * sa];
|
|
448
|
+
// style column
|
|
449
|
+
const col = [T(-3, 0), T(3, 0), T(3, 38), T(-3, 38)];
|
|
450
|
+
erase(c, col); wash(tc, col, '#a9b34e', 0.6, [1, -1]);
|
|
451
|
+
engrave(c, [0, 1, 2].map((k) => { const ln = []; for (let j = 0; j <= 10; j++) { const p = T(-1.5 + k * 1.5, j * 3.8); ln.push([p[0], p[1], 0.4 + k * 0.25]); } return ln; }), 0.9, 0.3);
|
|
452
|
+
contour(c, col, 0.6, 1.2);
|
|
453
|
+
const die = [T(-9, 38), T(9, 38), T(9, 54), T(-9, 54)];
|
|
454
|
+
c.lineWidth = 1.1;
|
|
455
|
+
for (let k = 0; k < 4; k++) { [[-9, -13], [9, 13]].forEach(([x0, x1]) => { const a = T(x0, 41 + k * 3.8), b = T(x1, 41 + k * 3.8); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.stroke(); }); }
|
|
456
|
+
erase(c, die); wash(tc, die, '#3d3a3a', 0.45, [1, -1]);
|
|
457
|
+
engrave(c, [0, 1, 2, 3, 4, 5].map((k) => [T(-8, 39.5 + k * 2.6), T(8, 39.5 + k * 2.6)].map((p, j) => [p[0], p[1], 0.65 + j * 0.2])), 0.9, 0.3);
|
|
458
|
+
contour(c, die, 0.8, 1.4);
|
|
459
|
+
const dot = T(-5.5, 50.5); c.beginPath(); c.arc(dot[0], dot[1], 1.4, 0, TAU); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ================= FIG. 2 — Scarabaeus machinalis =================
|
|
464
|
+
c.restore(); tc.restore();
|
|
465
|
+
c.save(); tc.save();
|
|
466
|
+
[c, tc].forEach((k) => { k.translate(958, 300); k.scale(0.86, 0.86); k.translate(-948, -300); });
|
|
467
|
+
{
|
|
468
|
+
const bx = 948, by = 112;
|
|
469
|
+
const L = (x, y) => [bx + x, by + y];
|
|
470
|
+
const legPhase = Math.sin(t * 3) * 0.04;
|
|
471
|
+
// legs first (explicit joint chains: coxa, femur, tibia, tarsi)
|
|
472
|
+
const legDefs = [
|
|
473
|
+
[[40, 128], [92, 112], [118, 58], [124, 40], [128, 26], [131, 12]],
|
|
474
|
+
[[50, 188], [116, 196], [158, 244], [168, 262], [176, 278], [182, 294]],
|
|
475
|
+
[[44, 236], [110, 282], [128, 356], [131, 376], [134, 394], [137, 412]],
|
|
476
|
+
];
|
|
477
|
+
[-1, 1].forEach((sg) => legDefs.forEach((def, li) => {
|
|
478
|
+
const wob = Math.sin(t * 3 + li * 2 + sg) * 3;
|
|
479
|
+
const pts = def.map(([x, y], k) => L(sg * (x + (k > 1 ? wob * 0.5 : 0)), y + (k > 1 ? wob : 0)));
|
|
480
|
+
const widths = [15, 11, 5.6, 5, 4.4];
|
|
481
|
+
for (let k = 0; k < pts.length - 1; k++) {
|
|
482
|
+
const p0 = pts[k], p1 = pts[k + 1], len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), w = widths[k];
|
|
483
|
+
const px = -(p1[1] - p0[1]) / len, py = (p1[0] - p0[0]) / len;
|
|
484
|
+
const S = (u, v) => { const ww = w * (k === 0 ? (0.75 + 0.35 * Math.sin(Math.PI * u)) : (1 - 0.3 * u)) * Math.sqrt(Math.max(0, 1 - Math.pow(2 * u - 1, 10))); return [p0[0] + (p1[0] - p0[0]) * u + px * ww * v / 2, p0[1] + (p1[1] - p0[1]) * u + py * ww * v / 2]; };
|
|
485
|
+
const o = outline(S, 24);
|
|
486
|
+
erase(c, o); wash(tc, o, k < 2 ? '#4a5a4a' : '#6a5532', 0.6, [1.5, -1]);
|
|
487
|
+
engrave(c, surfLines(S, (u, v) => H.clamp(0.3 + 0.55 * v * sg * Math.sign(py || 1) + 0.1), k < 2 ? 6 : 2, 24, 'u'), 1.0, 0.3);
|
|
488
|
+
contour(c, o, 0.6, 1.5);
|
|
489
|
+
// tibial spines
|
|
490
|
+
if (k === 1) for (let j = 1; j < 6; j++) { const q = S(j / 6, 1), q2 = [q[0] + px * 5 + (p1[0] - p0[0]) / len * 3, q[1] + py * 5 + (p1[1] - p0[1]) / len * 3]; c.beginPath(); c.moveTo(q[0], q[1]); c.lineTo(q2[0], q2[1]); c.lineWidth = 1.1; c.stroke(); }
|
|
491
|
+
if (k < 2) { const j = p1; c.beginPath(); c.arc(j[0], j[1], 3.6, 0, TAU); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore(); c.lineWidth = 1; c.stroke(); c.beginPath(); c.moveTo(j[0] - 2.2, j[1] - 1); c.lineTo(j[0] + 2.2, j[1] + 1); c.stroke(); wash(tc, [[j[0] - 3, j[1] - 3], [j[0] + 3, j[1] - 3], [j[0] + 3, j[1] + 3], [j[0] - 3, j[1] + 3]], '#b9893e', 0.6, [0, 0]); }
|
|
492
|
+
}
|
|
493
|
+
// claws
|
|
494
|
+
const e = pts[pts.length - 1], d = pts[pts.length - 2], a = Math.atan2(e[1] - d[1], e[0] - d[0]);
|
|
495
|
+
[-0.7, 0.7].forEach((da) => { c.beginPath(); c.moveTo(e[0], e[1]); c.quadraticCurveTo(e[0] + Math.cos(a + da * 0.4) * 7, e[1] + Math.sin(a + da * 0.4) * 7, e[0] + Math.cos(a + da * 1.6) * 7, e[1] + Math.sin(a + da * 1.6) * 7); c.lineWidth = 1.1; c.stroke(); });
|
|
496
|
+
}));
|
|
497
|
+
// antennae: chain of beads
|
|
498
|
+
[-1, 1].forEach((sg) => {
|
|
499
|
+
const pts = [];
|
|
500
|
+
for (let k = 0; k < 11; k++) {
|
|
501
|
+
const tt = k / 10, aw = Math.sin(t * 1.7 + sg) * 0.08;
|
|
502
|
+
pts.push(L(sg * (12 + tt * 70 + Math.sin(tt * 3) * 10), 30 - tt * 72 + tt * tt * 30 + Math.sin(tt * 4 + aw * 10) * 4));
|
|
503
|
+
}
|
|
504
|
+
pts.forEach((p, k) => {
|
|
505
|
+
const rr = k > 7 ? 5.2 : 3.2;
|
|
506
|
+
const bo = []; for (let j = 0; j < 18; j++) bo.push([p[0] + Math.cos(j / 18 * TAU) * rr, p[1] + Math.sin(j / 18 * TAU) * rr * 0.8]);
|
|
507
|
+
erase(c, bo); wash(tc, bo, '#6d5530', 0.6, [1, -0.5]);
|
|
508
|
+
engrave(c, [[0.4, 0.5], [0.7, 0.8]].map(([s0]) => { const ln = []; for (let j = 0; j <= 8; j++) { const a2 = -0.3 + j / 8 * 2; ln.push([p[0] + Math.cos(a2) * rr * s0, p[1] + Math.sin(a2) * rr * 0.8 * s0, 0.8]); } return ln; }), 0.8, 0.3);
|
|
509
|
+
contour(c, bo, 0.5, 1.1);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
// head
|
|
513
|
+
const head = []; for (let k = 0; k <= 40; k++) { const a = Math.PI + k / 40 * Math.PI; head.push(L(Math.cos(a) * 40, 52 + Math.sin(a) * 34)); }
|
|
514
|
+
head.push(L(40, 62)); head.push(L(-40, 62));
|
|
515
|
+
erase(c, head); wash(tc, head, '#2f6f6a', 0.55, [2, -1]);
|
|
516
|
+
const HS = (u, v) => { const a = Math.PI + u * Math.PI; return L(Math.cos(a) * 40 * (0.2 + 0.8 * (v + 1) / 2), 56 + Math.sin(a) * 34 * (0.2 + 0.8 * (v + 1) / 2)); };
|
|
517
|
+
engrave(c, surfLines(HS, (u, v) => H.clamp(0.25 + 0.55 * u * (v + 1) / 2 + 0.15 * (1 - (v + 1) / 2)), 9, 30, 'v', 0.02, 0.98), 1.0, 0.3);
|
|
518
|
+
// mandibles (sickle-shaped, tapered)
|
|
519
|
+
[-1, 1].forEach((sg) => {
|
|
520
|
+
const m0 = L(sg * 15, 26), m1 = L(sg * 34, 2), m2 = L(sg * 5, -12);
|
|
521
|
+
const run = []; for (let k = 0; k <= 16; k++) { const tt = k / 16, x = (1 - tt) * (1 - tt) * m0[0] + 2 * (1 - tt) * tt * m1[0] + tt * tt * m2[0], y = (1 - tt) * (1 - tt) * m0[1] + 2 * (1 - tt) * tt * m1[1] + tt * tt * m2[1]; run.push([x, y, 11 * (1 - tt) + 0.6]); }
|
|
522
|
+
c.beginPath(); ribbonPath(c, run); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore();
|
|
523
|
+
tc.save(); tc.translate(1.5, -1); tc.beginPath(); ribbonPath(tc, run); tc.fillStyle = 'rgba(80,60,34,0.6)'; tc.fill(); tc.restore();
|
|
524
|
+
engrave(c, [0.2, -0.1, -0.4].map((o) => run.map((q, k) => { const a = run[Math.max(0, k - 1)], b = run[Math.min(16, k + 1)], tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1; return [q[0] - ty / m * q[2] * o * sg, q[1] + tx / m * q[2] * o * sg, 0.55 + o * 0.6 * -1]; })), 0.9, 0.3);
|
|
525
|
+
const edge1 = run.map((q, k) => { const a = run[Math.max(0, k - 1)], b = run[Math.min(16, k + 1)], tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1; return [q[0] - ty / m * q[2] / 2, q[1] + tx / m * q[2] / 2, 1.3]; });
|
|
526
|
+
const edge2 = run.map((q, k) => { const a = run[Math.max(0, k - 1)], b = run[Math.min(16, k + 1)], tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1; return [q[0] + ty / m * q[2] / 2, q[1] - tx / m * q[2] / 2, 1.3]; });
|
|
527
|
+
c.beginPath(); ribbonPath(c, edge1); ribbonPath(c, edge2); c.fill();
|
|
528
|
+
});
|
|
529
|
+
// lens eyes
|
|
530
|
+
[-1, 1].forEach((sg) => {
|
|
531
|
+
const ec = L(sg * 24, 40);
|
|
532
|
+
const eo = []; for (let k = 0; k < 30; k++) eo.push([ec[0] + Math.cos(k / 30 * TAU) * 9, ec[1] + Math.sin(k / 30 * TAU) * 9]);
|
|
533
|
+
erase(c, eo); wash(tc, eo, '#b0442c', 0.6, [1, -0.5]);
|
|
534
|
+
[9, 6.4, 3.6].forEach((rr, k) => { const ln = []; for (let j = 0; j <= 30; j++) { const a = j / 30 * TAU; ln.push([ec[0] + Math.cos(a) * rr, ec[1] + Math.sin(a) * rr, H.clamp(0.45 + 0.4 * Math.sin(a + 0.8) + k * 0.1)]); } engrave(c, [ln], k ? 0.9 : 1.6, 0.05); });
|
|
535
|
+
c.beginPath(); c.arc(ec[0] - 2.5, ec[1] - 2.5, 1.6, 0, TAU); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore();
|
|
536
|
+
});
|
|
537
|
+
contour(c, head, 0.7, 1.8);
|
|
538
|
+
// pronotum
|
|
539
|
+
const PN = (u, v) => { const y = 56 + u * 90; const hw = H.lerp(50, 82, Math.pow(u, 0.7)) * (1 - 0.1 * Math.pow(2 * u - 1, 6)); return L(v * hw, y + 10 * Math.pow(Math.abs(v), 3) * (u < 0.5 ? 1 : -0.4)); };
|
|
540
|
+
const pno = outline(PN, 40);
|
|
541
|
+
erase(c, pno); wash(tc, pno, '#2b7a72', 0.55, [2, -1.5]);
|
|
542
|
+
const pnsh = (u, v) => H.clamp(0.18 + 0.6 * Math.pow(Math.abs(v + 0.25), 2) + 0.2 * u + 0.12 * (v > 0 ? 1 : 0));
|
|
543
|
+
engrave(c, surfLines(PN, pnsh, 30, 30, 'u'), 1.1, 0.3);
|
|
544
|
+
engrave(c, surfLines(PN, (u, v) => pnsh(u, v) - 0.3, 14, 30, 'v', 0.02, 0.98), 0.8, 0.3);
|
|
545
|
+
// vents on pronotum
|
|
546
|
+
[-1, 1].forEach((sg) => { for (let k = 0; k < 4; k++) { const a = L(sg * 22, 92 + k * 9), b = L(sg * 44, 94 + k * 9); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineWidth = 2.4; c.stroke(); } });
|
|
547
|
+
{ const a = L(0, 68), b = L(0, 144); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineWidth = 0.9; c.stroke(); }
|
|
548
|
+
contour(c, pno, 0.7, 1.9);
|
|
549
|
+
// elytra
|
|
550
|
+
[-1, 1].forEach((sg) => {
|
|
551
|
+
const EL = (u, v) => {
|
|
552
|
+
const y = 152 + u * 218; const vv = (v + 1) / 2;
|
|
553
|
+
const hw = 84 * Math.pow(Math.sin(Math.PI * Math.min(1, 0.32 + u * 0.68)), 0.55) * (u < 0.08 ? 0.93 + u : 1);
|
|
554
|
+
return L(sg * (2 + vv * hw), y - Math.pow(vv, 2) * Math.pow(u, 6) * 40 + Math.pow(vv, 3) * (1 - u) * 6);
|
|
555
|
+
};
|
|
556
|
+
const eo = outline(EL, 60);
|
|
557
|
+
erase(c, eo);
|
|
558
|
+
// iridescent wash: green->blue->copper
|
|
559
|
+
tc.save(); tc.translate(2, -1.5); poly(tc, eo);
|
|
560
|
+
const gr = tc.createLinearGradient(bx, by + 150, bx + sg * 40, by + 380);
|
|
561
|
+
gr.addColorStop(0, 'rgba(40,128,110,0.6)'); gr.addColorStop(0.5, 'rgba(48,96,140,0.55)'); gr.addColorStop(1, 'rgba(172,98,52,0.6)');
|
|
562
|
+
tc.fillStyle = gr; tc.fill(); tc.strokeStyle = 'rgba(30,70,90,0.35)'; tc.lineWidth = 2.2; tc.stroke(); tc.restore();
|
|
563
|
+
const esh = (u, v) => { const vv = (v + 1) / 2; return H.clamp(0.12 + 1.6 * Math.pow(vv - (sg < 0 ? 0.38 : 0.3), 2) * 2.2 + 0.25 * Math.pow(u, 2) + (sg > 0 ? 0.1 : 0) + 0.05 * nz.n2(u * 12, vv * 4)); };
|
|
564
|
+
engrave(c, surfLines(EL, esh, 26, 60, 'u'), 1.25, 0.26);
|
|
565
|
+
engrave(c, surfLines(EL, (u, v) => esh(u, v) - 0.36, 40, 26, 'v', 0.01, 0.99), 0.85, 0.3);
|
|
566
|
+
// striae: rows of punctures
|
|
567
|
+
c.beginPath();
|
|
568
|
+
for (let k = 1; k <= 5; k++) { const v = -1 + k * 0.33; for (let j = 2; j < 36; j++) { const p = EL(j / 40, v); c.moveTo(p[0] + 1.1, p[1]); c.arc(p[0], p[1], 1.1, 0, TAU); } }
|
|
569
|
+
c.fill();
|
|
570
|
+
// rivets along the seam
|
|
571
|
+
for (let j = 1; j < 9; j++) { const p = EL(j / 10, -0.86); c.beginPath(); c.arc(p[0], p[1], 2.6, 0, TAU); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore(); c.lineWidth = 0.9; c.stroke(); c.beginPath(); c.arc(p[0] + 0.8, p[1] + 0.8, 1.6, 0.2, 2.2); c.stroke(); }
|
|
572
|
+
contour(c, eo, 0.8, 2.1);
|
|
573
|
+
});
|
|
574
|
+
// scutellum
|
|
575
|
+
const sc = [L(-12, 150), L(12, 150), L(0, 176)]; erase(c, sc); wash(tc, sc, '#2b7a72', 0.6); contour(c, sc, 0.7, 1.4);
|
|
576
|
+
engrave(c, [[L(-6, 154), L(0, 168)].map((p) => [p[0], p[1], 0.8])], 0.9, 0.3);
|
|
577
|
+
}
|
|
578
|
+
c.restore(); tc.restore();
|
|
579
|
+
// ================= FIG. 5 — anthera aucta =================
|
|
580
|
+
{
|
|
581
|
+
const cx = 652, cy = 300, R = 31;
|
|
582
|
+
const f0 = [cx - 58, cy + 118], f1 = [cx - 40, cy + 60], f2 = [cx - 6, cy + R - 2];
|
|
583
|
+
const fl = []; for (let k = 0; k <= 24; k++) { const tt = k / 24, x = (1 - tt) * (1 - tt) * f0[0] + 2 * (1 - tt) * tt * f1[0] + tt * tt * f2[0], y = (1 - tt) * (1 - tt) * f0[1] + 2 * (1 - tt) * tt * f1[1] + tt * tt * f2[1]; fl.push([x, y, 9 - tt * 3]); }
|
|
584
|
+
c.beginPath(); ribbonPath(c, fl); c.save(); c.globalCompositeOperation = 'destination-out'; c.fill(); c.restore();
|
|
585
|
+
tc.save(); tc.translate(1.5, -1); tc.beginPath(); ribbonPath(tc, fl); tc.fillStyle = 'rgba(170,165,70,0.55)'; tc.fill(); tc.restore();
|
|
586
|
+
const fe = (o) => fl.map((q, k) => { const a = fl[Math.max(0, k - 1)], b = fl[Math.min(24, k + 1)], tx = b[0] - a[0], ty = b[1] - a[1], m = Math.hypot(tx, ty) || 1; return [q[0] - ty / m * q[2] * o, q[1] + tx / m * q[2] * o, 0.3 + 0.9 * (o + 0.5)]; });
|
|
587
|
+
engrave(c, [-0.25, -0.05, 0.15, 0.32].map(fe), 0.9, 0.3);
|
|
588
|
+
c.beginPath(); ribbonPath(c, fe(-0.5).map((q) => [q[0], q[1], 0.9])); ribbonPath(c, fe(0.5).map((q) => [q[0], q[1], 1.5])); c.fill();
|
|
589
|
+
{ const a = fe(-0.5)[0], b = fe(0.5)[0]; c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineWidth = 1; c.stroke(); }
|
|
590
|
+
const so = []; for (let k = 0; k < 64; k++) so.push([cx + Math.cos(k / 64 * TAU) * R, cy + Math.sin(k / 64 * TAU) * R]);
|
|
591
|
+
erase(c, so); wash(tc, so, '#8f9aa2', 0.55, [2, -1.5]);
|
|
592
|
+
tc.save(); poly(tc, so); tc.clip(); const rg = tc.createRadialGradient(cx + 18, cy + 18, 2, cx + 18, cy + 18, 22); rg.addColorStop(0, 'rgba(205,160,90,0.55)'); rg.addColorStop(1, 'rgba(205,160,90,0)'); tc.fillStyle = rg; tc.fillRect(cx - R, cy - R, 2 * R, 2 * R); tc.restore();
|
|
593
|
+
const fam = (dx, dy, n, off) => {
|
|
594
|
+
const out = [], px = -dy, py = dx;
|
|
595
|
+
for (let i = 0; i < n; i++) {
|
|
596
|
+
const o = -R + (i + 0.5) * 2 * R / n, half = Math.sqrt(Math.max(0, R * R - o * o)), ln = [];
|
|
597
|
+
for (let j = 0; j <= 24; j++) {
|
|
598
|
+
const sv = -half + 2 * half * j / 24, bow = 0.22 * half * (1 - Math.pow(sv / (half || 1), 2));
|
|
599
|
+
const x = cx + dx * (o + bow) + px * sv, y = cy + dy * (o + bow) + py * sv;
|
|
600
|
+
const lx = (x - cx) / R, ly = (y - cy) / R, z = Math.sqrt(Math.max(0, 1 - lx * lx - ly * ly));
|
|
601
|
+
const lit = -0.55 * lx - 0.6 * ly + 0.58 * z;
|
|
602
|
+
const refl = H.smooth(0.55, 1, (lx + ly) / 1.414) * 0.35;
|
|
603
|
+
ln.push([x, y, H.clamp(0.75 - 0.75 * lit - refl + off)]);
|
|
604
|
+
}
|
|
605
|
+
out.push(ln);
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
};
|
|
609
|
+
engrave(c, fam(0.6, 0.8, 26, 0.12), 1.3, 0.12, 1.1);
|
|
610
|
+
engrave(c, fam(0.8, -0.6, 24, -0.22), 0.95, 0.3);
|
|
611
|
+
contour(c, so, 0.7, 2.0);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ================= FIG. 3 — semen (seed chip) =================
|
|
615
|
+
{
|
|
616
|
+
const cx = 672, cy = 566;
|
|
617
|
+
const so = []; for (let k = 0; k < 50; k++) { const a = k / 50 * TAU; so.push([cx + Math.cos(a) * 38, cy + Math.sin(a) * 25 * (1 + 0.15 * Math.cos(a))]); }
|
|
618
|
+
// legs
|
|
619
|
+
for (let k = 0; k < 6; k++) { [-1, 1].forEach((sg) => { const x = cx - 22 + k * 9; c.beginPath(); c.moveTo(x, cy + sg * 18); c.lineTo(x, cy + sg * 32); c.lineWidth = 2.6; c.stroke(); }); }
|
|
620
|
+
erase(c, so); wash(tc, so, '#8a6a3c', 0.55, [2, -1]);
|
|
621
|
+
const SS = (u, v) => [cx + (u * 2 - 1) * 38 * Math.sqrt(1 - v * v * 0.0), cy + v * 25 * Math.sqrt(Math.max(0, 1 - Math.pow(u * 2 - 1, 2)))];
|
|
622
|
+
engrave(c, surfLines(SS, (u, v) => H.clamp(0.2 + 0.55 * (v + 1) / 2 + 0.3 * Math.pow(u, 3)), 14, 30, 'u'), 1.1, 0.3);
|
|
623
|
+
const dz = [[cx - 13, cy - 9], [cx + 13, cy - 9], [cx + 13, cy + 9], [cx - 13, cy + 9]];
|
|
624
|
+
erase(c, dz); wash(tc, dz, '#c69a48', 0.7, [1, -1]);
|
|
625
|
+
c.lineWidth = 0.8; c.beginPath(); for (let k = 1; k < 4; k++) { c.moveTo(cx - 13, cy - 9 + k * 4.5); c.lineTo(cx + 13, cy - 9 + k * 4.5); c.moveTo(cx - 13 + k * 6.5, cy - 9); c.lineTo(cx - 13 + k * 6.5, cy + 9); } c.stroke();
|
|
626
|
+
contour(c, dz, 0.7, 1.2); contour(c, so, 0.7, 1.8);
|
|
627
|
+
}
|
|
628
|
+
// ================= FIG. 4 — caulis sectio transversa =================
|
|
629
|
+
{
|
|
630
|
+
const cx = 1150, cy = 548, R = 46;
|
|
631
|
+
const ring = (rr, n = 64) => { const o = []; for (let k = 0; k < n; k++) o.push([cx + Math.cos(k / n * TAU) * rr, cy + Math.sin(k / n * TAU) * rr]); return o; };
|
|
632
|
+
wash(tc, ring(R), '#7d9a52', 0.5, [2, -1]);
|
|
633
|
+
wash(tc, ring(R - 9), '#d6d29a', 0.45, [2, -1]);
|
|
634
|
+
stipple(c, ring(R - 1), 2600, (x, y) => { const d = Math.hypot(x - cx, y - cy); return d > R - 9 ? 0.55 : 0.12 + 0.15 * ((x - cx + y - cy) / R + 1); }, H.rng(4), 0.55);
|
|
635
|
+
contour(c, ring(R), 0.9, 2); contour(c, ring(R - 9), 0.5, 0.9);
|
|
636
|
+
// vascular bundles = insulated wires
|
|
637
|
+
for (let k = 0; k < 7; k++) {
|
|
638
|
+
const a = k / 7 * TAU + 0.3, wx = cx + Math.cos(a) * (R - 20), wy = cy + Math.sin(a) * (R - 20);
|
|
639
|
+
const ob = ring(0).map(() => 0);
|
|
640
|
+
const outer = []; for (let j = 0; j < 24; j++) outer.push([wx + Math.cos(j / 24 * TAU) * 7.5, wy + Math.sin(j / 24 * TAU) * 7.5]);
|
|
641
|
+
erase(c, outer); wash(tc, outer, '#5d6b78', 0.45, [1, -0.5]);
|
|
642
|
+
c.save(); poly(c, outer); c.clip(); c.beginPath(); for (let j = -10; j <= 10; j++) { c.moveTo(wx - 10, wy + j * 2.2 - 10); c.lineTo(wx + 10, wy + j * 2.2 + 10); } c.lineWidth = 0.6; c.stroke(); c.restore();
|
|
643
|
+
const core = []; for (let j = 0; j < 18; j++) core.push([wx + Math.cos(j / 18 * TAU) * 3.6, wy + Math.sin(j / 18 * TAU) * 3.6]);
|
|
644
|
+
erase(c, core); wash(tc, core, '#c7782e', 0.85, [0.5, 0]);
|
|
645
|
+
contour(c, core, 0.6, 1.0); contour(c, outer, 0.6, 1.3);
|
|
646
|
+
}
|
|
647
|
+
const core = ring(9, 30); erase(c, core); wash(tc, core, '#c7782e', 0.8, [0.5, 0]);
|
|
648
|
+
c.save(); poly(c, core); c.clip(); c.beginPath(); for (let j = -6; j <= 6; j++) { c.moveTo(cx - 10, cy + j * 2.4 - 10); c.lineTo(cx + 10, cy + j * 2.4 + 10); } c.lineWidth = 0.7; c.stroke(); c.restore();
|
|
649
|
+
contour(c, core, 0.6, 1.2);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ---------- lettering ----------
|
|
653
|
+
c.textBaseline = 'alphabetic';
|
|
654
|
+
c.font = 'italic 400 17px "Libre Baskerville"'; c.textAlign = 'center';
|
|
655
|
+
c.fillText('1.', 196, 470); c.fillText('2.', 1150, 150); c.fillText('3.', 672, 628); c.fillText('4.', 1150, 618); c.fillText('5.', 652, 252);
|
|
656
|
+
c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'right'; c.fillText('Pl. XXIV.', W - 74, 62);
|
|
657
|
+
c.textAlign = 'left'; c.fillText('Vol. III.', 74, 62);
|
|
658
|
+
c.textAlign = 'center'; c.font = '600 15px "Cormorant Garamond"';
|
|
659
|
+
c.fillText('F L O R A E L E C T R I C A', W / 2, 62);
|
|
660
|
+
c.font = 'italic 400 31px "Cormorant Garamond"'; c.fillText('Silicoflora mirabilis, & Scarabaeus machinalis.', W / 2, 658);
|
|
661
|
+
c.font = 'italic 400 16px "Cormorant Garamond"';
|
|
662
|
+
c.fillText('1. Flos, caulis et bulbus. 2. Scarabaeus, a dorso visus. 3. Semen. 4. Caulis sectio transversa. 5. Anthera, aucta.', W / 2, 682);
|
|
663
|
+
c.font = 'italic 400 12px "Libre Baskerville"'; c.textAlign = 'left'; c.fillText('E. Marlowe del.', 74, 682);
|
|
664
|
+
c.textAlign = 'right'; c.fillText('T. Grieve sculp.', W - 74, 682);
|
|
665
|
+
// ruled border inside plate mark
|
|
666
|
+
c.lineWidth = 1.4; c.strokeRect(62, 42, W - 124, H_ - 76);
|
|
667
|
+
c.lineWidth = 0.6; c.strokeRect(67, 47, W - 134, H_ - 86);
|
|
668
|
+
// rule under caption heading
|
|
669
|
+
c.lineWidth = 0.6; c.beginPath(); c.moveTo(W / 2 - 60, 666); c.lineTo(W / 2 + 60, 666); c.stroke();
|
|
670
|
+
|
|
671
|
+
// ---------- watercolour grain/variation on tint layer ----------
|
|
672
|
+
{
|
|
673
|
+
const img = tc.getImageData(0, 0, W, H_), d = img.data;
|
|
674
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
675
|
+
if (!d[i + 3]) continue;
|
|
676
|
+
const p = i >> 2, x = p % W, y = (p / W) | 0;
|
|
677
|
+
const v = 0.7 + 0.42 * nz.fbm2(x * 0.018, y * 0.018, 3) + 0.14 * nz.n2(x * 0.35, y * 0.35);
|
|
678
|
+
d[i + 3] = H.clamp(d[i + 3] * v / 255) * 255;
|
|
679
|
+
}
|
|
680
|
+
tc.putImageData(img, 0, 0);
|
|
681
|
+
}
|
|
682
|
+
// ---------- composite ----------
|
|
683
|
+
// build 1: the print is pulled — ink appears behind the press roller sweeping left to right
|
|
684
|
+
const front = H.lerp(-80, W + 80, H.ease.inOut(H.clamp((t - 0.1) / 1.7)));
|
|
685
|
+
if (front < W + 60) {
|
|
686
|
+
ink.getContext('2d').save();
|
|
687
|
+
const ic = ink.getContext('2d'); ic.globalCompositeOperation = 'destination-in';
|
|
688
|
+
const mg = ic.createLinearGradient(front - 70, 0, front, 0); mg.addColorStop(0, 'rgba(0,0,0,1)'); mg.addColorStop(1, 'rgba(0,0,0,0)');
|
|
689
|
+
ic.fillStyle = mg; ic.fillRect(0, 0, W, H_); ic.restore();
|
|
690
|
+
}
|
|
691
|
+
// build 2: the colourist's washes bloom out figure by figure (noisy wet edge)
|
|
692
|
+
const blooms = [[350, 250, 1.7, 240], [300, 430, 2.0, 230], [505, 295, 2.15, 70], [958, 300, 2.35, 290], [652, 300, 2.7, 80], [672, 566, 2.9, 64], [1150, 548, 3.05, 64], [292, 595, 2.2, 50]];
|
|
693
|
+
if (t < 4.2) {
|
|
694
|
+
const mw = 320, mh = 180, m = H.layer(mw, mh), mc = m.getContext('2d'), mi = mc.createImageData(mw, mh), md = mi.data;
|
|
695
|
+
const rads = blooms.map(([, , t0, R]) => R * 1.25 * H.ease.out(H.clamp((t - t0) / 1.0)));
|
|
696
|
+
for (let y = 0; y < mh; y++) for (let x = 0; x < mw; x++) {
|
|
697
|
+
const X = x * 4, Y = y * 4, nn = nz.fbm2(X * 0.012, Y * 0.012, 3) * 34;
|
|
698
|
+
let a = 0;
|
|
699
|
+
for (let k = 0; k < blooms.length; k++) { if (!rads[k]) continue; const dd = Math.hypot(X - blooms[k][0], Y - blooms[k][1]); a = Math.max(a, H.clamp((rads[k] + nn - dd) / 14)); }
|
|
700
|
+
md[(y * mw + x) * 4 + 3] = a * 255;
|
|
701
|
+
}
|
|
702
|
+
mc.putImageData(mi, 0, 0);
|
|
703
|
+
tc.save(); tc.globalCompositeOperation = 'destination-in'; tc.imageSmoothingEnabled = true; tc.drawImage(m, 0, 0, W, H_); tc.restore();
|
|
704
|
+
}
|
|
705
|
+
ctx.save();
|
|
706
|
+
ctx.globalCompositeOperation = 'multiply';
|
|
707
|
+
ctx.filter = 'blur(0.8px)'; ctx.drawImage(tint, 0, 0); ctx.filter = 'none';
|
|
708
|
+
ctx.globalAlpha = 0.25; ctx.filter = 'blur(0.9px)'; ctx.drawImage(ink, 0, 0); ctx.filter = 'none';
|
|
709
|
+
ctx.globalAlpha = 0.92; ctx.drawImage(ink, 0, 0);
|
|
710
|
+
ctx.restore();
|
|
711
|
+
// ink tone: warm brown-black
|
|
712
|
+
ctx.save(); ctx.globalCompositeOperation = 'lighten'; ctx.fillStyle = 'rgb(38,30,24)'; ctx.fillRect(0, 0, W, H_); ctx.restore();
|
|
713
|
+
|
|
714
|
+
// ---------- foxing ----------
|
|
715
|
+
ctx.save(); ctx.globalCompositeOperation = 'multiply';
|
|
716
|
+
for (let i = 0; i < 110; i++) {
|
|
717
|
+
let x = r() * W, y = r() * H_;
|
|
718
|
+
if (r.chance(0.55)) { x = r.chance(0.5) ? r() * 200 : W - r() * 200; }
|
|
719
|
+
const rad = r.range(1, r.chance(0.12) ? 12 : 4);
|
|
720
|
+
const fg = ctx.createRadialGradient(x, y, 0, x, y, rad);
|
|
721
|
+
fg.addColorStop(0, `rgba(160,100,45,${r.range(0.2, 0.5)})`); fg.addColorStop(0.5, 'rgba(170,110,55,0.15)'); fg.addColorStop(1, 'rgba(170,110,55,0)');
|
|
722
|
+
ctx.fillStyle = fg; ctx.beginPath(); ctx.arc(x, y, rad, 0, TAU); ctx.fill();
|
|
723
|
+
}
|
|
724
|
+
ctx.restore();
|
|
725
|
+
// plate mark (pressed edge)
|
|
726
|
+
ctx.save();
|
|
727
|
+
ctx.strokeStyle = 'rgba(110,80,40,0.35)'; ctx.lineWidth = 1.5; ctx.strokeRect(PM[0], PM[1], PM[2] - PM[0], PM[3] - PM[1]);
|
|
728
|
+
ctx.strokeStyle = 'rgba(255,250,235,0.55)'; ctx.lineWidth = 2; ctx.strokeRect(PM[0] - 2, PM[1] - 2, PM[2] - PM[0] + 4, PM[3] - PM[1] + 4);
|
|
729
|
+
ctx.strokeStyle = 'rgba(110,80,40,0.12)'; ctx.lineWidth = 4; ctx.strokeRect(PM[0] + 3, PM[1] + 3, PM[2] - PM[0] - 6, PM[3] - PM[1] - 6);
|
|
730
|
+
ctx.restore();
|
|
731
|
+
// press roller passing over the sheet during the pull
|
|
732
|
+
if (front > -60 && front < W + 60) {
|
|
733
|
+
const rg2 = ctx.createLinearGradient(front - 90, 0, front + 30, 0);
|
|
734
|
+
rg2.addColorStop(0, 'rgba(60,40,20,0)'); rg2.addColorStop(0.6, 'rgba(60,40,20,0.16)'); rg2.addColorStop(0.8, 'rgba(255,250,235,0.18)'); rg2.addColorStop(1, 'rgba(60,40,20,0)');
|
|
735
|
+
ctx.fillStyle = rg2; ctx.fillRect(front - 90, 0, 120, H_);
|
|
736
|
+
}
|
|
737
|
+
H.grain(ctx, W, H_, 0.045, 5 + Math.floor(t * 12));
|
|
738
|
+
},
|
|
739
|
+
// ===== EXPLAINER =====
|
|
740
|
+
explainerDuration: 14.5,
|
|
741
|
+
explainerNote: 'Three plates from a naturalist\'s folio, each pulled fresh under the press roller: engraved specimens of cats and dogs feed the circuit-lily "model", a life-cycle plate with fletched engraved arrows shows Guess, Check and Adjust, and a growth-stage plate counts accuracy in an engraved cartouche, with the colourist\'s red wash ringing the key moments.',
|
|
742
|
+
explainer(ctx, W, H_, t) {
|
|
743
|
+
BX_WASH = true;
|
|
744
|
+
const cl = H.clamp, ease = H.ease.inOut;
|
|
745
|
+
// ---------- PLATE XXV: the examples ----------
|
|
746
|
+
const plateA = (c, tc, T) => {
|
|
747
|
+
// title: engraved large, then set as the running head
|
|
748
|
+
const k = ease(cl((T - 1.6) / 0.6));
|
|
749
|
+
const y = H.lerp(380, 72, k), fs = H.lerp(96, 28, k);
|
|
750
|
+
c.font = `italic 400 ${fs}px "Cormorant Garamond"`; c.textAlign = 'center'; c.textBaseline = 'alphabetic';
|
|
751
|
+
const txt = 'How does AI learn?', tw = c.measureText(txt).width;
|
|
752
|
+
c.save(); c.beginPath(); c.rect(W / 2 - tw / 2 - 10, y - fs, (tw + 20) * cl((T - 0.15) / 1.0), fs * 1.4); c.clip(); c.fillText(txt, W / 2, y); c.restore();
|
|
753
|
+
if (k < 1 && T > 1.0) { c.lineWidth = 0.8; c.beginPath(); c.moveTo(W / 2 - tw * 0.3, y + 22); c.lineTo(W / 2 + tw * 0.3, y + 22); c.stroke(); }
|
|
754
|
+
if (T < 2.2) return;
|
|
755
|
+
// specimens grid
|
|
756
|
+
const G = []; for (let i = 0; i < 9; i++) G.push([180 + (i % 3) * 150, 170 + Math.floor(i / 3) * 172, [1, 0, 1, 1, 0, 1, 0, 1, 0][i]]);
|
|
757
|
+
G.forEach(([x, y2, cat], i) => { if (T > 2.3 + i * 0.12) bxSpecimen(c, tc, x, y2, !!cat, 1.0, `${i + 1}.`); });
|
|
758
|
+
// the model
|
|
759
|
+
const [hx, hy] = bxLily(c, tc, 1010, 610, 1.6, Math.sin(T * 1.1) * 0.04);
|
|
760
|
+
c.font = 'italic 600 40px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText('the model', 1110, 664);
|
|
761
|
+
c.lineWidth = 0.9; c.beginPath(); c.moveTo(1090, 640); c.quadraticCurveTo(1050, 610, 1030, 560); c.stroke();
|
|
762
|
+
// arrow from the specimens into the model
|
|
763
|
+
const path = bxQ([520, 330], [690, 190], [880, 330], 40);
|
|
764
|
+
bxArrow(c, path, cl((T - 2.9) / 0.5), 3.4, 'training data', 690, 216);
|
|
765
|
+
// specimens travelling along the arrow
|
|
766
|
+
for (let k2 = 0; k2 < 8; k2++) { const u = ((T - 3.2) * 0.9 - k2 * 0.18); if (u < 0 || u > 1 || T > 5.9) continue; const q = path[Math.min(40, Math.floor(u * 40))]; bxSpecimen(c, tc, q[0], q[1], k2 % 3 !== 1, 0.42 * (1 - 0.5 * u)); }
|
|
767
|
+
// counter cartouche
|
|
768
|
+
if (T > 3.0) { const e = ease(cl((T - 3.0) / 2.6)), v = T >= 5.6 ? 1000000 : Math.round(Math.pow(10, 1 + 5 * e)); bxCartouche(c, tc, 740, 600, 330, 160, ['examples', v.toLocaleString('en-US')], [36, 72]); }
|
|
769
|
+
|
|
770
|
+
};
|
|
771
|
+
// ---------- PLATE XXVI: the cycle ----------
|
|
772
|
+
const plateB = (c, tc, T) => {
|
|
773
|
+
const right = T > 9.35;
|
|
774
|
+
c.font = 'italic 400 28px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText('How does AI learn? — the cycle of learning.', W / 2, 72);
|
|
775
|
+
[c, tc].forEach((k) => { k.save(); k.translate(0, 0); });
|
|
776
|
+
// Fig 1 GUESS
|
|
777
|
+
bxSpecimen(c, tc, 160, 300, true, 1.2, 'a.');
|
|
778
|
+
c.font = 'italic 400 28px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText('a new picture', 160, 450);
|
|
779
|
+
bxArrow(c, bxQ([240, 290], [290, 250], [330, 300], 16), cl((T - 6.75) / 0.3), 2.8);
|
|
780
|
+
const [hx, hy] = bxLily(c, tc, 420, 580, 1.1, right ? 0.0 : -0.12);
|
|
781
|
+
c.font = '600 44px "Cormorant Garamond"'; c.fillText('GUESS', 420, 640);
|
|
782
|
+
if (T > 7.0) {
|
|
783
|
+
// banner (ribbon scroll) with the guess
|
|
784
|
+
const bxp = 580, byp = 150, pr = cl((T - (right ? 9.35 : 7.0)) / 0.35);
|
|
785
|
+
const rib = [[bxp - 90, byp - 26], [bxp + 90, byp - 26], [bxp + 76, byp], [bxp + 90, byp + 26], [bxp - 90, byp + 26], [bxp - 76, byp]];
|
|
786
|
+
erase(c, rib); wash(tc, rib, right ? '#9fbf7a' : '#efe0b0', 0.6); contour(c, rib, 0.7, 1.8);
|
|
787
|
+
c.save(); c.beginPath(); c.rect(bxp - 80, byp - 30, 160 * pr, 60); c.clip(); c.font = 'italic 600 40px "Cormorant Garamond"'; c.textBaseline = 'middle'; c.fillText(right ? 'cat ✓' : 'dog?', bxp, byp + 2); c.restore(); c.textBaseline = 'alphabetic';
|
|
788
|
+
c.lineWidth = 0.8; c.beginPath(); c.moveTo(bxp - 70, byp + 26); c.quadraticCurveTo(500, 220, hx + 20, hy - 40); c.stroke();
|
|
789
|
+
}
|
|
790
|
+
// Fig 2 CHECK: lens over the specimen
|
|
791
|
+
bxSpecimen(c, tc, 980, 250, true, 1.0);
|
|
792
|
+
if (T > 7.4) {
|
|
793
|
+
const lens = bxEll(980, 250, 104, 104, 60); c.lineWidth = 7; c.beginPath(); poly(c, lens); c.stroke(); c.lineWidth = 1; poly(c, bxEll(980, 250, 96, 96, 60)); c.stroke();
|
|
794
|
+
const hd = [[1054, 324], [1150, 420], [1134, 436], [1038, 340]]; erase(c, hd); wash(tc, hd, '#7a5a34', 0.7); bxHatch(c, hd, Math.PI / 4, 2.2, () => 0.75); contour(c, hd, 0.7, 1.8);
|
|
795
|
+
c.lineWidth = 1.2; c.beginPath(); c.arc(946, 214, 52, Math.PI * 1.05, Math.PI * 1.45); c.stroke();
|
|
796
|
+
c.font = '600 44px "Cormorant Garamond"'; c.fillText('CHECK', 960, 440);
|
|
797
|
+
c.font = 'italic 600 34px "Cormorant Garamond"'; c.fillText("answer: cat.", 960, 478);
|
|
798
|
+
}
|
|
799
|
+
// Fig 3 ADJUST: dials
|
|
800
|
+
const tq = Math.floor(T * 12) / 12;
|
|
801
|
+
[[620, 540, 46], [724, 566, 40], [826, 532, 44]].forEach(([x, y, R], i) => { const a0 = [-2.4, -0.8, -1.7][i], a1 = [-0.9, -2.3, -0.3][i]; bxDial(c, tc, x, y, R, a0 + (a1 - a0) * ease(cl((tq - 8.65 - i * 0.12) / 0.6))); });
|
|
802
|
+
c.font = '600 44px "Cormorant Garamond"'; c.fillText('ADJUST', 724, 662);
|
|
803
|
+
// cycle arrows
|
|
804
|
+
bxArrow(c, bxQ([680, 130], [800, 70], [880, 150]), cl((T - 7.3) / 0.35), 2.8);
|
|
805
|
+
bxArrow(c, bxQ([1030, 500], [1020, 590], [890, 570]), cl((T - 8.2) / 0.35), 2.8, 'error', 1070, 600);
|
|
806
|
+
bxArrow(c, bxQ([570, 560], [500, 600], [480, 470]), cl((T - 9.05) / 0.3), 2.8);
|
|
807
|
+
[c, tc].forEach((k) => k.restore());
|
|
808
|
+
// colourist's emphasis: red wash cross over the wrong guess, ring around it
|
|
809
|
+
if (T > 7.8 && !right) { const p = cl((T - 7.8) / 0.25); tc.save(); tc.strokeStyle = 'rgba(184,46,36,1)'; tc.lineCap = 'round'; tc.lineWidth = 13; tc.beginPath(); tc.moveTo(520, 125); tc.lineTo(520 + 120 * p, 125 + 50 * p); tc.moveTo(640, 125); tc.lineTo(640 - 120 * cl(p * 2 - 1), 125 + 50 * cl(p * 2 - 1)); tc.stroke(); tc.restore(); bxRing(tc, 580, 150, 125, 55, cl((T - 7.95) / 0.35)); }
|
|
810
|
+
if (right) bxRing(tc, 580, 150, 125, 55, cl((T - 9.6) / 0.35), '#4f8a3c');
|
|
811
|
+
};
|
|
812
|
+
// ---------- PLATE XXVII: growth by training rounds ----------
|
|
813
|
+
const plateC = (c, tc, T) => {
|
|
814
|
+
c.font = 'italic 400 28px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText('How does AI learn? — growth by rounds of training.', W / 2, 72);
|
|
815
|
+
const accs = [61, 74, 88, 97], xs = [180, 360, 540, 730], sc = [0.55, 0.75, 0.95, 1.15];
|
|
816
|
+
c.lineWidth = 1.2; c.beginPath(); c.moveTo(110, 560); c.lineTo(820, 560); c.stroke();
|
|
817
|
+
bxHatch(c, [[110, 560], [820, 560], [820, 574], [110, 574]], Math.PI / 4, 3, () => 0.6);
|
|
818
|
+
xs.forEach((x, i) => {
|
|
819
|
+
const t0 = 10.9 + i * 0.42; if (T < t0) return;
|
|
820
|
+
const g = H.ease.out(cl((T - t0) / 0.45));
|
|
821
|
+
bxLily(c, tc, x, 560, sc[i], Math.sin(T + i) * 0.03, g);
|
|
822
|
+
c.font = 'italic 400 28px "Cormorant Garamond"'; c.textAlign = 'center'; c.fillText(`round ${i + 1}`, x, 600);
|
|
823
|
+
c.font = '600 40px "Cormorant Garamond"'; c.fillText(accs[i] + '%', x, 642);
|
|
824
|
+
});
|
|
825
|
+
if (T > 11.0) bxArrow(c, bxQ([120, 380], [420, 300], [640, 110], 40), cl((T - 11.0) / 1.5), 2.8, 'better & better', 300, 270);
|
|
826
|
+
const pr = cl((T - 10.95) / 1.55), v = Math.round(61 + 36 * (1 - Math.pow(1 - pr, 2.3)));
|
|
827
|
+
if (T > 10.95) bxCartouche(c, tc, 1040, 250, 330, 230, ['Accuracy', v + '%'], [44, 110]);
|
|
828
|
+
if (T > 12.5) { bxRing(tc, 1040, 250, 196, 140, cl((T - 12.5) / 0.35)); bxRing(tc, 730, 400, 120, 260, cl((T - 12.7) / 0.35)); }
|
|
829
|
+
if (T > 13.0) { c.font = 'italic 600 52px "Cormorant Garamond"'; c.textAlign = 'center'; c.save(); c.beginPath(); c.rect(820, 420, 440 * cl((T - 13.0) / 0.45), 200); c.clip(); c.fillText('More examples →', 1040, 480); c.fillText('better guesses.', 1040, 540); c.restore(); }
|
|
830
|
+
};
|
|
831
|
+
// ---------- press-roller transitions between plates ----------
|
|
832
|
+
const pull = (A, B, p) => {
|
|
833
|
+
const front = H.lerp(-80, W + 80, ease(p));
|
|
834
|
+
ctx.drawImage(A, 0, 0);
|
|
835
|
+
ctx.save(); ctx.beginPath(); ctx.rect(0, 0, Math.max(0, front), H_); ctx.clip(); ctx.drawImage(B, 0, 0); ctx.restore();
|
|
836
|
+
const rg = ctx.createLinearGradient(front - 90, 0, front + 30, 0); rg.addColorStop(0, 'rgba(60,40,20,0)'); rg.addColorStop(0.6, 'rgba(60,40,20,0.2)'); rg.addColorStop(0.8, 'rgba(255,250,235,0.25)'); rg.addColorStop(1, 'rgba(60,40,20,0)');
|
|
837
|
+
ctx.fillStyle = rg; ctx.fillRect(front - 90, 0, 120, H_);
|
|
838
|
+
};
|
|
839
|
+
const A = (T) => bxCompose(W, H_, plateA, T, 'Pl. XXV.'), B = (T) => bxCompose(W, H_, plateB, T, 'Pl. XXVI.'), C = (T) => bxCompose(W, H_, plateC, T, 'Pl. XXVII.');
|
|
840
|
+
if (t < 0.4) pull(bxPaper(W, H_), A(t), t / 0.4);
|
|
841
|
+
else if (t < 6.0) ctx.drawImage(A(t), 0, 0);
|
|
842
|
+
else if (t < 6.55) pull(A(5.99), B(t), (t - 6.0) / 0.55);
|
|
843
|
+
else if (t < 10.3) ctx.drawImage(B(t), 0, 0);
|
|
844
|
+
else if (t < 10.85) pull(B(10.29), C(t), (t - 10.3) / 0.55);
|
|
845
|
+
else ctx.drawImage(C(t), 0, 0);
|
|
846
|
+
H.grain(ctx, W, H_, 0.045, 5 + Math.floor(t * 12));
|
|
847
|
+
},
|
|
848
|
+
// Sound: a printer's study. Each plate is pulled under the press roller (a heavy rolling drum + the damp sheet
|
|
849
|
+
// lifting); lines that engrave themselves on are a burin cutting copper (dry, gritty, low-mid, never squeaky);
|
|
850
|
+
// the colourist's red and green rings are a soft wet brush; brass dials click; the cartouche counter ticks like a
|
|
851
|
+
// numbering wheel. A very quiet pad sits under it (calm, painterly). Times copied from explainer() above.
|
|
852
|
+
explainerSound(k) {
|
|
853
|
+
// Chrome glitch: a noise buffer that starts within ~1e-8 s of a sample boundary (float error like 4.15 + 0.1 * 1 + 0.11)
|
|
854
|
+
// spikes far above its gain. Put every start a quarter sample past the 48 kHz grid so none land there.
|
|
855
|
+
{ const k0 = k, sn = (t) => (Math.round(t * 48000) + 0.25) / 48000; k = { ...k0, play: (n, t, o) => k0.play(n, sn(t), o), tone: (t, o) => k0.tone(sn(t), o), noise: (t, o) => k0.noise(sn(t), o) }; }
|
|
856
|
+
const D = 14.5, cl = H.clamp, ease = H.ease.inOut;
|
|
857
|
+
const pan = (x) => cl(x / 640 - 1, -1, 1) * 0.6;
|
|
858
|
+
let seed = 7; const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
|
|
859
|
+
// burin: short gritty cuts through copper, a grain every ~30 ms, with a little low body
|
|
860
|
+
const burin = (a, d, g = 1, p = 0, minor = true) => {
|
|
861
|
+
for (let x = 0; x < d; x += 0.026 + rnd() * 0.02) k.noise(a + x, { dur: 0.04, filter: 'bandpass', f: 1700 + rnd() * 900, q: 1.8, gain: 0.13 * g * (0.7 + rnd() * 0.5), a: 0.006, pan: p, minor });
|
|
862
|
+
k.noise(a, { dur: d + 0.03, color: 'pink', filter: 'bandpass', f: 700, q: 1, gain: 0.05 * g, a: 0.01, hold: d * 0.7, pan: p, minor });
|
|
863
|
+
};
|
|
864
|
+
// colourist's brush laying a wash (rings, the red cross)
|
|
865
|
+
const wash = (a, d, g = 1, p = 0, minor = false) => { k.noise(a, { dur: 0.04, color: 'pink', filter: 'bandpass', f: 1000, q: 0.8, gain: 0.16 * g, a: 0.004, pan: p, minor }); k.play('stroke', a, { material: 'brush', dur: d, gain: 2.2 * g, pan: p, minor }); }; // wet bristles touch down, then drag
|
|
866
|
+
// press pull: the roller crosses the bed left -> right on an inOut ease, the damp sheet peels off at the start
|
|
867
|
+
const pull = (a, d, g = 1) => {
|
|
868
|
+
k.play('paper', a, { dur: 0.22, gain: 0.35 * g });
|
|
869
|
+
k.play('roller', a + 0.04, { dur: d - 0.04, gain: 0.55 * g });
|
|
870
|
+
[0.25, 0.5, 0.75].forEach((u) => { const tu = a + d * u; k.noise(tu - 0.08, { dur: 0.22, color: 'brown', filter: 'lowpass', f: 260, gain: 0.32 * g, a: 0.06, pan: (ease(u) * 2 - 1) * 0.6 }); });
|
|
871
|
+
k.play('thud', a + d - 0.02, { f: 85, gain: 0.25 * g, pan: 0.5 }); // roller runs off the bed onto its stop
|
|
872
|
+
};
|
|
873
|
+
// numbering wheel tick for the cartouche counters
|
|
874
|
+
const wheel = (t, g, p, f = 1500) => k.withMinor(() => { k.noise(t, { dur: 0.025, filter: 'bandpass', f, q: 2, gain: 0.26 * g, a: 0.001, pan: p }); k.tone(t, { f: f * 0.18, type: 'triangle', dur: 0.035, gain: 0.06 * g, pan: p }); });
|
|
875
|
+
|
|
876
|
+
k.bed('room', 0, D, { gain: 0.35 });
|
|
877
|
+
k.music('pad', 0.4, D, { bpm: 64, gain: 0.35, chords: [['F3', 'A3', 'C4', 'E4'], ['D3', 'F3', 'A3', 'C4'], ['Bb2', 'D3', 'F3', 'A3'], ['C3', 'E3', 'G3', 'Bb3']] });
|
|
878
|
+
|
|
879
|
+
// ---- Plate XXV: pulled (0 -> 0.4), title engraved left to right (0.15 -> 1.15), rule appears at 1.0
|
|
880
|
+
pull(0, 0.4, 0.8);
|
|
881
|
+
[[0, 3], [4, 8], [9, 11], [12, 18]].forEach(([s, e], i) => burin(0.15 + s / 18 * 1.0, (e - s) / 18 * 1.0, 1, [-0.3, -0.1, 0.08, 0.28][i], i > 0));
|
|
882
|
+
burin(1.0, 0.1, 1.3, 0, false);
|
|
883
|
+
k.noise(1.6, { dur: 0.6, color: 'brown', filter: 'bandpass', f: [[0, 260], [0.3, 520], [0.6, 300]], q: 0.6, gain: 0.16, a: 0.25, minor: true }); // title slides up into the head
|
|
884
|
+
// the model lily and the specimen grid print in (2.2, then 2.3 + i*0.12)
|
|
885
|
+
k.play('press', 2.2, { gain: 0.2, pan: pan(1010) });
|
|
886
|
+
for (let i = 0; i < 9; i++) burin(2.3 + i * 0.12, 0.06, 0.85, pan(180 + (i % 3) * 150));
|
|
887
|
+
burin(2.9, 0.5, 0.9, pan(700)); // fletched arrow cut along (2.9 -> 3.4)
|
|
888
|
+
// specimens ride the arrow, landing in the model at 4.31 + 0.2k (k = 0..7)
|
|
889
|
+
for (let j = 0; j < 8; j++) k.noise(4.31 + j * 0.2, { dur: 0.05, filter: 'bandpass', f: 900, q: 1, gain: 0.12, a: 0.004, pan: pan(880), minor: true });
|
|
890
|
+
// examples cartouche: 10 -> 1,000,000 over 3.0..5.6 (inOut); wheel ticks follow the speed, at most 12/s
|
|
891
|
+
for (let t = 3.05; t < 5.55;) { const e = cl((t - 3.0) / 2.6), sp = 6 * e * (1 - e); wheel(t, 0.6 + 0.4 * Math.min(1, sp), pan(740)); t += 1 / (4 + 8 * Math.min(1, sp)); }
|
|
892
|
+
k.play('tock', 5.6, { f: 560, gain: 0.6, pan: pan(740) }); k.play('thud', 5.6, { f: 120, gain: 0.25, pan: pan(740) }); // 1,000,000 lands
|
|
893
|
+
|
|
894
|
+
// ---- Plate XXVI pulled (6.0 -> 6.55)
|
|
895
|
+
pull(6.0, 0.55, 1);
|
|
896
|
+
burin(6.75, 0.3, 0.8, pan(290)); // arrow into the lily
|
|
897
|
+
k.play('paper', 7.0, { dur: 0.12, gain: 0.25, pan: pan(580), minor: true }); // ribbon unfurls
|
|
898
|
+
burin(7.02, 0.33, 0.9, pan(580)); // "dog?" engraved into the ribbon (7.0 -> 7.35)
|
|
899
|
+
burin(7.3, 0.35, 0.8, pan(780)); // arrow to the lens
|
|
900
|
+
k.play('tock', 7.4, { f: 380, gain: 0.45, pan: pan(980) }); // the lens is set down over the specimen
|
|
901
|
+
// WRONG: two red brushed slashes (7.8 -> 7.925 -> 8.05), then a red ring (7.95 -> 8.3)
|
|
902
|
+
wash(7.8, 0.12, 1.4, pan(580)); wash(7.925, 0.12, 1.4, pan(580));
|
|
903
|
+
k.play('error', 7.8, { f: 'E3', gain: 0.5, pan: pan(580) });
|
|
904
|
+
wash(7.95, 0.35, 1.1, pan(580), true);
|
|
905
|
+
burin(8.2, 0.35, 0.8, pan(1000)); // error arrow
|
|
906
|
+
// dials turn (quantized to 12 fps, 8.65 + i*0.12, 0.6 s each): a brass detent per frame
|
|
907
|
+
for (let f = Math.ceil(8.66 * 12); f / 12 < 9.49; f++) k.play('click', f / 12, { f: 850 + (f % 3) * 80, gain: 0.5, pan: pan(620 + (f % 3) * 103), minor: true });
|
|
908
|
+
burin(9.05, 0.3, 0.8, pan(520)); // arrow back to the guess
|
|
909
|
+
// CORRECTION: "cat ✓" engraved into a green-washed ribbon (9.35 -> 9.7), green ring (9.6 -> 9.95)
|
|
910
|
+
k.play('paper', 9.35, { dur: 0.12, gain: 0.25, pan: pan(580) });
|
|
911
|
+
burin(9.37, 0.33, 1.0, pan(580), false);
|
|
912
|
+
wash(9.6, 0.35, 0.9, pan(580));
|
|
913
|
+
k.play('success', 9.62, { f: 'F5', gain: 0.55, pan: pan(580) });
|
|
914
|
+
|
|
915
|
+
// ---- Plate XXVII pulled (10.3 -> 10.85)
|
|
916
|
+
pull(10.3, 0.55, 1);
|
|
917
|
+
// four lilies grow (10.9 + 0.42i, 0.45 s ease-out): a soft wash bloom with a low plucked note rising
|
|
918
|
+
['F4', 'A4', 'C5', 'F5'].forEach((n, i) => { const t0 = 10.9 + i * 0.42; wash(t0, 0.4, 0.5, pan([180, 360, 540, 730][i]), true); k.tone(t0 + 0.05, { f: n, type: 'triangle', dur: 0.6, gain: 0.07, a: 0.02, verb: 0.3, pan: pan([180, 360, 540, 730][i]), minor: true }); });
|
|
919
|
+
burin(11.0, 1.5, 0.45, pan(380)); // 'better & better' arrow cut slowly (11.0 -> 12.5)
|
|
920
|
+
{ let last = 61, lastT = -1; // Accuracy cartouche 61 -> 97 over 10.95..12.5, a tick per new value, <= 12/s
|
|
921
|
+
for (let t = 10.95; t < 12.5; t += 1 / 120) { const v = Math.round(61 + 36 * (1 - Math.pow(1 - cl((t - 10.95) / 1.55), 2.3))); if (v !== last && t - lastT >= 1 / 12) { wheel(t, 0.9, pan(1040), 1300 + (v - 61) * 12); last = v; lastT = t; } } }
|
|
922
|
+
// 97% lands and the colourist rings it red (12.5): the big hit; then the round-4 lily (12.7)
|
|
923
|
+
k.play('impact', 12.5, { gain: 1.3, pan: pan(1040) });
|
|
924
|
+
k.play('ding', 12.5, { f: 'C5', gain: 0.45, pan: pan(1040) });
|
|
925
|
+
wash(12.52, 0.35, 1.2, pan(1040));
|
|
926
|
+
wash(12.7, 0.35, 0.8, pan(730), true);
|
|
927
|
+
// takeaway engraved (13.0 -> 13.45), resolves
|
|
928
|
+
burin(13.0, 0.45, 1.0, pan(1040), false);
|
|
929
|
+
['F4', 'C5', 'A5'].forEach((f, i) => k.play('ding', 13.47 + i * 0.08, { f, gain: 0.4 - i * 0.07, pan: (i - 1) * 0.25 }));
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
// ===== END EXPLAINER =====
|
|
933
|
+
});
|
|
934
|
+
}
|