@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,738 @@
|
|
|
1
|
+
{
|
|
2
|
+
// ---------- FLIR-style thermal image ----------
|
|
3
|
+
const TMIN = 16, TMAX = 72;
|
|
4
|
+
const IRONBOW = [
|
|
5
|
+
[0.00, [0, 0, 6]], [0.08, [16, 4, 64]], [0.20, [72, 6, 130]], [0.33, [140, 12, 140]],
|
|
6
|
+
[0.46, [196, 34, 96]], [0.58, [232, 70, 40]], [0.70, [248, 120, 12]], [0.82, [252, 184, 24]],
|
|
7
|
+
[0.92, [254, 232, 110]], [1.00, [255, 255, 236]],
|
|
8
|
+
];
|
|
9
|
+
function buildLUT() {
|
|
10
|
+
const lut = new Uint8ClampedArray(1024 * 3);
|
|
11
|
+
for (let i = 0; i < 1024; i++) {
|
|
12
|
+
const v = i / 1023;
|
|
13
|
+
let k = 0; while (k < IRONBOW.length - 2 && v > IRONBOW[k + 1][0]) k++;
|
|
14
|
+
const [a, ca] = IRONBOW[k], [b, cb] = IRONBOW[k + 1], f = H.clamp((v - a) / (b - a));
|
|
15
|
+
for (let c = 0; c < 3; c++) lut[i * 3 + c] = ca[c] + (cb[c] - ca[c]) * f;
|
|
16
|
+
}
|
|
17
|
+
return lut;
|
|
18
|
+
}
|
|
19
|
+
const g = (T) => { const v = H.clamp((T - TMIN) / (TMAX - TMIN)) * 255; return `rgb(${v},${v},${v})`; };
|
|
20
|
+
const ga = (T, a) => { const v = H.clamp((T - TMIN) / (TMAX - TMIN)) * 255; return `rgba(${v},${v},${v},${a})`; };
|
|
21
|
+
|
|
22
|
+
// ================= EXPLAINER: the IR camera films a model learning; heat = activity, cold = error =================
|
|
23
|
+
let exLUT = null;
|
|
24
|
+
function catSil(f, cx, cy, s, T, Tf) {
|
|
25
|
+
f.fillStyle = g(T);
|
|
26
|
+
f.beginPath(); f.ellipse(cx, cy + s * 0.08, s * 0.95, s * 0.78, 0, 0, 7); f.fill();
|
|
27
|
+
f.beginPath(); f.moveTo(cx - s * 0.9, cy - s * 0.2); f.lineTo(cx - s * 0.78, cy - s * 1.05); f.lineTo(cx - s * 0.3, cy - s * 0.62); f.fill();
|
|
28
|
+
f.beginPath(); f.moveTo(cx + s * 0.9, cy - s * 0.2); f.lineTo(cx + s * 0.78, cy - s * 1.05); f.lineTo(cx + s * 0.3, cy - s * 0.62); f.fill();
|
|
29
|
+
f.fillStyle = g(Tf); f.beginPath(); f.ellipse(cx - s * 0.35, cy - s * 0.05, s * 0.14, s * 0.1, 0, 0, 7); f.ellipse(cx + s * 0.35, cy - s * 0.05, s * 0.14, s * 0.1, 0, 0, 7); f.fill();
|
|
30
|
+
f.beginPath(); f.ellipse(cx, cy + s * 0.25, s * 0.12, s * 0.08, 0, 0, 7); f.fill();
|
|
31
|
+
}
|
|
32
|
+
function dogSil(f, cx, cy, s, T, Tf) {
|
|
33
|
+
f.fillStyle = g(T);
|
|
34
|
+
f.beginPath(); f.ellipse(cx, cy, s * 0.7, s * 0.85, 0, 0, 7); f.fill();
|
|
35
|
+
f.beginPath(); f.ellipse(cx - s * 0.78, cy - s * 0.05, s * 0.26, s * 0.6, 0.25, 0, 7); f.ellipse(cx + s * 0.78, cy - s * 0.05, s * 0.26, s * 0.6, -0.25, 0, 7); f.fill();
|
|
36
|
+
f.beginPath(); f.ellipse(cx, cy + s * 0.42, s * 0.34, s * 0.26, 0, 0, 7); f.fill();
|
|
37
|
+
f.fillStyle = g(Tf); f.beginPath(); f.ellipse(cx - s * 0.28, cy - s * 0.2, s * 0.11, s * 0.09, 0, 0, 7); f.ellipse(cx + s * 0.28, cy - s * 0.2, s * 0.11, s * 0.09, 0, 0, 7); f.fill();
|
|
38
|
+
f.fillStyle = g(T + 6); f.beginPath(); f.ellipse(cx, cy + s * 0.32, s * 0.13, s * 0.09, 0, 0, 7); f.fill();
|
|
39
|
+
}
|
|
40
|
+
// static sensor layers, built once: noise, sensor canvases, slow convective mottling field, column FPN
|
|
41
|
+
const MOT_PAD = 48;
|
|
42
|
+
let exNz = null, exF = null, exB = null, exMot = null, exFPN = null, exEdge = null, exSoft = null, exIX = null, exAX = null;
|
|
43
|
+
function exCache() {
|
|
44
|
+
if (exNz) return;
|
|
45
|
+
exNz = H.makeNoise(17); exLUT = buildLUT();
|
|
46
|
+
exF = H.layer(640, 360); exB = H.layer(640, 360); exB.getContext('2d').filter = 'blur(3px)';
|
|
47
|
+
// same temperature mottling as the still (fbm2(x*.02, y*.02 + t*.05) * 3.2), pre-sampled; t scrolls it by 2.5 px/s
|
|
48
|
+
exMot = new Float32Array(640 * (360 + MOT_PAD));
|
|
49
|
+
for (let y = 0; y < 360 + MOT_PAD; y++) for (let x = 0; x < 640; x++) exMot[y * 640 + x] = exNz.fbm2(x * 0.02, y * 0.02, 3) * 5.4;
|
|
50
|
+
const R = H.rng(4243); exFPN = new Float32Array(4096);
|
|
51
|
+
for (let x = 0; x < 4096; x++) exFPN[x] = (R() - 0.5) * 0.012 + Math.sin(x * 0.9) * 0.002;
|
|
52
|
+
exEdge = new Float32Array(640 * 360); exSoft = new Float32Array(640 * 360);
|
|
53
|
+
}
|
|
54
|
+
function thermalExplainer(ctx, W, Hh, t) {
|
|
55
|
+
exCache();
|
|
56
|
+
const sm = H.smooth, E = H.ease, nz = exNz, Rn = H.rng(5000 + Math.floor(t * 30));
|
|
57
|
+
const FW = 640, FH = 360, F = exF, f = F.getContext('2d');
|
|
58
|
+
f.setTransform(1, 0, 0, 1, 0, 0);
|
|
59
|
+
// scenes separated by the camera's shutter calibration click (NUC)
|
|
60
|
+
const NUC = [[0, 0.3], [6.0, 6.22], [10.45, 10.67]];
|
|
61
|
+
const inNUC = NUC.some(([a, b]) => t >= a && t < b);
|
|
62
|
+
const scene = t < 6.1 ? 1 : t < 10.55 ? 2 : 3;
|
|
63
|
+
const sceneStart = scene === 1 ? 0.3 : scene === 2 ? 6.22 : 10.67;
|
|
64
|
+
const settle = sm(sceneStart, sceneStart + 0.6, t);
|
|
65
|
+
const shakeX = nz.n2(t * 0.8, 50) * 5, shakeY = nz.n2(60, t * 0.7) * 3.5;
|
|
66
|
+
f.scale(FW / W, FH / Hh); f.translate(shakeX, shakeY);
|
|
67
|
+
// room: warm ceiling, cool floor
|
|
68
|
+
const wg = f.createLinearGradient(0, 0, 0, Hh); wg.addColorStop(0, g(24)); wg.addColorStop(0.65, g(21.5)); wg.addColorStop(1, g(20));
|
|
69
|
+
f.fillStyle = wg; f.fillRect(-20, -20, W + 40, Hh + 40);
|
|
70
|
+
// ceiling light strip (warm) and the wall/floor junction with a cooler floor, as in the still's room
|
|
71
|
+
f.fillStyle = g(26.5); f.fillRect(-20, -20, W + 40, 30);
|
|
72
|
+
const fl = f.createLinearGradient(0, 604, 0, Hh); fl.addColorStop(0, g(21.2)); fl.addColorStop(1, g(19.6));
|
|
73
|
+
f.fillStyle = fl; f.fillRect(-20, 604, W + 40, Hh - 584);
|
|
74
|
+
f.fillStyle = g(22.6); f.fillRect(-20, 598, W + 40, 6);
|
|
75
|
+
const hud = []; // deferred HUD drawing (needs screen coords incl. shake)
|
|
76
|
+
// ---- neural net as heat: nodes = hot spots, connections = warm wires whose temperature is the weight ----
|
|
77
|
+
const net = (ox, oy, sc, heat, wts, pulses, outHot) => {
|
|
78
|
+
const L = [[-110, [-80, 0, 80]], [0, [-110, -37, 37, 110]], [110, [-40, 40]]];
|
|
79
|
+
const P = (li, k) => [ox + L[li][0] * sc, oy + L[li][1][k] * sc];
|
|
80
|
+
let ci = 0;
|
|
81
|
+
for (let li = 0; li < 2; li++) for (let a = 0; a < L[li][1].length; a++) for (let b = 0; b < L[li + 1][1].length; b++) {
|
|
82
|
+
const w = wts(ci++), A = P(li, a), B = P(li + 1, b);
|
|
83
|
+
f.strokeStyle = g(22 + w * 20 + heat * 4); f.lineWidth = 3 + w * 6;
|
|
84
|
+
f.beginPath(); f.moveTo(...A); f.lineTo(...B); f.stroke();
|
|
85
|
+
for (const [lay, u0, T] of pulses) { const u = H.clamp(u0 * 1.35 - ((ci * 7) % 5) * 0.08); if (lay === li && u > 0 && u < 1) {
|
|
86
|
+
const x = A[0] + (B[0] - A[0]) * u, y = A[1] + (B[1] - A[1]) * u, gr = f.createRadialGradient(x, y, 0, x, y, 16 * sc);
|
|
87
|
+
gr.addColorStop(0, ga(T, 0.95)); gr.addColorStop(1, ga(T, 0)); f.fillStyle = gr; f.fillRect(x - 20 * sc, y - 20 * sc, 40 * sc, 40 * sc);
|
|
88
|
+
} }
|
|
89
|
+
}
|
|
90
|
+
L.forEach((l, li) => l[1].forEach((_, k) => {
|
|
91
|
+
const [x, y] = P(li, k), hot = li === 2 ? (outHot ? outHot(k) : 0) : 0;
|
|
92
|
+
const gr = f.createRadialGradient(x, y, 0, x, y, 26 * sc);
|
|
93
|
+
gr.addColorStop(0, g(32 + heat * 6 + hot * 14)); gr.addColorStop(0.55, g(29 + heat * 4 + hot * 8)); gr.addColorStop(1, ga(25, 0));
|
|
94
|
+
f.fillStyle = gr; f.beginPath(); f.arc(x, y, 26 * sc, 0, 7); f.fill();
|
|
95
|
+
}));
|
|
96
|
+
return P;
|
|
97
|
+
};
|
|
98
|
+
const card = (x, y, kind, T, sc = 1) => {
|
|
99
|
+
f.fillStyle = g(27); f.beginPath(); f.roundRect(x - 52 * sc, y - 46 * sc, 104 * sc, 92 * sc, 6); f.fill();
|
|
100
|
+
(kind === 'cat' ? catSil : dogSil)(f, x, y + 6 * sc, 30 * sc, T, T - 5);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let header = 0;
|
|
104
|
+
if (scene === 1) {
|
|
105
|
+
// conveyor belt feeding labelled examples into the model
|
|
106
|
+
f.fillStyle = g(23); f.fillRect(-20, 486, 860, 22);
|
|
107
|
+
for (let k = 0; k < 18; k++) { f.fillStyle = g(25.5); f.fillRect(((k * 52 + t * 380) % 900) - 40, 488, 12, 18); }
|
|
108
|
+
f.fillStyle = g(38); f.beginPath(); f.arc(830, 497, 20, 0, 7); f.fill(); // warm drive motor
|
|
109
|
+
const fed = H.clamp((t - 2.6) / 0.32, 0, 9);
|
|
110
|
+
const heat = sm(2.6, 6, t);
|
|
111
|
+
// model housing (server chassis) with the net inside
|
|
112
|
+
f.fillStyle = g(25 + heat * 3); f.beginPath(); f.roundRect(830, 140, 330, 330, 18); f.fill();
|
|
113
|
+
f.fillStyle = g(23 + heat * 2); f.beginPath(); f.roundRect(848, 158, 294, 294, 12); f.fill();
|
|
114
|
+
net(995, 305, 1.05, heat, (c) => 0.3 + 0.4 * Math.abs(Math.sin(c * 1.7)), [], null);
|
|
115
|
+
// exhaust above the model grows with work
|
|
116
|
+
const ex = f.createLinearGradient(0, 60, 0, 140); ex.addColorStop(0, ga(26, 0)); ex.addColorStop(1, ga(30 + heat * 6, 0.8));
|
|
117
|
+
f.fillStyle = ex; f.fillRect(860, 60, 270, 80);
|
|
118
|
+
for (let k = 0; k < 9; k++) {
|
|
119
|
+
const sk = 2.6 + k * 0.32; if (t < sk) continue;
|
|
120
|
+
const x = -60 + (t - sk) * 380; if (x > 800) continue;
|
|
121
|
+
const kind = k % 3 === 1 ? 'dog' : 'cat';
|
|
122
|
+
card(x, 420, kind, 41, 1.25);
|
|
123
|
+
hud.push(['tag', x + shakeX, 352 + shakeY, kind]);
|
|
124
|
+
}
|
|
125
|
+
if (t > 2.7) hud.push(['counter', sm(2.7, 5.3, t)]);
|
|
126
|
+
header = sm(1.55, 2.25, t);
|
|
127
|
+
} else if (scene === 2) {
|
|
128
|
+
const cin = E.out(sm(6.25, 6.7, t));
|
|
129
|
+
card(-140 + cin * 360, 330, 'cat', 41, 1.8);
|
|
130
|
+
// weights before / after the adjustment
|
|
131
|
+
const adj = sm(8.85, 9.3, t);
|
|
132
|
+
const wts = (c) => H.lerp(0.15 + 0.75 * Math.abs(Math.sin(c * 2.3)), 0.15 + 0.75 * Math.abs(Math.cos(c * 1.1 + 0.4)), adj);
|
|
133
|
+
const pulses = [];
|
|
134
|
+
const fw1 = sm(6.75, 7.05, t), fw2 = sm(7.05, 7.35, t); // first guess: heat flows forward
|
|
135
|
+
if (fw1 > 0 && fw1 < 1) pulses.push([0, fw1, 44]); if (fw2 > 0 && fw2 < 1) pulses.push([1, fw2, 44]);
|
|
136
|
+
const bk1 = sm(8.2, 8.5, t), bk2 = sm(8.5, 8.8, t); // error: a cold wave runs backwards
|
|
137
|
+
if (bk1 > 0 && bk1 < 1) pulses.push([1, 1 - bk1, 14]); if (bk2 > 0 && bk2 < 1) pulses.push([0, 1 - bk2, 14]);
|
|
138
|
+
const g1 = sm(9.35, 9.6, t), g2 = sm(9.6, 9.85, t); // second guess
|
|
139
|
+
if (g1 > 0 && g1 < 1) pulses.push([0, g1, 44]); if (g2 > 0 && g2 < 1) pulses.push([1, g2, 44]);
|
|
140
|
+
const outHot = (k) => k === 1 ? sm(7.3, 7.45, t) * (1 - sm(8.2, 8.5, t)) : sm(9.8, 9.95, t);
|
|
141
|
+
net(640, 330, 1.25, 0.6, wts, pulses, outHot);
|
|
142
|
+
hud.push(['loop']);
|
|
143
|
+
header = 1;
|
|
144
|
+
} else {
|
|
145
|
+
const lp = sm(10.9, 12.4, t), heat = 0.4 + lp * 0.9;
|
|
146
|
+
f.fillStyle = g(25 + heat * 2); f.beginPath(); f.roundRect(110, 170, 340, 330, 18); f.fill();
|
|
147
|
+
f.fillStyle = g(23 + heat * 2); f.beginPath(); f.roundRect(128, 188, 304, 294, 12); f.fill();
|
|
148
|
+
net(280, 335, 1.1, heat, (c) => H.lerp(0.15 + 0.75 * Math.abs(Math.sin(c * 2.3)), 0.75 + 0.2 * Math.sin(c), lp), [], () => lp * 0.6);
|
|
149
|
+
const ex = f.createLinearGradient(0, 80, 0, 170); ex.addColorStop(0, ga(26, 0)); ex.addColorStop(1, ga(30 + heat * 8, 0.85));
|
|
150
|
+
f.fillStyle = ex; f.fillRect(140, 80, 280, 90);
|
|
151
|
+
hud.push(['chart', lp]);
|
|
152
|
+
header = 1;
|
|
153
|
+
}
|
|
154
|
+
// ---- optics + palette (same pipeline as the still) ----
|
|
155
|
+
f.setTransform(1, 0, 0, 1, 0, 0);
|
|
156
|
+
const sharp = f.getImageData(0, 0, FW, FH).data;
|
|
157
|
+
const B = exB, b = B.getContext('2d'); b.clearRect(0, 0, FW, FH); b.drawImage(F, 0, 0);
|
|
158
|
+
const soft8 = b.getImageData(0, 0, FW, FH).data;
|
|
159
|
+
// convective mottling + emissivity variation (slowly drifting, as in the still), added in float so the
|
|
160
|
+
// equalisation below cannot posterise it into 8-bit contour steps
|
|
161
|
+
const soft = exSoft, mOff = t * 2.5, mRow = Math.floor(mOff) % MOT_PAD, mA = mOff - Math.floor(mOff);
|
|
162
|
+
for (let y = 0; y < FH; y++) {
|
|
163
|
+
const r0 = (y + mRow) * FW, r1 = r0 + FW, o = y * FW;
|
|
164
|
+
for (let x = 0; x < FW; x++) {
|
|
165
|
+
const v = soft8[(o + x) * 4] + exMot[r0 + x] * (1 - mA) + exMot[r1 + x] * mA;
|
|
166
|
+
soft[o + x] = v < 0 ? 0 : v > 254.99 ? 254.99 : v;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// MSX-style edge map from the sharp field (Sobel)
|
|
170
|
+
const edge = exEdge;
|
|
171
|
+
for (let y = 1; y < FH - 1; y++) {
|
|
172
|
+
const o = y * FW;
|
|
173
|
+
for (let x = 1; x < FW - 1; x++) {
|
|
174
|
+
const a = (o - FW + x) * 4, c = (o + x) * 4, e = (o + FW + x) * 4;
|
|
175
|
+
const gx = -sharp[a - 4] - 2 * sharp[c - 4] - sharp[e - 4] + sharp[a + 4] + 2 * sharp[c + 4] + sharp[e + 4];
|
|
176
|
+
const gy = -sharp[a - 4] - 2 * sharp[a] - sharp[a + 4] + sharp[e - 4] + 2 * sharp[e] + sharp[e + 4];
|
|
177
|
+
const mg = Math.sqrt(gx * gx + gy * gy) / 90; edge[o + x] = mg > 1 ? 1 : mg;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// plateau histogram equalisation, blended with the fixed linear span the scale bar reports
|
|
181
|
+
const hist = new Float32Array(256);
|
|
182
|
+
for (let i = 0; i < soft.length; i++) hist[soft[i] | 0]++;
|
|
183
|
+
const plateau = (FW * FH) / 70;
|
|
184
|
+
let tot = 0; for (let i = 0; i < 256; i++) { hist[i] = Math.min(hist[i], plateau); tot += hist[i]; }
|
|
185
|
+
const cdf = new Float32Array(257); let acc = 0;
|
|
186
|
+
for (let i = 0; i < 256; i++) { acc += hist[i]; cdf[i] = acc / tot; } cdf[256] = 1;
|
|
187
|
+
const img = ctx.createImageData(W, Hh), d = img.data, sx = FW / W, sy = FH / Hh;
|
|
188
|
+
const lo = 19.5, hi = 47, kT = (TMAX - TMIN) / (hi - lo), kO = (TMIN - lo) / (hi - lo);
|
|
189
|
+
const cont = 0.3 + 0.7 * settle;
|
|
190
|
+
if (!exIX || exIX.length !== W) { exIX = new Int32Array(W); exAX = new Float32Array(W);
|
|
191
|
+
for (let x = 0; x < W; x++) { const fx = H.clamp(x * sx - 0.5, 0, FW - 1.001); exIX[x] = fx | 0; exAX[x] = fx - (fx | 0); } }
|
|
192
|
+
for (let y = 0; y < Hh; y++) {
|
|
193
|
+
const fy = H.clamp(y * sy - 0.5, 0, FH - 1.001), iy = fy | 0, ay = fy - iy, rowN = (Rn() - 0.5) * 0.006;
|
|
194
|
+
for (let x = 0; x < W; x++) {
|
|
195
|
+
const ix = exIX[x], ax = exAX[x], j = iy * FW + ix;
|
|
196
|
+
const v = ((soft[j] * (1 - ax) + soft[j + 1] * ax) * (1 - ay) + (soft[j + FW] * (1 - ax) + soft[j + FW + 1] * ax) * ay) / 255;
|
|
197
|
+
const e = (edge[j] * (1 - ax) + edge[j + 1] * ax) * (1 - ay) + (edge[j + FW] * (1 - ax) + edge[j + FW + 1] * ax) * ay;
|
|
198
|
+
const v255 = v * 255, ci = v255 | 0, cf = v255 - ci, q = cdf[ci] * (1 - cf) + cdf[ci + 1] * cf;
|
|
199
|
+
let n = 0.4 * q + 0.6 * (v * kT + kO);
|
|
200
|
+
n = 0.4 + (n - 0.4) * cont;
|
|
201
|
+
if (inNUC) n = 0.36 + (x / W - 0.5) * 0.02;
|
|
202
|
+
else n += e * 0.05 + exFPN[x];
|
|
203
|
+
n += (Rn() + Rn() - 1) * 0.02 + rowN;
|
|
204
|
+
const li = (n < 0 ? 0 : n > 1 ? 1 : n) * 1023 | 0, i = (y * W + x) * 4;
|
|
205
|
+
d[i] = exLUT[li * 3]; d[i + 1] = exLUT[li * 3 + 1]; d[i + 2] = exLUT[li * 3 + 2]; d[i + 3] = 255;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
ctx.putImageData(img, 0, 0);
|
|
209
|
+
H.vignette(ctx, W, Hh, 0.35, '0,0,10');
|
|
210
|
+
|
|
211
|
+
// ---- HUD (the camera's own annotation layer does the explaining) ----
|
|
212
|
+
ctx.save(); ctx.textBaseline = 'middle';
|
|
213
|
+
const T_ = (s, x, y, font, align = 'left', col = '#fff') => { ctx.font = font; ctx.textAlign = align; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillText(s, x + 1.5, y + 1.5); ctx.fillStyle = col; ctx.fillText(s, x, y); };
|
|
214
|
+
const Ln = (pts, w = 2, col = '#fff', prog = 1) => {
|
|
215
|
+
let total = 0; for (let i = 1; i < pts.length; i++) total += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
216
|
+
const draw = (c, lw) => { ctx.strokeStyle = c; ctx.lineWidth = lw; ctx.beginPath(); ctx.moveTo(...pts[0]); let acc = 0;
|
|
217
|
+
for (let i = 1; i < pts.length; i++) { const L = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); if (acc + L <= total * prog) { ctx.lineTo(...pts[i]); acc += L; } else { const u = (total * prog - acc) / L; ctx.lineTo(pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * u, pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * u); break; } } ctx.stroke(); };
|
|
218
|
+
draw('rgba(0,0,0,0.55)', w + 2); draw(col, w);
|
|
219
|
+
};
|
|
220
|
+
const Arw = (pts, prog, col = '#fff') => {
|
|
221
|
+
if (prog <= 0) return; Ln(pts, 2.5, col, prog);
|
|
222
|
+
if (prog >= 1) { const e = pts[pts.length - 1], p = pts[pts.length - 2], a = Math.atan2(e[1] - p[1], e[0] - p[0]);
|
|
223
|
+
Ln([[e[0] + Math.cos(a + 2.6) * 14, e[1] + Math.sin(a + 2.6) * 14], e, [e[0] + Math.cos(a - 2.6) * 14, e[1] + Math.sin(a - 2.6) * 14]], 2.5, col); }
|
|
224
|
+
};
|
|
225
|
+
const curve = (x0, y0, qx, qy, x1, y1, n = 24) => Array.from({ length: n + 1 }, (_, i) => { const u = i / n; return [(1 - u) * (1 - u) * x0 + 2 * (1 - u) * u * qx + u * u * x1, (1 - u) * (1 - u) * y0 + 2 * (1 - u) * u * qy + u * u * y1]; });
|
|
226
|
+
const box = (x, y, w, h, prog, col = '#fff') => Ln([[x + w / 2, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y], [x + w / 2, y]], 2, col, prog);
|
|
227
|
+
const tri = (x, y, col) => { ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x - 9, y + 8); ctx.lineTo(x + 9, y + 8); ctx.lineTo(x, y - 8); ctx.fill(); };
|
|
228
|
+
if (!inNUC) {
|
|
229
|
+
// status bar
|
|
230
|
+
T_('ε 0.95 IRONBOW', 1090, 34, '400 16px "Inter"', 'right');
|
|
231
|
+
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(1112, 25, 34, 17); ctx.fillStyle = '#fff'; ctx.fillRect(1146, 30, 3, 7); ctx.fillRect(1115, 28, 21, 11);
|
|
232
|
+
T_('REC', 1176, 34, '600 16px "Inter"'); if (Math.floor(t * 2) % 2 === 0) { ctx.fillStyle = '#ff3b2a'; ctx.beginPath(); ctx.arc(1166, 34, 5, 0, 7); ctx.fill(); }
|
|
233
|
+
// colour scale bar
|
|
234
|
+
const sg = ctx.createLinearGradient(0, 600, 0, 112); IRONBOW.forEach(([p, c]) => sg.addColorStop(p, H.rgba(c, 1)));
|
|
235
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(1216, 110, 26, 494); ctx.fillStyle = sg; ctx.fillRect(1218, 112, 22, 490);
|
|
236
|
+
T_('47.0', 1240, 92, '600 18px "Inter"', 'right'); T_('19.5', 1240, 622, '600 18px "Inter"', 'right'); T_('°C', 1229, 646, '400 15px "Inter"', 'center');
|
|
237
|
+
// the question: big annotation, then docks into the measurement table as the header
|
|
238
|
+
if (t > 0.35) {
|
|
239
|
+
const h = E.inOut(header), typed = 'How does AI learn?'.slice(0, Math.floor((t - 0.35) * 22));
|
|
240
|
+
const fs = H.lerp(56, 28, h), x = H.lerp(640, 30, h), y = H.lerp(300, 42, h);
|
|
241
|
+
if (h > 0.02) { ctx.fillStyle = `rgba(0,0,0,${0.38 * h})`; ctx.fillRect(18, 18, 340 * h, 50); }
|
|
242
|
+
T_(typed, x, y, `600 ${fs}px "Inter"`, h < 0.5 ? 'center' : 'left');
|
|
243
|
+
if (h < 0.05 && t > 0.6) { ctx.strokeStyle = '#fff'; Ln([[640 - 30, 360], [640 - 9, 360]]); Ln([[649, 360], [670, 360]]); Ln([[640, 330 + 10], [640, 351]]); Ln([[640, 369], [640, 390]]); }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const it of hud) {
|
|
247
|
+
if (inNUC) break;
|
|
248
|
+
if (it[0] === 'tag') {
|
|
249
|
+
const [, x, y, kind] = it;
|
|
250
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(x - 42, y - 40, 84, 36);
|
|
251
|
+
T_(kind, x, y - 22, '800 26px "Inter"', 'center'); Ln([[x, y - 4], [x, y + 12]], 1.5);
|
|
252
|
+
} else if (it[0] === 'counter') {
|
|
253
|
+
const p = it[1], v = p >= 1 ? 1000000 : Math.round(Math.pow(10, 6 * p));
|
|
254
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(18, 548, 560, 156);
|
|
255
|
+
T_('TRAINING DATA', 34, 576, '600 24px "Inter"', 'left', 'rgba(255,255,255,0.85)');
|
|
256
|
+
T_(v.toLocaleString('en-US'), 34, 642, '800 68px "Inter"'); T_('examples', 560, 660, '700 30px "Inter"', 'right');
|
|
257
|
+
Arw([[560, 330], [700, 330], [820, 300]], sm(2.7, 3.1, t));
|
|
258
|
+
T_('model', 995, 110, '800 30px "Inter"', 'center'); box(838, 132, 314, 346, sm(2.9, 3.4, t));
|
|
259
|
+
} else if (it[0] === 'loop') {
|
|
260
|
+
T_('new picture', 220, 214, '800 28px "Inter"', 'center', 'rgba(255,255,255,0.9)');
|
|
261
|
+
// GUESS arrow -> answer
|
|
262
|
+
Arw([[790, 330], [900, 330]], sm(7.3, 7.55, t));
|
|
263
|
+
T_('Guess', 845, 296, '800 32px "Inter"', 'center');
|
|
264
|
+
const ans = t < 9.85 ? 'dog?' : 'cat';
|
|
265
|
+
if (t > 7.5) T_(ans, 1010, 330, '800 54px "Inter"', 'center', t < 9.85 ? '#fff' : '#7dff8a');
|
|
266
|
+
// CHECK: red box flashes round the wrong answer
|
|
267
|
+
if (t > 7.75 && t < 9.85) {
|
|
268
|
+
const fl = t < 8.4 ? (Math.floor(t * 8) % 2 ? 1 : 0.25) : 1;
|
|
269
|
+
ctx.globalAlpha = fl; box(930, 286, 160, 88, sm(7.75, 8.0, t), '#ff3b2a');
|
|
270
|
+
T_('✗', 1120, 330, '800 46px "Inter"', 'center', '#ff3b2a'); ctx.globalAlpha = 1;
|
|
271
|
+
T_('Check', 1010, 256, '800 32px "Inter"', 'center', '#ff8a7a');
|
|
272
|
+
}
|
|
273
|
+
if (t >= 9.85) { box(930, 286, 160, 88, 1, '#7dff8a'); T_('✓', 1120, 330, '800 46px "Inter"', 'center', '#7dff8a'); T_('Check', 1010, 256, '800 32px "Inter"', 'center', '#7dff8a'); }
|
|
274
|
+
// ADJUST: error flows back (the cold wave), weights retune
|
|
275
|
+
Arw(curve(1010, 390, 860, 560, 660, 480), sm(8.2, 8.8, t), '#8fd3ff');
|
|
276
|
+
if (t > 8.5) T_('Adjust', 830, 556, '800 32px "Inter"', 'center', '#8fd3ff');
|
|
277
|
+
if (t > 8.85 && t < 9.4) { tri(640, 470, '#28a8ff'); T_('weights retuned', 640, 506, '800 26px "Inter"', 'center'); }
|
|
278
|
+
tri(780, 380 - 0, t > 7.4 && t < 8.6 ? '#ff2a1a' : 'rgba(0,0,0,0)');
|
|
279
|
+
} else if (it[0] === 'chart') {
|
|
280
|
+
const lp = it[1];
|
|
281
|
+
ctx.fillStyle = 'rgba(0,0,0,0.42)'; ctx.fillRect(560, 130, 620, 380);
|
|
282
|
+
T_('LINE PROFILE · accuracy per training round', 578, 154, '600 20px "Inter"', 'left', 'rgba(255,255,255,0.8)');
|
|
283
|
+
Ln([[600, 180], [600, 470], [1150, 470]], 2, '#fff', sm(10.7, 10.95, t));
|
|
284
|
+
T_('rounds →', 1150, 492, '700 24px "Inter"', 'right', 'rgba(255,255,255,0.85)');
|
|
285
|
+
const pts = []; for (let r = 0; r <= 24; r++) { const acc = 0.97 - 0.36 * Math.exp(-r / 6) + (r > 0 && r < 24 ? Math.sin(r * 3.1) * 0.012 : 0); pts.push([610 + r * 22, 470 - (acc - 0.5) / 0.5 * 280]); }
|
|
286
|
+
if (lp > 0) Ln(pts, 3, '#ffd23a', lp);
|
|
287
|
+
const pct = Math.round(61 + 36 * E.out(lp));
|
|
288
|
+
ctx.fillStyle = 'rgba(0,0,0,0.45)'; ctx.fillRect(18, 80, 300, 46);
|
|
289
|
+
T_('Accuracy', 30, 103, '600 28px "Inter"'); T_(pct + ' %', 306, 102, '800 30px "Inter"', 'right', pct >= 97 ? '#7dff8a' : '#fff');
|
|
290
|
+
ctx.fillStyle = 'rgba(8,0,18,0.7)'; ctx.fillRect(915, 210, 170, 80); // readout plate: the profile runs behind it
|
|
291
|
+
T_(pct + '%', 1000, 250, '800 64px "Inter"', 'center', pct >= 97 ? '#7dff8a' : '#fff');
|
|
292
|
+
if (lp >= 1) { const fl = t < 12.9 ? (Math.floor(t * 8) % 2 ? 1 : 0.3) : 1; ctx.globalAlpha = fl; box(915, 210, 170, 80, sm(12.4, 12.65, t), '#7dff8a'); ctx.globalAlpha = 1; tri(pts[24][0], pts[24][1] - 16, '#ff2a1a'); }
|
|
293
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(222, 129, 116, 36);
|
|
294
|
+
T_('model', 280, 146, '800 30px "Inter"', 'center');
|
|
295
|
+
if (t > 13.0) {
|
|
296
|
+
const tp = sm(13.0, 13.3, t);
|
|
297
|
+
ctx.fillStyle = `rgba(0,0,0,${0.6 * tp})`; ctx.fillRect(110, 548, 1060, 100);
|
|
298
|
+
const s = 'More examples → better guesses'; T_(s.slice(0, Math.floor((t - 13.0) * 40)), 640, 598, '800 52px "Inter"', 'center');
|
|
299
|
+
box(110, 548, 1060, 100, tp);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (inNUC && t > 0.05) T_('NUC', W / 2, Hh / 2, '600 22px "Inter"', 'center', 'rgba(255,255,255,0.85)');
|
|
304
|
+
T_(`2026-10-09 23:41:${String(7 + Math.floor(t)).padStart(2, '0')}`, 1180, 700, '600 15px "IBM Plex Mono"', 'right', 'rgba(255,255,255,0.8)');
|
|
305
|
+
ctx.restore();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Sound: we are holding the IR camera. Its electronics whisper under everything; scene changes are the shutter-flag
|
|
309
|
+
// NUC calibration (solenoid clack in, clack out); the HUD speaks in soft low beeps and data ticks; heat moving through
|
|
310
|
+
// the net is a warm low swell, the error is a cold falling hiss; the measurement-box alarm beeps in step with its
|
|
311
|
+
// flashing; the model's fan spins up as it works. The takeaway lands as a snapshot capture + save tone.
|
|
312
|
+
// All times are copied from thermalExplainer() above (same NUC table, smooth/ease windows and flash formulas).
|
|
313
|
+
// Voiceover mode (k.vo): beds drop out; typing ticks, counter ticks, card thumps, HUD line draws and fan are minor.
|
|
314
|
+
function thermalSound(k) {
|
|
315
|
+
const sm = H.smooth, E = H.ease, D = 15;
|
|
316
|
+
const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
|
|
317
|
+
// shutter flag solenoid: a dry plastic clack with a tiny rattle, no ring
|
|
318
|
+
const clack = (t, g = 1) => {
|
|
319
|
+
k.noise(t, { dur: 0.018, filter: 'bandpass', f: 2100, q: 1.1, gain: 0.4 * g, a: 0.001 });
|
|
320
|
+
k.tone(t, { f: [[0, 260], [0.035, 110]], dur: 0.05, gain: 0.26 * g });
|
|
321
|
+
k.noise(t + 0.022, { dur: 0.012, filter: 'bandpass', f: 1600, q: 2, gain: 0.14 * g, a: 0.001 });
|
|
322
|
+
};
|
|
323
|
+
const nuc = (a, b) => { clack(a, 1); k.noise(a + 0.02, { dur: b - a - 0.02, filter: 'bandpass', f: 900, q: 3, gain: 0.03 }); clack(b, 0.8); };
|
|
324
|
+
// HUD beep: soft, low, slightly square (camera piezo through a lowpass)
|
|
325
|
+
const beep = (t, f = 1050, dur = 0.06, g = 1, pan = 0, minor = false) => k.tone(t, { f, type: 'square', dur, gain: 0.05 * g, a: 0.003, hold: dur * 0.7, filter: 'lowpass', f2: 1800, q: 0.7, pan, minor });
|
|
326
|
+
// data tick: what a HUD annotation "sounds" like as it writes; throttled so it stays under ~12/s
|
|
327
|
+
const ticks = (t0, n, rate, every, pan = 0, g = 1) => { for (let i = 0; i < n; i += every)
|
|
328
|
+
k.noise(t0 + i / rate, { dur: 0.012, filter: 'bandpass', f: 1700 + (i % 3) * 120, q: 3, gain: 0.2 * g, a: 0.001, pan, minor: true }); };
|
|
329
|
+
// HUD line drawing on: a faint electronic trace that rises slightly, then a beep when it completes
|
|
330
|
+
const trace = (a, b, pan = 0, f0 = 420) => k.tone(a, { f: [[0, f0], [b - a, f0 * 1.35]], type: 'triangle', dur: b - a, gain: 0.03, a: 0.03, hold: (b - a) * 0.7, pan, minor: true });
|
|
331
|
+
// heat flowing forward through a layer: warm, low, breathy swell
|
|
332
|
+
const heat = (a, b, pan = 0) => { const d = b - a;
|
|
333
|
+
k.noise(a, { dur: d + 0.1, color: 'brown', filter: 'lowpass', f: [[0, 220], [d, 700]], gain: 0.35, a: d * 0.6, pan });
|
|
334
|
+
k.tone(a, { f: [[0, 90], [d, 140]], dur: d + 0.1, gain: 0.07, a: d * 0.6, pan }); };
|
|
335
|
+
// cold wave running back: an airy hiss that falls
|
|
336
|
+
const cold = (a, b, pan = 0) => { const d = b - a;
|
|
337
|
+
k.noise(a, { dur: d + 0.08, color: 'pink', filter: 'bandpass', f: [[0, 2200], [d, 650]], q: 1.4, gain: 0.22, a: d * 0.3, pan }); };
|
|
338
|
+
// a dry counter tick per change, throttled
|
|
339
|
+
const counter = (val, a, b, gap, pan, avoid = []) => { let last = -1, prev = val(a);
|
|
340
|
+
for (let t = a; t <= b; t += 0.002) { const v = val(t);
|
|
341
|
+
if (v !== prev && t - last >= gap && !avoid.some((x) => t > x - 0.03 && t < x + 0.1)) { k.play('tick', t, { f: 2300, gain: 1.15, pan, minor: true }); last = t; }
|
|
342
|
+
prev = v; } };
|
|
343
|
+
// the model's cooling fan: noise band that rises with the heat (amb bus, minor, kept low)
|
|
344
|
+
const fan = (a, b, f0, f1, g) => k.noise(a, { dur: b - a, color: 'pink', filter: 'bandpass', f: [[0, f0], [b - a - 0.3, f1]], q: 2.5, gain: g, a: 0.6, hold: b - a - 1.0, bus: 'amb', pan: 0.3, minor: true });
|
|
345
|
+
|
|
346
|
+
// ---- beds: the camera's electronics, the room
|
|
347
|
+
k.bed('room', 0, D, { gain: 0.45 });
|
|
348
|
+
k.bed('hiss', 0, D, { gain: 0.9 });
|
|
349
|
+
k.tone(0.3, { f: 165, type: 'triangle', dur: D - 0.3, gain: 0.006, a: 0.5, hold: D - 1.3, bus: 'amb', minor: true }); // faint core whine, low
|
|
350
|
+
|
|
351
|
+
// ---- 0–0.3 opens on a NUC; 0.3–0.9 auto-gain settles (a short soft rise)
|
|
352
|
+
nuc(0.0, 0.3);
|
|
353
|
+
k.tone(0.32, { f: [[0, 300], [0.5, 460]], type: 'sine', dur: 0.55, gain: 0.03, a: 0.3 });
|
|
354
|
+
// title types at 22 chars/s from 0.35 (18 chars); spot-meter crosshair appears at 0.6
|
|
355
|
+
ticks(0.35, 18, 22, 2, 0, 0.9);
|
|
356
|
+
beep(0.6, 880, 0.05, 0.8, 0, true);
|
|
357
|
+
// header docks 1.55–2.25 (eased inOut): a soft smooth low slide, fastest in the middle
|
|
358
|
+
k.noise(1.55, { dur: 0.7, color: 'pink', filter: 'lowpass', f: [[0, 300], [0.35, 900], [0.7, 300]], gain: 0.06, a: 0.35, pan: -0.4 });
|
|
359
|
+
|
|
360
|
+
// ---- scene 1: conveyor runs from 0.3 (belt rumble + drive motor, right side)
|
|
361
|
+
k.noise(0.3, { dur: 5.75, color: 'brown', filter: 'bandpass', f: 180, q: 1.2, gain: 0.16, a: 0.4, hold: 5.0, bus: 'amb', pan: -0.2, minor: true });
|
|
362
|
+
k.tone(0.3, { f: 98, type: 'sawtooth', dur: 5.75, gain: 0.012, a: 0.4, hold: 5.0, filter: 'lowpass', f2: 400, pan: 0.5, minor: true });
|
|
363
|
+
// cards drop onto the belt at 2.6 + k*0.32 (short soft tap each)
|
|
364
|
+
k.withMinor(() => { for (let c = 0; c < 9; c++) { const tc = 2.6 + c * 0.32;
|
|
365
|
+
k.tone(tc, { f: [[0, 150], [0.06, 70]], dur: 0.08, gain: 0.15, pan: -0.8 }); k.noise(tc, { dur: 0.03, filter: 'bandpass', f: 700, q: 1, gain: 0.14, pan: -0.8 }); } });
|
|
366
|
+
// arrow draws 2.7–3.1, model box 2.9–3.4
|
|
367
|
+
trace(2.7, 3.1, 0.2); beep(3.1, 940, 0.05, 0.7, 0.2, true);
|
|
368
|
+
trace(2.9, 3.4, 0.5, 520); beep(3.4, 1180, 0.08, 1.0, 0.5);
|
|
369
|
+
// TRAINING DATA counter 10^(6p), p = sm(2.7, 5.3); locks on 1,000,000
|
|
370
|
+
counter((t) => { const p = sm(2.7, 5.3, t); return p >= 1 ? 1e6 : Math.round(Math.pow(10, 6 * p)); }, 2.72, 5.25, 0.09, -0.5, [2.92, 3.24, 3.4, 3.56, 3.88, 4.2, 4.52, 4.84, 5.16]);
|
|
371
|
+
beep(5.3, 1180, 0.05, 1, -0.5); beep(5.37, 1580, 0.09, 1, -0.5);
|
|
372
|
+
// the chassis heats sm(2.6, 6): its fan spins up
|
|
373
|
+
fan(2.6, 6.15, 500, 1100, 0.05);
|
|
374
|
+
|
|
375
|
+
// ---- NUC 6.0–6.22 (scene change)
|
|
376
|
+
nuc(6.0, 6.22);
|
|
377
|
+
|
|
378
|
+
// ---- scene 2: cat card slides in E.out(sm(6.25, 6.7)) — a soft smooth slide
|
|
379
|
+
k.noise(6.25, { dur: 0.45, color: 'pink', filter: 'lowpass', f: [[0, 800], [0.45, 250]], gain: 0.14, a: 0.02, pan: -0.5 });
|
|
380
|
+
beep(6.4, 880, 0.04, 0.6, -0.6, true); // "new picture" tag
|
|
381
|
+
heat(6.75, 7.05, -0.1); heat(7.05, 7.35, 0.15); // first guess: heat flows forward
|
|
382
|
+
k.tone(7.3, { f: 180, dur: 0.25, gain: 0.08, a: 0.1, pan: 0.25 }); // output node glows hot (7.3–7.45)
|
|
383
|
+
trace(7.3, 7.55, 0.3); beep(7.4, 740, 0.04, 0.7, 0.3, true); // Guess arrow + hot-spot marker
|
|
384
|
+
beep(7.5, 1050, 0.09, 1.1, 0.45); // the answer "dog?" posts
|
|
385
|
+
// WRONG: red box + ✗ flash at 8 Hz (on when floor(t*8) is odd): on at 7.875, 8.125, then steady from 8.375
|
|
386
|
+
k.play('thud', 7.875, { f: 110, gain: 0.6, pan: 0.5 });
|
|
387
|
+
[7.875, 8.125, 8.375].forEach((ta, i) => { beep(ta, 620, 0.1, 1.6, 0.5); beep(ta, 930, 0.1, 0.8, 0.5); });
|
|
388
|
+
// error flows back as a cold wave 8.2–8.5, 8.5–8.8 (arrow 8.2–8.8), "Adjust" 8.5
|
|
389
|
+
cold(8.2, 8.5, 0.25); cold(8.5, 8.8, -0.1);
|
|
390
|
+
beep(8.5, 820, 0.04, 0.6, 0.0, true);
|
|
391
|
+
// weights retuned 8.85–9.3: a low servo-like retune sweep under the label
|
|
392
|
+
k.tone(8.85, { f: [[0, 200], [0.45, 320]], type: 'sawtooth', dur: 0.5, gain: 0.045, a: 0.05, hold: 0.35, filter: 'lowpass', f2: 700, q: 2 });
|
|
393
|
+
beep(8.85, 700, 0.05, 0.8, 0, true);
|
|
394
|
+
heat(9.35, 9.6, -0.1); heat(9.6, 9.85, 0.15); // second guess
|
|
395
|
+
// RIGHT: "cat", green box and ✓ at 9.85: the camera's confirm tones
|
|
396
|
+
k.play('success', 9.85, { f: 'C5', gain: 0.7, pan: 0.45 });
|
|
397
|
+
beep(9.85, 1050, 0.05, 0.8, 0.45);
|
|
398
|
+
|
|
399
|
+
// ---- NUC 10.45–10.67
|
|
400
|
+
nuc(10.45, 10.67);
|
|
401
|
+
|
|
402
|
+
// ---- scene 3: axes 10.7–10.95; line profile lp = sm(10.9, 12.4) with % counting 61 -> 97
|
|
403
|
+
trace(10.7, 10.95, 0.3);
|
|
404
|
+
const pct = (t) => Math.round(61 + 36 * E.out(sm(10.9, 12.4, t)));
|
|
405
|
+
const t97 = when(pct, 97, 10.9, 12.4);
|
|
406
|
+
k.tone(10.9, { f: [[0, 260], [t97 - 10.9, 520]], type: 'triangle', dur: t97 - 10.9, gain: 0.022, a: 0.2, hold: t97 - 11.2, pan: 0.3, minor: true }); // trace rising with the line
|
|
407
|
+
counter(pct, 10.92, t97 - 0.04, 0.085, 0.3);
|
|
408
|
+
beep(t97, 1180, 0.06, 1, 0.3); beep(t97 + 0.07, 1580, 0.08, 0.9, 0.3); // number turns green
|
|
409
|
+
fan(10.67, 14.95, 700, 1500, 0.055); // model heats with lp
|
|
410
|
+
// THE HIT at 12.4: lp reaches 1, measurement box locks on 97% and flashes (on 12.4, 12.625, steady from 12.875)
|
|
411
|
+
k.play('thud', 12.4, { f: 85, gain: 1.2, pan: 0.2 }); clack(12.4, 1.1);
|
|
412
|
+
k.noise(12.4, { dur: 0.5, color: 'pink', filter: 'lowpass', f: [[0, 2000], [0.45, 300]], gain: 0.22, verb: 0.3 });
|
|
413
|
+
[12.4, 12.625, 12.875].forEach((ta, i) => { beep(ta, 784, 0.11, 1.9 - i * 0.3, 0.3); beep(ta, 1175, 0.11, 1.1 - i * 0.2, 0.3); });
|
|
414
|
+
|
|
415
|
+
// ---- 13.0 takeaway panel: box draws sm(13.0, 13.3), text types at 40 chars/s (30 chars, done 13.75)
|
|
416
|
+
trace(13.0, 13.3, 0, 360);
|
|
417
|
+
ticks(13.0, 30, 40, 3, 0, 0.9);
|
|
418
|
+
// resolve: snapshot captured + saved (shutter flag, then a low two-note save tone)
|
|
419
|
+
clack(13.78, 0.9); k.noise(13.84, { dur: 0.04, filter: 'bandpass', f: 1500, q: 1, gain: 0.2 }); clack(13.86, 0.5);
|
|
420
|
+
k.tone(13.95, { f: 'G4', dur: 0.5, gain: 0.12, a: 0.005, verb: 0.3 });
|
|
421
|
+
k.tone(14.1, { f: 'C5', dur: 0.85, gain: 0.13, a: 0.005, verb: 0.35 });
|
|
422
|
+
k.tone(14.1, { f: 'E5', dur: 0.85, gain: 0.06, a: 0.01, verb: 0.35 });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
registerStyle({
|
|
426
|
+
id: 'thermal-camera',
|
|
427
|
+
name: 'Thermal Camera',
|
|
428
|
+
category: 'Light & Imaging',
|
|
429
|
+
description: 'A handheld IR camera frame in ironbow: soft low-res heat field, cold glasses and warm breath, hot coffee and GPU racks, edge-enhanced outlines, sensor noise and the camera’s own HUD with spot meter, box measurement and scale bar.',
|
|
430
|
+
motion: 'Camera clicks through its shutter calibration, the auto-gain settles, coffee is hot-poured and steam rises, rack bands heat up bottom to top while the spot meter glides onto the mug and the readouts tick live.',
|
|
431
|
+
fonts: ['600 22px "Inter"', '400 18px "Inter"', '600 16px "IBM Plex Mono"'],
|
|
432
|
+
frameT: 4.6,
|
|
433
|
+
duration: 5,
|
|
434
|
+
readsPixels: true,
|
|
435
|
+
explainerDuration: 15,
|
|
436
|
+
explainerNote: 'Everything is filmed through the IR camera: scenes change with the shutter-calibration (NUC) click, heat is activity and a cold wave is the error signal, while the camera\u2019s own HUD does the explaining with spot-meter arrows, measurement-box emphasis, tag labels riding on objects and live readouts.',
|
|
437
|
+
explainer(ctx, W, Hh, t) { thermalExplainer(ctx, W, Hh, t); },
|
|
438
|
+
explainerSound(k) { thermalSound(k); },
|
|
439
|
+
draw(ctx, W, Hh, t) {
|
|
440
|
+
const R = H.rng(4242), nz = H.makeNoise(17);
|
|
441
|
+
const Rn = H.rng(9000 + Math.floor(t * 30)); // per-frame sensor noise
|
|
442
|
+
const coffee = H.ease.out(H.smooth(0.55, 1.7, t)), mugWarm = H.smooth(0.8, 2.6, t), steamOn = H.smooth(1.1, 2.4, t);
|
|
443
|
+
const settle = H.smooth(0.42, 1.3, t);
|
|
444
|
+
const shakeX = nz.n2(t * 0.9, 50) * 4, shakeY = nz.n2(60, t * 0.8) * 3;
|
|
445
|
+
const FW = 640, FH = 360; // sensor resolution
|
|
446
|
+
const F = H.layer(FW, FH), f = F.getContext('2d');
|
|
447
|
+
f.scale(FW / W, FH / Hh); f.translate(shakeX, shakeY);
|
|
448
|
+
|
|
449
|
+
// ---- room: ceiling warmer than floor ----
|
|
450
|
+
const wg = f.createLinearGradient(0, 0, 0, Hh);
|
|
451
|
+
wg.addColorStop(0, g(24.5)); wg.addColorStop(0.6, g(22)); wg.addColorStop(1, g(20.5));
|
|
452
|
+
f.fillStyle = wg; f.fillRect(0, 0, W, Hh);
|
|
453
|
+
// ceiling light fixture warm streak, radiator glow lower-left
|
|
454
|
+
f.fillStyle = g(27); f.fillRect(0, 0, W, 10);
|
|
455
|
+
// window (cold glass, warmer frame)
|
|
456
|
+
f.fillStyle = g(19.5); f.fillRect(40, 40, 260, 300);
|
|
457
|
+
const wn = f.createLinearGradient(0, 52, 0, 330);
|
|
458
|
+
wn.addColorStop(0, g(11)); wn.addColorStop(1, g(13.5));
|
|
459
|
+
f.fillStyle = wn; f.fillRect(54, 54, 108, 272); f.fillRect(176, 54, 110, 272);
|
|
460
|
+
// reflection of the ceiling lamp in the glass (IR mirror)
|
|
461
|
+
f.fillStyle = ga(17, 0.6); f.beginPath(); f.ellipse(120, 120, 24, 70, 0.2, 0, 7); f.fill();
|
|
462
|
+
// radiator under window
|
|
463
|
+
for (let i = 0; i < 9; i++) { f.fillStyle = g(44 - (i % 2) * 6); f.fillRect(60 + i * 26, 382, 16, 70); }
|
|
464
|
+
const rh = f.createLinearGradient(0, 230, 0, 382); rh.addColorStop(0, ga(30, 0)); rh.addColorStop(1, ga(30, 0.5));
|
|
465
|
+
f.fillStyle = rh; f.fillRect(50, 230, 250, 152);
|
|
466
|
+
|
|
467
|
+
// ---- server racks (right) ----
|
|
468
|
+
const racks = [[880, 70, 170], [1062, 50, 190]];
|
|
469
|
+
racks.forEach(([x, y, w], ri) => {
|
|
470
|
+
f.fillStyle = g(27); f.fillRect(x, y, w, 560 - y);
|
|
471
|
+
f.fillStyle = g(25); f.fillRect(x, y, 10, 560 - y); f.fillRect(x + w - 10, y, 10, 560 - y);
|
|
472
|
+
for (let u = y + 20, k = 0; u < 540; u += 24, k++) {
|
|
473
|
+
const load = 0.5 + 0.5 * nz.n2(k * 0.7 + ri * 5, t * 0.6);
|
|
474
|
+
const spin = H.smooth(0.7 + (20 - k) * 0.07 + ri * 0.15, 1.3 + (20 - k) * 0.07 + ri * 0.15, t); // servers spin up bottom->top
|
|
475
|
+
const T = 27 + spin * (3 + load * 18 + (k % 5 === 2 ? 6 : 0));
|
|
476
|
+
f.fillStyle = g(T); f.fillRect(x + 16, u, w - 32, 18);
|
|
477
|
+
// vent perforation: hotter slots
|
|
478
|
+
f.fillStyle = g(T + 5);
|
|
479
|
+
for (let s = 0; s < 8; s++) f.fillRect(x + 26 + s * ((w - 50) / 8), u + 5, (w - 50) / 16, 8);
|
|
480
|
+
f.fillStyle = g(T - 6); f.fillRect(x + 16, u + 18, w - 32, 6);
|
|
481
|
+
}
|
|
482
|
+
// warm exhaust rising above rack
|
|
483
|
+
const ex = f.createLinearGradient(0, y - 60, 0, y);
|
|
484
|
+
ex.addColorStop(0, ga(30, 0)); ex.addColorStop(1, ga(36, 0.7));
|
|
485
|
+
f.fillStyle = ex; f.fillRect(x + 10, y - 60, w - 20, 60);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ---- person (behind laptop) ----
|
|
489
|
+
const hx = 520, hy = 200;
|
|
490
|
+
// body heat halo on wall
|
|
491
|
+
const halo = f.createRadialGradient(hx, hy + 140, 60, hx, hy + 140, 300);
|
|
492
|
+
halo.addColorStop(0, ga(27, 0.6)); halo.addColorStop(1, ga(24, 0));
|
|
493
|
+
f.fillStyle = halo; f.fillRect(hx - 320, hy - 160, 640, 520);
|
|
494
|
+
// shoulders / torso (shirt ~30)
|
|
495
|
+
f.fillStyle = g(28.2);
|
|
496
|
+
f.beginPath(); f.moveTo(hx - 210, 560); f.bezierCurveTo(hx - 215, 380, hx - 150, 320, hx - 60, 300);
|
|
497
|
+
f.lineTo(hx + 60, 300); f.bezierCurveTo(hx + 150, 320, hx + 215, 380, hx + 210, 560); f.closePath(); f.fill();
|
|
498
|
+
// shirt folds cooler/warmer
|
|
499
|
+
f.strokeStyle = ga(27, 0.7); f.lineWidth = 10; f.lineCap = 'round';
|
|
500
|
+
f.beginPath(); f.moveTo(hx - 140, 380); f.quadraticCurveTo(hx - 120, 450, hx - 150, 520); f.stroke();
|
|
501
|
+
f.beginPath(); f.moveTo(hx + 135, 370); f.quadraticCurveTo(hx + 120, 440, hx + 150, 520); f.stroke();
|
|
502
|
+
// arms reaching forward to keyboard
|
|
503
|
+
f.fillStyle = g(29);
|
|
504
|
+
f.beginPath(); f.ellipse(hx - 190, 470, 48, 110, 0.35, 0, 7); f.fill();
|
|
505
|
+
f.beginPath(); f.ellipse(hx + 190, 470, 48, 110, -0.35, 0, 7); f.fill();
|
|
506
|
+
// neck
|
|
507
|
+
const nk = f.createLinearGradient(hx - 40, 0, hx + 40, 0);
|
|
508
|
+
nk.addColorStop(0, g(32)); nk.addColorStop(0.5, g(35)); nk.addColorStop(1, g(32));
|
|
509
|
+
f.fillStyle = nk; f.fillRect(hx - 38, 250, 76, 64);
|
|
510
|
+
// collar V hot skin
|
|
511
|
+
f.fillStyle = g(34.5); f.beginPath(); f.moveTo(hx - 34, 300); f.lineTo(hx, 345); f.lineTo(hx + 34, 300); f.fill();
|
|
512
|
+
// head: hot face, limb-darkened edges
|
|
513
|
+
const hd = f.createRadialGradient(hx, hy + 10, 10, hx, hy, 82);
|
|
514
|
+
hd.addColorStop(0, g(36)); hd.addColorStop(0.65, g(35)); hd.addColorStop(1, g(31.5));
|
|
515
|
+
f.fillStyle = hd; f.beginPath(); f.ellipse(hx, hy, 70, 86, 0, 0, 7); f.fill();
|
|
516
|
+
// ears (cooler)
|
|
517
|
+
f.fillStyle = g(32); f.beginPath(); f.ellipse(hx - 70, hy + 6, 12, 22, 0, 0, 7); f.ellipse(hx + 70, hy + 6, 12, 22, 0, 0, 7); f.fill();
|
|
518
|
+
// hair (insulating -> cool)
|
|
519
|
+
f.fillStyle = g(27.5);
|
|
520
|
+
f.beginPath(); f.moveTo(hx - 74, hy + 10); f.bezierCurveTo(hx - 90, hy - 80, hx - 20, hy - 112, hx + 30, hy - 100);
|
|
521
|
+
f.bezierCurveTo(hx + 90, hy - 90, hx + 86, hy - 20, hx + 74, hy + 4);
|
|
522
|
+
f.bezierCurveTo(hx + 60, hy - 40, hx + 20, hy - 58, hx - 20, hy - 54);
|
|
523
|
+
f.bezierCurveTo(hx - 50, hy - 50, hx - 66, hy - 20, hx - 74, hy + 10); f.fill();
|
|
524
|
+
// glasses: glass blocks LWIR -> look cold
|
|
525
|
+
f.fillStyle = g(25.5);
|
|
526
|
+
f.beginPath(); f.roundRect(hx - 60, hy - 12, 50, 36, 10); f.roundRect(hx + 10, hy - 12, 50, 36, 10); f.fill();
|
|
527
|
+
f.strokeStyle = g(29); f.lineWidth = 4;
|
|
528
|
+
f.beginPath(); f.roundRect(hx - 60, hy - 12, 50, 36, 10); f.roundRect(hx + 10, hy - 12, 50, 36, 10); f.stroke();
|
|
529
|
+
f.beginPath(); f.moveTo(hx - 10, hy - 2); f.lineTo(hx + 10, hy - 2); f.stroke();
|
|
530
|
+
// nose tip slightly cool, inner eye corners hottest (seen as bright dots at bridge)
|
|
531
|
+
f.fillStyle = g(32.6); f.beginPath(); f.ellipse(hx, hy + 40, 11, 9, 0, 0, 7); f.fill();
|
|
532
|
+
f.fillStyle = g(36.4); f.beginPath(); f.arc(hx - 7, hy + 6, 5, 0, 7); f.arc(hx + 7, hy + 6, 5, 0, 7); f.fill();
|
|
533
|
+
// mouth warm line
|
|
534
|
+
f.fillStyle = g(36); f.beginPath(); f.ellipse(hx, hy + 62, 16, 4, 0, 0, 7); f.fill();
|
|
535
|
+
// warm breath plume
|
|
536
|
+
for (let i = 0; i < 6; i++) {
|
|
537
|
+
f.fillStyle = ga(31, 0.18 - i * 0.025);
|
|
538
|
+
f.beginPath(); f.ellipse(hx + 6 + i * 6, hy + 82 + i * 10 + Math.sin(t * 2 + i) * 3, 14 + i * 7, 8 + i * 3, 0.3, 0, 7); f.fill();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ---- desk ----
|
|
542
|
+
const dg = f.createLinearGradient(0, 520, 0, Hh);
|
|
543
|
+
dg.addColorStop(0, g(22.8)); dg.addColorStop(1, g(21.5));
|
|
544
|
+
f.fillStyle = dg; f.fillRect(0, 520, W, Hh - 520);
|
|
545
|
+
f.fillStyle = g(21); f.fillRect(0, 520, W, 5);
|
|
546
|
+
// warm patch under laptop (conducted heat)
|
|
547
|
+
const lp = f.createRadialGradient(540, 590, 20, 540, 590, 300);
|
|
548
|
+
lp.addColorStop(0, ga(31, 0.85)); lp.addColorStop(1, ga(24, 0));
|
|
549
|
+
f.fillStyle = lp; f.beginPath(); f.ellipse(540, 600, 320, 70, 0, 0, 7); f.fill();
|
|
550
|
+
// residual handprint on desk (thermal ghost)
|
|
551
|
+
const hpx = 230, hpy = 640;
|
|
552
|
+
f.fillStyle = ga(26.5, 0.75);
|
|
553
|
+
f.beginPath(); f.ellipse(hpx, hpy, 36, 22, -0.2, 0, 7); f.fill();
|
|
554
|
+
[[-34, -26, 0.5], [-12, -36, 0.25], [10, -36, 0], [30, -28, -0.25]].forEach(([dx, dy, a]) => {
|
|
555
|
+
f.beginPath(); f.ellipse(hpx + dx, hpy + dy, 7, 20, a, 0, 7); f.fill();
|
|
556
|
+
});
|
|
557
|
+
f.beginPath(); f.ellipse(hpx + 46, hpy + 2, 7, 18, 1.2, 0, 7); f.fill();
|
|
558
|
+
|
|
559
|
+
// ---- laptop: lid back toward camera ----
|
|
560
|
+
const lx0 = 340, lx1 = 720, ly0 = 318, ly1 = 560;
|
|
561
|
+
f.fillStyle = g(25.8);
|
|
562
|
+
f.beginPath(); f.moveTo(lx0 + 14, ly0); f.lineTo(lx1 - 14, ly0); f.lineTo(lx1, ly1); f.lineTo(lx0, ly1); f.closePath(); f.fill();
|
|
563
|
+
// warmer toward hinge/bottom (heat from the base)
|
|
564
|
+
const lg = f.createLinearGradient(0, ly0, 0, ly1);
|
|
565
|
+
lg.addColorStop(0, ga(26, 0)); lg.addColorStop(0.6, ga(28, 0.35)); lg.addColorStop(1, ga(36, 0.9));
|
|
566
|
+
f.fillStyle = lg; f.fill();
|
|
567
|
+
// lid logo-less emblem: cooler circle (different emissivity metal)
|
|
568
|
+
f.fillStyle = g(24.5); f.beginPath(); f.arc((lx0 + lx1) / 2, 420, 22, 0, 7); f.fill();
|
|
569
|
+
// base / hinge: hot exhaust slot
|
|
570
|
+
f.fillStyle = g(41); f.fillRect(lx0 - 6, ly1 - 4, lx1 - lx0 + 12, 14);
|
|
571
|
+
f.fillStyle = g(47); f.fillRect(lx0 + 40, ly1 + 2, 120, 6);
|
|
572
|
+
// exhaust plume
|
|
573
|
+
const pl = f.createLinearGradient(0, ly1 + 10, 0, ly1 + 80);
|
|
574
|
+
pl.addColorStop(0, ga(38, 0.7)); pl.addColorStop(1, ga(30, 0));
|
|
575
|
+
f.fillStyle = pl; f.beginPath(); f.ellipse(lx0 + 100, ly1 + 30, 90, 30, 0, 0, 7); f.fill();
|
|
576
|
+
// hands on keyboard visible either side of lid
|
|
577
|
+
f.fillStyle = g(32.5);
|
|
578
|
+
f.beginPath(); f.ellipse(lx0 - 18, 548, 34, 20, -0.3, 0, 7); f.fill();
|
|
579
|
+
f.beginPath(); f.ellipse(lx1 + 18, 548, 34, 20, 0.3, 0, 7); f.fill();
|
|
580
|
+
f.fillStyle = g(29.5); // fingertips cooler
|
|
581
|
+
for (let i = 0; i < 4; i++) { f.beginPath(); f.arc(lx0 + 6 + i * 6, 560 + i, 6, 0, 7); f.arc(lx1 - 6 - i * 6, 560 + i, 6, 0, 7); f.fill(); }
|
|
582
|
+
|
|
583
|
+
// ---- coffee mug ----
|
|
584
|
+
const mx = 820, my = 470, mw = 92, mh = 108;
|
|
585
|
+
const mg = f.createLinearGradient(mx - mw / 2, 0, mx + mw / 2, 0);
|
|
586
|
+
const mT = (T) => g(23 + (T - 23) * mugWarm);
|
|
587
|
+
mg.addColorStop(0, mT(44)); mg.addColorStop(0.35, mT(56)); mg.addColorStop(0.6, mT(57)); mg.addColorStop(1, mT(43));
|
|
588
|
+
f.fillStyle = mg; f.fillRect(mx - mw / 2, my, mw, mh);
|
|
589
|
+
f.beginPath(); f.ellipse(mx, my + mh, mw / 2, 12, 0, 0, Math.PI); f.fill();
|
|
590
|
+
// handle
|
|
591
|
+
f.strokeStyle = mT(46); f.lineWidth = 14;
|
|
592
|
+
f.beginPath(); f.ellipse(mx + mw / 2 + 4, my + 50, 26, 32, 0, -1.3, 1.3); f.stroke();
|
|
593
|
+
// liquid surface: hottest
|
|
594
|
+
f.fillStyle = mT(52); f.beginPath(); f.ellipse(mx, my, mw / 2, 13, 0, 0, 7); f.fill();
|
|
595
|
+
f.fillStyle = g(23 + 44 * coffee); f.beginPath(); f.ellipse(mx, my + 1, mw / 2 - 7, 9, 0, 0, 7); f.fill();
|
|
596
|
+
// mug heat reflected on desk
|
|
597
|
+
f.fillStyle = ga(30, 0.5 * mugWarm); f.beginPath(); f.ellipse(mx, my + mh + 14, 70, 14, 0, 0, 7); f.fill();
|
|
598
|
+
// steam plume: soft rising puffs, cooling and spreading with height
|
|
599
|
+
for (let k = 0; k < 110; k++) {
|
|
600
|
+
const strand = k % 2, u = (k >> 1) / 55, yy = my - 6 - u * 260 + (R() - 0.5) * 14;
|
|
601
|
+
const xx = mx + (strand ? 14 : -14) * (1 - u) + Math.sin(u * 5.2 + t * 1.3 + strand * 2.2) * (6 + u * 34)
|
|
602
|
+
+ nz.n2(u * 2.5 + strand * 4, t * 0.4) * 34 * u + (R() - 0.5) * 18 * u;
|
|
603
|
+
const r = 10 + u * 30 + R() * 8;
|
|
604
|
+
const dens = H.clamp(0.6 + nz.n2(u * 6 + strand * 3, t * 0.8) * 1.2, 0.1, 1) * steamOn * (1 - H.smooth((t - 1.1) * 0.9 - 0.15, (t - 1.1) * 0.9 + 0.05, u));
|
|
605
|
+
const gr = f.createRadialGradient(xx, yy, 0, xx, yy, r);
|
|
606
|
+
gr.addColorStop(0, ga(46 - u * 16, 0.2 * dens * (1 - u * 0.75))); gr.addColorStop(1, ga(30, 0));
|
|
607
|
+
f.fillStyle = gr; f.beginPath(); f.arc(xx, yy, r, 0, 7); f.fill();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ---- optics: blur the field (lens + low-res microbolometer) ----
|
|
611
|
+
const sharpImg = f.getImageData(0, 0, FW, FH), sharp = sharpImg.data;
|
|
612
|
+
// organic temperature mottling (convection, emissivity variation)
|
|
613
|
+
for (let y = 0; y < FH; y++) for (let x = 0; x < FW; x++) {
|
|
614
|
+
const i = (y * FW + x) * 4, m = nz.fbm2(x * 0.02, y * 0.02 + t * 0.05, 3) * 3.2;
|
|
615
|
+
sharp[i] = sharp[i + 1] = sharp[i + 2] = H.clamp(sharp[i] + m, 0, 255);
|
|
616
|
+
}
|
|
617
|
+
f.setTransform(1, 0, 0, 1, 0, 0); f.putImageData(sharpImg, 0, 0);
|
|
618
|
+
const B = H.layer(FW, FH), b = B.getContext('2d');
|
|
619
|
+
b.filter = 'blur(3.2px)'; b.drawImage(F, 0, 0);
|
|
620
|
+
const soft = b.getImageData(0, 0, FW, FH).data;
|
|
621
|
+
// MSX-style edge map from sharp field (Sobel)
|
|
622
|
+
const edge = new Float32Array(FW * FH);
|
|
623
|
+
for (let y = 1; y < FH - 1; y++) for (let x = 1; x < FW - 1; x++) {
|
|
624
|
+
const p = (yy, xx) => sharp[((y + yy) * FW + x + xx) * 4];
|
|
625
|
+
const gx = -p(-1, -1) - 2 * p(0, -1) - p(1, -1) + p(-1, 1) + 2 * p(0, 1) + p(1, 1);
|
|
626
|
+
const gy = -p(-1, -1) - 2 * p(-1, 0) - p(-1, 1) + p(1, -1) + 2 * p(1, 0) + p(1, 1);
|
|
627
|
+
edge[y * FW + x] = Math.min(1, Math.hypot(gx, gy) / 90);
|
|
628
|
+
}
|
|
629
|
+
// auto-range from scene (with slight pumping over time)
|
|
630
|
+
let lo = 255, hi = 0;
|
|
631
|
+
for (let i = 0; i < soft.length; i += 4) { const v = soft[i]; if (v < lo) lo = v; if (v > hi) hi = v; }
|
|
632
|
+
const tLo = TMIN + lo / 255 * (TMAX - TMIN), tHi = TMIN + hi / 255 * (TMAX - TMIN) + Math.sin(t * 0.7) * 0.3;
|
|
633
|
+
|
|
634
|
+
// plateau histogram equalisation (what real IR cameras do so faces don't go dull next to a hot mug)
|
|
635
|
+
const hist = new Float32Array(256);
|
|
636
|
+
for (let i = 0; i < soft.length; i += 4) hist[soft[i]]++;
|
|
637
|
+
const plateau = (FW * FH) / 70;
|
|
638
|
+
let tot = 0; for (let i = 0; i < 256; i++) { hist[i] = Math.min(hist[i], plateau); tot += hist[i]; }
|
|
639
|
+
const cdf = new Float32Array(256); let acc = 0;
|
|
640
|
+
for (let i = 0; i < 256; i++) { acc += hist[i]; cdf[i] = acc / tot; }
|
|
641
|
+
const eq = (v255) => { const i = Math.min(254, v255 | 0), f2 = v255 - i; return cdf[i] * (1 - f2) + cdf[i + 1] * f2; };
|
|
642
|
+
// ---- palette mapping at output resolution ----
|
|
643
|
+
const lut = buildLUT();
|
|
644
|
+
const img = ctx.createImageData(W, Hh), d = img.data;
|
|
645
|
+
const colFPN = new Float32Array(W); for (let x = 0; x < W; x++) colFPN[x] = (R() - 0.5) * 0.012 + Math.sin(x * 0.9) * 0.002;
|
|
646
|
+
const sx = FW / W, sy = FH / Hh;
|
|
647
|
+
for (let y = 0; y < Hh; y++) {
|
|
648
|
+
const fy = H.clamp(y * sy - 0.5, 0, FH - 1.001), iy = fy | 0, ay = fy - iy;
|
|
649
|
+
const rowN = (Rn() - 0.5) * 0.006;
|
|
650
|
+
for (let x = 0; x < W; x++) {
|
|
651
|
+
const fx = H.clamp(x * sx - 0.5, 0, FW - 1.001), ix = fx | 0, ax = fx - ix, j = iy * FW + ix;
|
|
652
|
+
const v = ((soft[j * 4] * (1 - ax) + soft[j * 4 + 4] * ax) * (1 - ay) + (soft[(j + FW) * 4] * (1 - ax) + soft[(j + FW) * 4 + 4] * ax) * ay) / 255;
|
|
653
|
+
const e = (edge[j] * (1 - ax) + edge[j + 1] * ax) * (1 - ay) + (edge[j + FW] * (1 - ax) + edge[j + FW + 1] * ax) * ay;
|
|
654
|
+
const T = TMIN + v * (TMAX - TMIN);
|
|
655
|
+
let n = 0.55 * eq(v * 255) + 0.45 * (T - tLo) / (tHi - tLo);
|
|
656
|
+
n = 0.42 + (n - 0.42) * (0.25 + 0.75 * settle); // AGC settling after calibration
|
|
657
|
+
n += (Rn() + Rn() - 1) * 0.022 + colFPN[x] + rowN;
|
|
658
|
+
n += e * 0.035;
|
|
659
|
+
if (t < 0.42) n = 0.36 + (x / W - 0.5) * 0.02 + (Rn() - 0.5) * 0.03; // shutter closed (NUC): flat field
|
|
660
|
+
const li = H.clamp(n, 0, 1) * 1023 | 0;
|
|
661
|
+
const i = (y * W + x) * 4;
|
|
662
|
+
d[i] = lut[li * 3]; d[i + 1] = lut[li * 3 + 1]; d[i + 2] = lut[li * 3 + 2]; d[i + 3] = 255;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
ctx.putImageData(img, 0, 0);
|
|
666
|
+
// lens vignetting (microbolometer narcissus/ fall-off)
|
|
667
|
+
H.vignette(ctx, W, Hh, 0.35, '0,0,10');
|
|
668
|
+
|
|
669
|
+
// ---- HUD ----
|
|
670
|
+
const tempAt = (x, y) => { const j = ((y * sy) | 0) * FW + ((x * sx) | 0); return TMIN + soft[j * 4] / 255 * (TMAX - TMIN); };
|
|
671
|
+
ctx.save();
|
|
672
|
+
ctx.textBaseline = 'middle';
|
|
673
|
+
const hudText = (s, x, y, font, align = 'left', col = '#fff') => {
|
|
674
|
+
ctx.font = font; ctx.textAlign = align;
|
|
675
|
+
ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillText(s, x + 1.5, y + 1.5);
|
|
676
|
+
ctx.fillStyle = col; ctx.fillText(s, x, y);
|
|
677
|
+
};
|
|
678
|
+
const line = (pts, w = 2) => {
|
|
679
|
+
ctx.lineCap = 'butt';
|
|
680
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.55)'; ctx.lineWidth = w + 2; ctx.beginPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.stroke();
|
|
681
|
+
ctx.strokeStyle = '#fff'; ctx.lineWidth = w; ctx.stroke();
|
|
682
|
+
};
|
|
683
|
+
// spot meter crosshair on the mug
|
|
684
|
+
const hudOn = t >= 0.45;
|
|
685
|
+
const glide = H.ease.inOut(H.smooth(1.9, 2.7, t));
|
|
686
|
+
const cx = Math.round(H.lerp(640, mx + 4, glide)), cy = Math.round(H.lerp(360, my + 4, glide));
|
|
687
|
+
const spotT = tempAt(cx, cy) + Math.sin(t * 3.1) * 0.1;
|
|
688
|
+
if (!hudOn) { ctx.font = '600 22px "Inter"'; ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.textAlign = 'center'; ctx.fillText('CALIBRATING\u2026', W / 2, Hh / 2); ctx.restore(); return; }
|
|
689
|
+
line([[cx - 30, cy], [cx - 9, cy]]); line([[cx + 9, cy], [cx + 30, cy]]);
|
|
690
|
+
line([[cx, cy - 30], [cx, cy - 9]]); line([[cx, cy + 9], [cx, cy + 30]]);
|
|
691
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(cx, cy, 2, 0, 7); ctx.stroke();
|
|
692
|
+
hudText(spotT.toFixed(1) + '\u00b0', cx + 36, cy - 22, '600 20px "Inter"');
|
|
693
|
+
// measurement box on GPU rack
|
|
694
|
+
const bx0 = 1074, by0 = 160, bx1 = 1200, by1 = 360;
|
|
695
|
+
const boxP = H.ease.out(H.smooth(2.85, 3.15, t));
|
|
696
|
+
const bl = (a, b) => H.lerp(a, b, boxP), mxb = bx0 + 63, myb = by0 + 100;
|
|
697
|
+
ctx.setLineDash([]);
|
|
698
|
+
if (boxP > 0) line([[bl(mxb, bx0), bl(myb, by0)], [bl(mxb, bx1), bl(myb, by0)], [bl(mxb, bx1), bl(myb, by1)], [bl(mxb, bx0), bl(myb, by1)], [bl(mxb, bx0), bl(myb, by0) - 1]], 2);
|
|
699
|
+
let bmax = -99, bmx = 0, bmy = 0;
|
|
700
|
+
for (let y = by0; y < by1; y += 4) for (let x = bx0; x < bx1; x += 4) { const T = tempAt(x, y); if (T > bmax) { bmax = T; bmx = x; bmy = y; } }
|
|
701
|
+
if (boxP >= 1) hudText('Bx1', bx0 + 4, by0 - 14, '600 16px "Inter"');
|
|
702
|
+
// hot/cold markers
|
|
703
|
+
const tri = (x, y, col, up) => {
|
|
704
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.moveTo(x - 9, y + (up ? 8 : -8) + 1); ctx.lineTo(x + 10, y + (up ? 8 : -8) + 1); ctx.lineTo(x + 1, y + (up ? -8 : 8) + 1); ctx.fill();
|
|
705
|
+
ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x - 9, y + (up ? 8 : -8)); ctx.lineTo(x + 9, y + (up ? 8 : -8)); ctx.lineTo(x, y + (up ? -8 : 8)); ctx.fill();
|
|
706
|
+
};
|
|
707
|
+
if (boxP >= 1) tri(bmx, bmy, '#ff2a1a', true);
|
|
708
|
+
tri(110, 300, '#28a8ff', false);
|
|
709
|
+
|
|
710
|
+
// top-left measurement table
|
|
711
|
+
ctx.fillStyle = 'rgba(0,0,0,0.38)'; ctx.fillRect(18, 18, 238, boxP >= 1 ? 84 : 46);
|
|
712
|
+
hudText('Sp1', 30, 40, '600 22px "Inter"'); hudText(spotT.toFixed(1) + ' °C', 244, 40, '600 22px "Inter"', 'right');
|
|
713
|
+
if (boxP >= 1) { hudText('Bx1 Max', 30, 78, '600 22px "Inter"'); hudText(bmax.toFixed(1) + ' °C', 244, 78, '600 22px "Inter"', 'right'); }
|
|
714
|
+
// top-right status: emissivity + battery
|
|
715
|
+
hudText('ε 0.95 IRONBOW', 1090, 34, '400 16px "Inter"', 'right');
|
|
716
|
+
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(1112, 25, 34, 17); ctx.fillStyle = '#fff'; ctx.fillRect(1146, 30, 3, 7);
|
|
717
|
+
ctx.fillRect(1115, 28, 21, 11);
|
|
718
|
+
hudText('AUTO', 1172, 34, '600 16px "Inter"');
|
|
719
|
+
|
|
720
|
+
// colour scale bar (right edge)
|
|
721
|
+
const sbx = 1218, sbt = 112, sbb = 600;
|
|
722
|
+
const sg = ctx.createLinearGradient(0, sbb, 0, sbt);
|
|
723
|
+
IRONBOW.forEach(([p, c]) => sg.addColorStop(p, H.rgba(c, 1)));
|
|
724
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(sbx - 2, sbt - 2, 26, sbb - sbt + 4);
|
|
725
|
+
ctx.fillStyle = sg; ctx.fillRect(sbx, sbt, 22, sbb - sbt);
|
|
726
|
+
hudText(tHi.toFixed(1), sbx + 22, sbt - 20, '600 20px "Inter"', 'right');
|
|
727
|
+
hudText(tLo.toFixed(1), sbx + 22, sbb + 22, '600 20px "Inter"', 'right');
|
|
728
|
+
hudText('°C', sbx + 11, sbb + 48, '400 16px "Inter"', 'center');
|
|
729
|
+
|
|
730
|
+
// annotation tag (on-screen label)
|
|
731
|
+
ctx.fillStyle = 'rgba(0,0,0,0.42)'; ctx.fillRect(18, t >= 3.1 ? 640 : 670, 330, t >= 3.1 ? 62 : 32);
|
|
732
|
+
if (t >= 3.1) hudText('GPU LOAD: ' + Math.round(98 * H.ease.out(H.smooth(3.1, 3.8, t))) + '%', 32, 662, '600 22px "Inter"');
|
|
733
|
+
const hh = 23 + Math.floor(t / 60), ss = (41 * 60 + 7 + Math.floor(t)) % 3600;
|
|
734
|
+
hudText(`2026-10-09 ${hh}:${String(Math.floor(ss / 60)).padStart(2, '0')}:${String(ss % 60).padStart(2, '0')}`, 32, 688, '600 16px "IBM Plex Mono"', 'left', 'rgba(255,255,255,0.85)');
|
|
735
|
+
ctx.restore();
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
}
|