feat: reusable registry galleries and individual HTML examples [verified]

This commit is contained in:
jarvis-at-skic committed 2026-10-25 09:19:23 +00:00
1 parent 55e559dcd1
commit 3bb9555e6a
67 files changed
+1229 -77

No files matched your search

+9 -1
View File
@@ -42,10 +42,18 @@ For architecture decomposition, modular products and inspect/reassemble interact
- [Composition registry](references/registry-animejs-compositions.md): dependency flow and assemble/explode/reassemble.
- `assets/animejs/examples.js`: editable configurations; `gallery.js`: shared renderer and lifecycle; `index.html`: local source preview.
Build with `python3 scripts/build-gallery.py` and open `dist/animejs-gallery-0.4.0.html`. This portable offline gallery includes Play, Replay, Settle and Seek for all five examples. `npm test` runs the deck, motion, mechanism and gallery suites. Registry coverage and source links are validated so new examples cannot silently go unlisted. Existing deck and mechanism artifact versions remain available.
Build with `python3 scripts/build-gallery.py` and open `dist/animejs-gallery-0.5.0/index.html`. This portable offline gallery includes Play, Replay, Settle and Seek for all five examples. `npm test` runs the deck, motion, mechanism and gallery suites. Registry coverage and source links are validated so new examples cannot silently go unlisted. Existing deck and mechanism artifact versions remain available.
## Licenses and scope
Original code/documentation: MIT, RootAtSkic and Hermes Agent. Reveal.js and Anime.js: MIT; Work Sans: SIL OFL 1.1. Notices are preserved alongside vendor bytes and embedded in the output. No copyrighted showcase artwork was scraped into the distributable. Research documents disclose unavailable pages and link-only references. See `references/examples.md` for attribution and reuse decisions.
Local Chromium verification is included; no hosted CI or cross-browser certification is claimed. No automation credentials belong in this repository.
### Gallery hierarchy (0.5.0)
Open `dist/animejs-gallery-0.5.0/index.html` for all five previews, or `elements/index.html` and `compositions/index.html` inside that package. Each gallery embeds `examples/<id>.html` through relative iframe `src` and links to that same file. Each individual page is self-contained (CSS, configuration, renderer, pinned Anime.js and full license embedded), so it also plays offline by itself. Breadcrumb links require keeping the package hierarchy together. Extract the complete package before opening its index; no server is needed. Previous version artifacts are retained.
The editable hierarchy mirrors this under `assets/animejs/`, sharing `examples.js`, `gallery.js`, `gallery.css` and `assets/vendor/`. Edit the configuration/renderer once, not the generated dist pages. Individual source documents select exactly one configuration using `body[data-example]`; unknown IDs fail explicitly, with no query-string fallback. Galleries never mount a renderer themselves. Every embedded document owns its lifecycle.
`python3 scripts/build-gallery.py` inlines local dependencies and records every page hash plus source hashes in `dist/animejs-gallery-manifest.json`. `scripts/test-gallery.mjs` tests both editable and built hierarchies, iframe identity, navigation, all five examples in normal/reduced motion and deterministic hold/cleanup behavior. Screenshots and exact page hashes are recorded under `evidence/`.
+2 -2
View File
@@ -1,7 +1,7 @@
---
name: presentation-html
description: Build offline HTML decks with deliberate motion.
version: 0.4.0
version: 0.5.0
author: RootAtSkic, Hermes Agent
license: MIT
platforms: [linux, macos, windows]
@@ -37,7 +37,7 @@ Select examples by the relationship they explain, not just their visual effect:
- [Element registry](references/registry-animejs-elements.md): SVG connection drawing, motion-path travel and spatial stagger. Read when choosing a single technique.
- [Composition registry](references/registry-animejs-compositions.md): dependency flow and assemble/explode/reassemble. Read when explaining a multi-part mechanism.
Editable configurations, renderer and preview live in `assets/animejs/`. Build the single-file offline gallery with `python3 scripts/build-gallery.py` through `terminal`, then open `dist/animejs-gallery-0.4.0.html`. `npm test` exercises every registered example. Keep the registry ID, source, parameter constraints and verification coverage aligned when adding an example; do not load all examples into context unnecessarily. Gallery controls are for inspecting recipes, not mandatory controls for client slides. Wire slide departure/teardown explicitly when reusing a recipe.
Editable configurations, renderer and preview live in `assets/animejs/`. Build the reusable offline gallery package with `python3 scripts/build-gallery.py` through `terminal`, then open `dist/animejs-gallery-0.5.0/index.html`. `npm test` exercises every registered example. Keep the registry ID, source, parameter constraints and verification coverage aligned when adding an example; do not load all examples into context unnecessarily. Gallery controls are for inspecting recipes, not mandatory controls for client slides. Wire slide departure/teardown explicitly when reusing a recipe.
## Read on demand: choreography and deterministic motion
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Compositions — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-gallery="compositions"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <span aria-current="page">Compositions</span></nav><h1>Compositions</h1><p>The same individual examples, collected by registry.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Elements — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-gallery="elements"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <span aria-current="page">Elements</span></nav><h1>Elements</h1><p>The same individual examples, collected by registry.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Separate, inspect, reassemble — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-example="assemble-explode"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../compositions/index.html">Compositions</a> / <span aria-current="page">Separate, inspect, reassemble</span></nav><h1>Separate, inspect, reassemble</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../../vendor/anime.umd.min.js"></script><script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Explain a dependency — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-example="dependency-flow"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../compositions/index.html">Compositions</a> / <span aria-current="page">Explain a dependency</span></nav><h1>Explain a dependency</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../../vendor/anime.umd.min.js"></script><script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Follow a route — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-example="motion-path"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Follow a route</span></nav><h1>Follow a route</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../../vendor/anime.umd.min.js"></script><script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Propagate from the center — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-example="spatial-stagger"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Propagate from the center</span></nav><h1>Propagate from the center</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../../vendor/anime.umd.min.js"></script><script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Draw a connection — Motion that explains</title><link rel="stylesheet" href="../gallery.css"></head>
<body data-example="svg-draw"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Draw a connection</span></nav><h1>Draw a connection</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="../../vendor/anime.umd.min.js"></script><script src="../examples.js"></script><script src="../gallery.js"></script></body></html>
+3 -1
View File
@@ -1 +1,3 @@
*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
+25 -3
View File
@@ -55,7 +55,29 @@
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const controllers={};
for(const config of window.animeExamples){const host=document.createElement('article');host.id=config.id;document.querySelector('main').append(host);controllers[config.id]=mount(host,config);}
window.animeGallery={controllers,mount};
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
+12 -3
View File
@@ -1,4 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Anime.js — bounded examples</title><link rel="stylesheet" href="gallery.css"></head><body>
<header><h1>Motion that explains</h1><p>Five editable, finite examples · Anime.js 4.5.0 · Offline gallery 0.4.0</p><p>Scenes start complete. Play or replay to inspect their sequence; seek pauses the scene. Reduced motion keeps every scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples. System fonts; no remote resources. Each scene owns its animation and cleanup.</p><details><summary>Anime.js license</summary><pre>ANIME_LICENSE</pre></details></footer>
<script src="../vendor/anime.umd.min.js"></script><script src="examples.js"></script><script src="gallery.js"></script></body></html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Motion that explains — Anime.js gallery</title><link rel="stylesheet" href="gallery.css"></head>
<body data-gallery="all"><header><nav aria-label="Breadcrumb"><span aria-current="page">All examples</span></nav><h1>Motion that explains</h1><p>Five editable, finite examples. Explore a registry or open an individual example.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script src="examples.js"></script><script src="gallery.js"></script></body></html>
+14
View File
@@ -0,0 +1,14 @@
{
"file": "animejs-gallery-0.4.0.html",
"sha256": "eda1a94fc4a55642a90a42e70d7b5cbea3b2f2b73b818a0b941b61103a154d75",
"anime": "4.5.0",
"examples": 5,
"inputs": {
"assets/animejs/examples.js": "37d68cac1e1f38b1974811f9ba3527cf4a92b1c98509e8353e39f7e88b94512e",
"assets/animejs/gallery.css": "7bbf68e24e5d001b636b84faf76d682f1596618d7f6798866e19851d728ed04c",
"assets/animejs/gallery.js": "1e9cd1ee35e7584f2082eaddf2cc8def0e480048f9a85cb7578aa182682c0d88",
"assets/animejs/index.html": "d6d49f9d116d0a978ff0600dd78ed7ffe076fe5fbac364a73fc14243dd7907bc",
"assets/vendor/anime.umd.min.js": "8d5b3a58a1f64023a04a4cedeef135a2263b18da04b35177ac13438a0bad033b",
"assets/vendor/anime-LICENSE.md": "3f3e835a9952cfc2a6ca836fb95b3257c981f24fa3ef51249055acb09931897b"
}
}
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Compositions — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-gallery="compositions"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <span aria-current="page">Compositions</span></nav><h1>Compositions</h1><p>The same individual examples, collected by registry.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Elements — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-gallery="elements"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <span aria-current="page">Elements</span></nav><h1>Elements</h1><p>The same individual examples, collected by registry.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Separate, inspect, reassemble — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-example="assemble-explode"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../compositions/index.html">Compositions</a> / <span aria-current="page">Separate, inspect, reassemble</span></nav><h1>Separate, inspect, reassemble</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/**
* Anime.js - UMD minified bundle
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=[],f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},b=/\)\s*[-.\d]/,v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,T=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,x=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,C=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,E=/([a-z])([A-Z])/g,k=/(\*=|\+=|-=)/,N=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,D={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},A={current:null,root:i},I={defaults:D,precision:4,timeScale:1,tickThreshold:200,editor:null},R={version:"4.5.0",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(R));const L=t=>t.replace(E,"$1-$2").toLowerCase(),B=(t,e)=>0===t.indexOf(e),P=Date.now,F=Array.isArray,M=t=>t&&t.constructor===Object,V=t=>"number"==typeof t&&!isNaN(t),O=t=>"string"==typeof t,z=t=>"function"==typeof t,H=t=>void 0===t,X=t=>H(t)||null===t,Y=t=>e&&t instanceof SVGElement,W=t=>v.test(t),U=t=>B(t,"rgb"),q=t=>B(t,"hsl"),j=t=>!I.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Z=t=>O(t)?parseFloat(t):t,Q=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,lt=Math.PI,ht=Math.round,dt=(t,e,s)=>t<e?e:t>s?s:t,ct=(t,e)=>{if(e<0)return t;if(!e)return ht(t);const s=10**e;return ht(t*s)/s},ut=(t,e)=>F(e)?e.reduce((e,s)=>et(s-t)<et(e-t)?s:e):e?ht(t/e)*e:t,pt=(t,e,s)=>1===s?e:0===s?t:t+(e-t)*s,mt=t=>t===1/0?c:t===-1/0?-c:t,ft=t=>t<=d?d:mt(ct(t,11)),gt=t=>F(t)?[...t]:t,yt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},_t=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},Tt=t=>{let e=p;for(let s=0,i=g.length;s<i;s++){const i=g[s],r=t[i];if(void 0!==r){if("translateX"===i){const i=t.translateY;if(void 0!==i){const n=t.translateZ;void 0!==n?(e+=`translate3d(${r},${i},${n}) `,s+=2):(e+=`translate(${r},${i}) `,s+=1);continue}}if("scaleX"===i&&void 0===t.scale){const i=t.scaleY;if(void 0!==i){const n=t.scaleZ;void 0!==n?(e+=`scale3d(${r},${i},${n}) `,s+=2):(e+=`scale(${r},${i}) `,s+=1);continue}}e+=`${y[i]}${r}) `}"rotateZ"===i&&void 0!==t.rotate3d&&(e+=`rotate3d(${t.rotate3d}) `)}return void 0!==t.matrix&&(e+=`matrix(${t.matrix}) `),void 0!==t.matrix3d&&(e+=`matrix3d(${t.matrix3d}) `),e},xt=[];function St(t,e){if(!t)return null;const s=xt.length;t:for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.targetAdapters;for(let s=0,i=r.length;s<i;s++){const i=r[s];if(i.detect(t)){const s=i.props[e];if(s&&(!s.gate||s.gate(t)))return s;break t}}}for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.propertyResolvers;for(let s=0,i=r.length;s<i;s++){const i=r[s](t,e);if(i)return i}}return null}const wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),$t=(t,e)=>H(t)?e:t,Ct=(t,e)=>{const s=t.match(N),i=e[o]?e:document.documentElement;let r=getComputedStyle(i)?.getPropertyValue(s[1]);return r&&r.trim()!==p||!s[2]||(r=s[2].trim()),r||0},Et=(t,e,s,i,r,n)=>{if(z(t)){if(!r){const r=t(e,s,i,n);return isNaN(+r)?r||0:+r}const o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};return r.func=o,o()}if(O(t)&&B(t,"var(")){if(!r)return Ct(t,e);const s=()=>Ct(t,e);return r.func=s,s()}return t},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:B(e,"--")?4:e in t.style?2:e in t?0:1:0,Nt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[Line truncated
</script><script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Explain a dependency — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-example="dependency-flow"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../compositions/index.html">Compositions</a> / <span aria-current="page">Explain a dependency</span></nav><h1>Explain a dependency</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/**
* Anime.js - UMD minified bundle
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=[],f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},b=/\)\s*[-.\d]/,v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,T=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,x=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,C=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,E=/([a-z])([A-Z])/g,k=/(\*=|\+=|-=)/,N=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,D={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},A={current:null,root:i},I={defaults:D,precision:4,timeScale:1,tickThreshold:200,editor:null},R={version:"4.5.0",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(R));const L=t=>t.replace(E,"$1-$2").toLowerCase(),B=(t,e)=>0===t.indexOf(e),P=Date.now,F=Array.isArray,M=t=>t&&t.constructor===Object,V=t=>"number"==typeof t&&!isNaN(t),O=t=>"string"==typeof t,z=t=>"function"==typeof t,H=t=>void 0===t,X=t=>H(t)||null===t,Y=t=>e&&t instanceof SVGElement,W=t=>v.test(t),U=t=>B(t,"rgb"),q=t=>B(t,"hsl"),j=t=>!I.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Z=t=>O(t)?parseFloat(t):t,Q=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,lt=Math.PI,ht=Math.round,dt=(t,e,s)=>t<e?e:t>s?s:t,ct=(t,e)=>{if(e<0)return t;if(!e)return ht(t);const s=10**e;return ht(t*s)/s},ut=(t,e)=>F(e)?e.reduce((e,s)=>et(s-t)<et(e-t)?s:e):e?ht(t/e)*e:t,pt=(t,e,s)=>1===s?e:0===s?t:t+(e-t)*s,mt=t=>t===1/0?c:t===-1/0?-c:t,ft=t=>t<=d?d:mt(ct(t,11)),gt=t=>F(t)?[...t]:t,yt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},_t=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},Tt=t=>{let e=p;for(let s=0,i=g.length;s<i;s++){const i=g[s],r=t[i];if(void 0!==r){if("translateX"===i){const i=t.translateY;if(void 0!==i){const n=t.translateZ;void 0!==n?(e+=`translate3d(${r},${i},${n}) `,s+=2):(e+=`translate(${r},${i}) `,s+=1);continue}}if("scaleX"===i&&void 0===t.scale){const i=t.scaleY;if(void 0!==i){const n=t.scaleZ;void 0!==n?(e+=`scale3d(${r},${i},${n}) `,s+=2):(e+=`scale(${r},${i}) `,s+=1);continue}}e+=`${y[i]}${r}) `}"rotateZ"===i&&void 0!==t.rotate3d&&(e+=`rotate3d(${t.rotate3d}) `)}return void 0!==t.matrix&&(e+=`matrix(${t.matrix}) `),void 0!==t.matrix3d&&(e+=`matrix3d(${t.matrix3d}) `),e},xt=[];function St(t,e){if(!t)return null;const s=xt.length;t:for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.targetAdapters;for(let s=0,i=r.length;s<i;s++){const i=r[s];if(i.detect(t)){const s=i.props[e];if(s&&(!s.gate||s.gate(t)))return s;break t}}}for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.propertyResolvers;for(let s=0,i=r.length;s<i;s++){const i=r[s](t,e);if(i)return i}}return null}const wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),$t=(t,e)=>H(t)?e:t,Ct=(t,e)=>{const s=t.match(N),i=e[o]?e:document.documentElement;let r=getComputedStyle(i)?.getPropertyValue(s[1]);return r&&r.trim()!==p||!s[2]||(r=s[2].trim()),r||0},Et=(t,e,s,i,r,n)=>{if(z(t)){if(!r){const r=t(e,s,i,n);return isNaN(+r)?r||0:+r}const o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};return r.func=o,o()}if(O(t)&&B(t,"var(")){if(!r)return Ct(t,e);const s=()=>Ct(t,e);return r.func=s,s()}return t},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:B(e,"--")?4:e in t.style?2:e in t?0:1:0,Nt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[Line truncated
</script><script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Follow a route — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-example="motion-path"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Follow a route</span></nav><h1>Follow a route</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/**
* Anime.js - UMD minified bundle
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=[],f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},b=/\)\s*[-.\d]/,v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,T=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,x=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,C=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,E=/([a-z])([A-Z])/g,k=/(\*=|\+=|-=)/,N=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,D={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},A={current:null,root:i},I={defaults:D,precision:4,timeScale:1,tickThreshold:200,editor:null},R={version:"4.5.0",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(R));const L=t=>t.replace(E,"$1-$2").toLowerCase(),B=(t,e)=>0===t.indexOf(e),P=Date.now,F=Array.isArray,M=t=>t&&t.constructor===Object,V=t=>"number"==typeof t&&!isNaN(t),O=t=>"string"==typeof t,z=t=>"function"==typeof t,H=t=>void 0===t,X=t=>H(t)||null===t,Y=t=>e&&t instanceof SVGElement,W=t=>v.test(t),U=t=>B(t,"rgb"),q=t=>B(t,"hsl"),j=t=>!I.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Z=t=>O(t)?parseFloat(t):t,Q=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,lt=Math.PI,ht=Math.round,dt=(t,e,s)=>t<e?e:t>s?s:t,ct=(t,e)=>{if(e<0)return t;if(!e)return ht(t);const s=10**e;return ht(t*s)/s},ut=(t,e)=>F(e)?e.reduce((e,s)=>et(s-t)<et(e-t)?s:e):e?ht(t/e)*e:t,pt=(t,e,s)=>1===s?e:0===s?t:t+(e-t)*s,mt=t=>t===1/0?c:t===-1/0?-c:t,ft=t=>t<=d?d:mt(ct(t,11)),gt=t=>F(t)?[...t]:t,yt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},_t=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},Tt=t=>{let e=p;for(let s=0,i=g.length;s<i;s++){const i=g[s],r=t[i];if(void 0!==r){if("translateX"===i){const i=t.translateY;if(void 0!==i){const n=t.translateZ;void 0!==n?(e+=`translate3d(${r},${i},${n}) `,s+=2):(e+=`translate(${r},${i}) `,s+=1);continue}}if("scaleX"===i&&void 0===t.scale){const i=t.scaleY;if(void 0!==i){const n=t.scaleZ;void 0!==n?(e+=`scale3d(${r},${i},${n}) `,s+=2):(e+=`scale(${r},${i}) `,s+=1);continue}}e+=`${y[i]}${r}) `}"rotateZ"===i&&void 0!==t.rotate3d&&(e+=`rotate3d(${t.rotate3d}) `)}return void 0!==t.matrix&&(e+=`matrix(${t.matrix}) `),void 0!==t.matrix3d&&(e+=`matrix3d(${t.matrix3d}) `),e},xt=[];function St(t,e){if(!t)return null;const s=xt.length;t:for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.targetAdapters;for(let s=0,i=r.length;s<i;s++){const i=r[s];if(i.detect(t)){const s=i.props[e];if(s&&(!s.gate||s.gate(t)))return s;break t}}}for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.propertyResolvers;for(let s=0,i=r.length;s<i;s++){const i=r[s](t,e);if(i)return i}}return null}const wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),$t=(t,e)=>H(t)?e:t,Ct=(t,e)=>{const s=t.match(N),i=e[o]?e:document.documentElement;let r=getComputedStyle(i)?.getPropertyValue(s[1]);return r&&r.trim()!==p||!s[2]||(r=s[2].trim()),r||0},Et=(t,e,s,i,r,n)=>{if(z(t)){if(!r){const r=t(e,s,i,n);return isNaN(+r)?r||0:+r}const o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};return r.func=o,o()}if(O(t)&&B(t,"var(")){if(!r)return Ct(t,e);const s=()=>Ct(t,e);return r.func=s,s()}return t},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:B(e,"--")?4:e in t.style?2:e in t?0:1:0,Nt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[Line truncated
</script><script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Propagate from the center — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-example="spatial-stagger"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Propagate from the center</span></nav><h1>Propagate from the center</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/**
* Anime.js - UMD minified bundle
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=[],f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},b=/\)\s*[-.\d]/,v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,T=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,x=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,C=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,E=/([a-z])([A-Z])/g,k=/(\*=|\+=|-=)/,N=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,D={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},A={current:null,root:i},I={defaults:D,precision:4,timeScale:1,tickThreshold:200,editor:null},R={version:"4.5.0",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(R));const L=t=>t.replace(E,"$1-$2").toLowerCase(),B=(t,e)=>0===t.indexOf(e),P=Date.now,F=Array.isArray,M=t=>t&&t.constructor===Object,V=t=>"number"==typeof t&&!isNaN(t),O=t=>"string"==typeof t,z=t=>"function"==typeof t,H=t=>void 0===t,X=t=>H(t)||null===t,Y=t=>e&&t instanceof SVGElement,W=t=>v.test(t),U=t=>B(t,"rgb"),q=t=>B(t,"hsl"),j=t=>!I.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Z=t=>O(t)?parseFloat(t):t,Q=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,lt=Math.PI,ht=Math.round,dt=(t,e,s)=>t<e?e:t>s?s:t,ct=(t,e)=>{if(e<0)return t;if(!e)return ht(t);const s=10**e;return ht(t*s)/s},ut=(t,e)=>F(e)?e.reduce((e,s)=>et(s-t)<et(e-t)?s:e):e?ht(t/e)*e:t,pt=(t,e,s)=>1===s?e:0===s?t:t+(e-t)*s,mt=t=>t===1/0?c:t===-1/0?-c:t,ft=t=>t<=d?d:mt(ct(t,11)),gt=t=>F(t)?[...t]:t,yt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},_t=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},Tt=t=>{let e=p;for(let s=0,i=g.length;s<i;s++){const i=g[s],r=t[i];if(void 0!==r){if("translateX"===i){const i=t.translateY;if(void 0!==i){const n=t.translateZ;void 0!==n?(e+=`translate3d(${r},${i},${n}) `,s+=2):(e+=`translate(${r},${i}) `,s+=1);continue}}if("scaleX"===i&&void 0===t.scale){const i=t.scaleY;if(void 0!==i){const n=t.scaleZ;void 0!==n?(e+=`scale3d(${r},${i},${n}) `,s+=2):(e+=`scale(${r},${i}) `,s+=1);continue}}e+=`${y[i]}${r}) `}"rotateZ"===i&&void 0!==t.rotate3d&&(e+=`rotate3d(${t.rotate3d}) `)}return void 0!==t.matrix&&(e+=`matrix(${t.matrix}) `),void 0!==t.matrix3d&&(e+=`matrix3d(${t.matrix3d}) `),e},xt=[];function St(t,e){if(!t)return null;const s=xt.length;t:for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.targetAdapters;for(let s=0,i=r.length;s<i;s++){const i=r[s];if(i.detect(t)){const s=i.props[e];if(s&&(!s.gate||s.gate(t)))return s;break t}}}for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.propertyResolvers;for(let s=0,i=r.length;s<i;s++){const i=r[s](t,e);if(i)return i}}return null}const wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),$t=(t,e)=>H(t)?e:t,Ct=(t,e)=>{const s=t.match(N),i=e[o]?e:document.documentElement;let r=getComputedStyle(i)?.getPropertyValue(s[1]);return r&&r.trim()!==p||!s[2]||(r=s[2].trim()),r||0},Et=(t,e,s,i,r,n)=>{if(z(t)){if(!r){const r=t(e,s,i,n);return isNaN(+r)?r||0:+r}const o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};return r.func=o,o()}if(O(t)&&B(t,"var(")){if(!r)return Ct(t,e);const s=()=>Ct(t,e);return r.func=s,s()}return t},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:B(e,"--")?4:e in t.style?2:e in t?0:1:0,Nt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[Line truncated
</script><script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Draw a connection — Motion that explains</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-example="svg-draw"><header><nav aria-label="Breadcrumb"><a href="../index.html">All examples</a> / <a href="../elements/index.html">Elements</a> / <span aria-current="page">Draw a connection</span></nav><h1>Draw a connection</h1><p>Scenes start complete. Play or replay the sequence; seek pauses. Reduced motion keeps the scene complete.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/**
* Anime.js - UMD minified bundle
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).anime={})}(this,function(t){"use strict";const e="undefined"!=typeof window,s=e?window:null,i=e?document:null,r={replace:0,none:1,blend:2},n=Symbol(),o=Symbol(),a=Symbol(),l=Symbol(),h=Symbol(),d=1e-11,c=1e12,u=1e3,p="",m=[],f=(()=>{const t=new Map;return t.set("x","translateX"),t.set("y","translateY"),t.set("z","translateZ"),t})(),g=["perspective","translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY"],y=g.reduce((t,e)=>({...t,[e]:e+"("}),{}),_=()=>{},b=/\)\s*[-.\d]/,v=/(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i,T=/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i,x=/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,S=/hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i,w=/hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i,$=/[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi,C=/^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i,E=/([a-z])([A-Z])/g,k=/(\*=|\+=|-=)/,N=/var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/,D={id:null,keyframes:null,playbackEase:null,playbackRate:1,frameRate:240,loop:0,reversed:!1,alternate:!1,autoplay:!0,persist:!1,duration:u,delay:0,loopDelay:0,ease:"out(2)",composition:r.replace,modifier:t=>t,onBegin:_,onBeforeUpdate:_,onUpdate:_,onLoop:_,onPause:_,onComplete:_,onRender:_},A={current:null,root:i},I={defaults:D,precision:4,timeScale:1,tickThreshold:200,editor:null},R={version:"4.5.0",engine:null};e&&(s.AnimeJS||(s.AnimeJS=[]),s.AnimeJS.push(R));const L=t=>t.replace(E,"$1-$2").toLowerCase(),B=(t,e)=>0===t.indexOf(e),P=Date.now,F=Array.isArray,M=t=>t&&t.constructor===Object,V=t=>"number"==typeof t&&!isNaN(t),O=t=>"string"==typeof t,z=t=>"function"==typeof t,H=t=>void 0===t,X=t=>H(t)||null===t,Y=t=>e&&t instanceof SVGElement,W=t=>v.test(t),U=t=>B(t,"rgb"),q=t=>B(t,"hsl"),j=t=>!I.defaults.hasOwnProperty(t),G=["opacity","rotate","overflow","color"],Z=t=>O(t)?parseFloat(t):t,Q=Math.pow,J=Math.sqrt,K=Math.sin,tt=Math.cos,et=Math.abs,st=Math.exp,it=Math.ceil,rt=Math.floor,nt=Math.asin,ot=Math.max,at=Math.atan2,lt=Math.PI,ht=Math.round,dt=(t,e,s)=>t<e?e:t>s?s:t,ct=(t,e)=>{if(e<0)return t;if(!e)return ht(t);const s=10**e;return ht(t*s)/s},ut=(t,e)=>F(e)?e.reduce((e,s)=>et(s-t)<et(e-t)?s:e):e?ht(t/e)*e:t,pt=(t,e,s)=>1===s?e:0===s?t:t+(e-t)*s,mt=t=>t===1/0?c:t===-1/0?-c:t,ft=t=>t<=d?d:mt(ct(t,11)),gt=t=>F(t)?[...t]:t,yt=(t,e)=>{const s={...t};for(let i in e){const r=t[i];s[i]=H(r)?e[i]:r}return s},_t=(t,e,s,i="_prev",r="_next")=>{let n=t._head,o=r;for(s&&(n=t._tail,o=i);n;){const t=n[o];e(n),n=t}},bt=(t,e,s="_prev",i="_next")=>{const r=e[s],n=e[i];r?r[i]=n:t._head=n,n?n[s]=r:t._tail=r,e[s]=null,e[i]=null},vt=(t,e,s,i="_prev",r="_next")=>{let n=t._tail;for(;n&&s&&s(n,e);)n=n[i];const o=n?n[r]:t._head;n?n[r]=e:t._head=e,o?o[i]=e:t._tail=e,e[i]=n,e[r]=o},Tt=t=>{let e=p;for(let s=0,i=g.length;s<i;s++){const i=g[s],r=t[i];if(void 0!==r){if("translateX"===i){const i=t.translateY;if(void 0!==i){const n=t.translateZ;void 0!==n?(e+=`translate3d(${r},${i},${n}) `,s+=2):(e+=`translate(${r},${i}) `,s+=1);continue}}if("scaleX"===i&&void 0===t.scale){const i=t.scaleY;if(void 0!==i){const n=t.scaleZ;void 0!==n?(e+=`scale3d(${r},${i},${n}) `,s+=2):(e+=`scale(${r},${i}) `,s+=1);continue}}e+=`${y[i]}${r}) `}"rotateZ"===i&&void 0!==t.rotate3d&&(e+=`rotate3d(${t.rotate3d}) `)}return void 0!==t.matrix&&(e+=`matrix(${t.matrix}) `),void 0!==t.matrix3d&&(e+=`matrix3d(${t.matrix3d}) `),e},xt=[];function St(t,e){if(!t)return null;const s=xt.length;t:for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.targetAdapters;for(let s=0,i=r.length;s<i;s++){const i=r[s];if(i.detect(t)){const s=i.props[e];if(s&&(!s.gate||s.gate(t)))return s;break t}}}for(let i=0;i<s;i++){const s=xt[i];if(s.detect&&!s.detect(t))continue;const r=s.propertyResolvers;for(let s=0,i=r.length;s<i;s++){const i=r[s](t,e);if(i)return i}}return null}const wt=(t,e,s)=>(s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+(e-t)*(2/3-s)*6:t),$t=(t,e)=>H(t)?e:t,Ct=(t,e)=>{const s=t.match(N),i=e[o]?e:document.documentElement;let r=getComputedStyle(i)?.getPropertyValue(s[1]);return r&&r.trim()!==p||!s[2]||(r=s[2].trim()),r||0},Et=(t,e,s,i,r,n)=>{if(z(t)){if(!r){const r=t(e,s,i,n);return isNaN(+r)?r||0:+r}const o=()=>{const r=t(e,s,i,n);return isNaN(+r)?r||0:+r};return r.func=o,o()}if(O(t)&&B(t,"var(")){if(!r)return Ct(t,e);const s=()=>Ct(t,e);return r.func=s,s()}return t},kt=(t,e)=>t[o]?t[a]&&((t,e)=>{if(G.includes(e))return!1;if(t.getAttribute(e)||e in t){if("scale"===e){const e=t.parentNode;return e&&"filter"===e.tagName}return!0}})(t,e)?1:g.includes(e)||f.get(e)?3:B(e,"--")?4:e in t.style?2:e in t?0:1:0,Nt=(t,e,s)=>{const i=t.style[e];i&&s&&(s[e]=i);const r=i||getComputedStyle(t[Line truncated
</script><script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Motion that explains — Anime.js gallery</title><style>*{box-sizing:border-box}body{margin:0;background:#101823;color:#eef5ff;font:16px/1.5 system-ui,sans-serif}header,main,footer{max-width:1100px;margin:auto;padding:24px}h1{font-size:2.2rem;margin:0}h2{font-size:1.3rem}header p,article p{color:#bfcede}main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}article{padding:22px;border:1px solid #425065;border-radius:12px;background:#182333}article p{min-height:3em}svg{width:100%;display:block;background:#111c2b;border-radius:8px}svg text{fill:#eef5ff;font-size:14px}.guide{fill:none;stroke:#425065;stroke-width:5}.trace{fill:none;stroke:#69ded2;stroke-width:4}.cell{fill:#2a6d82;stroke:#85e7df;stroke-width:2}.packet{fill:#ffd787;stroke:#101823;stroke-width:2}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}button{font:inherit;border:1px solid #8da4be;background:#263b52;color:#fff;border-radius:5px;padding:6px 12px;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #ffd787;outline-offset:3px}label{display:flex;align-items:center;gap:6px}input{width:100px}output{display:block;color:#bfcede;font-size:.85rem;margin-top:8px}footer{color:#bfcede}summary{cursor:pointer}pre{white-space:pre-wrap}@media print{.controls,output{display:none}article{break-inside:avoid}main{display:block}}
/* Gallery pages compose the very same individual documents, never a second renderer. */
a{color:#85e7df;text-underline-offset:3px}nav{margin-bottom:18px;color:#bfcede}main{display:block}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr));gap:22px}section{margin-bottom:36px}.preview-grid article{padding:0;overflow:hidden}iframe{width:100%;height:510px;border:0;display:block}.open-example{display:block;padding:16px 22px;border-top:1px solid #425065}body[data-example] main{max-width:850px}body[data-example] article h2{display:none}body[data-example] article p{min-height:0}.embedded header,.embedded footer{display:none}.embedded main{padding:18px}.embedded article{padding:12px;border:0}body[data-example].embedded article h2{display:block;margin-top:0}.embedded article p{min-height:3em}.embedded svg{max-height:255px}a:focus-visible{outline:3px solid #ffd787;outline-offset:3px}@media(max-width:500px){header,main,footer{padding:16px}h1{font-size:1.8rem}}
</style></head>
<body data-gallery="all"><header><nav aria-label="Breadcrumb"><span aria-current="page">All examples</span></nav><h1>Motion that explains</h1><p>Five editable, finite examples. Explore a registry or open an individual example.</p></header><main aria-label="Animation examples"></main><footer><p>Original examples · Anime.js 4.5.0 · Offline library 0.5.0</p><details><summary>Anime.js license</summary><pre>The MIT License
Copyright (c) 2025 Julian Garnier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre></details></footer>
<script>/* Original examples; Anime.js v4.5.0. No third-party demo source. */
window.animeExamples = [
{id:'svg-draw', kind:'element', title:'Draw a connection', description:'A trace reveals a directed connection; labels remain readable.', duration:900, path:'M 55 110 C 140 20 240 200 345 110', start:'Source', end:'Destination'},
{id:'motion-path', kind:'element', title:'Follow a route', description:'A packet follows the same coordinate space as its route.', duration:1200, path:'M 55 110 C 140 20 240 200 345 110', start:'Send', end:'Receive'},
{id:'spatial-stagger', kind:'element', title:'Propagate from the center', description:'Row-major cells activate outward from the center of a grid.', duration:350, delay:100, columns:5, rows:3},
{id:'dependency-flow', kind:'composition', title:'Explain a dependency', description:'Input connects to a check, then the checked result reaches output.', duration:600, labels:['Input','Check','Output']},
{id:'assemble-explode', kind:'composition', title:'Separate, inspect, reassemble', description:'Stable parts separate to expose their relationship, then return to one assembly.', duration:700, distance:105, labels:['Interface','Compute','Storage']}
];
</script><script>/* Each mounted card owns one paused finite timeline and abortable listeners. */
(() => {
const ns='http://www.w3.org/2000/svg';
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
function mount(host,c) {
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
let destroyed=false;
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
const add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
const tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
if(c.id==='svg-draw'||c.id==='motion-path') {
add('path',{d:c.path,class:'guide'});
const path=add('path',{d:c.path,class:'trace'});
label(65,190,c.start); label(325,190,c.end);
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
} else if(c.id==='spatial-stagger') {
const cells=[];
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
} else if(c.id==='dependency-flow') {
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
} else if(c.id==='assemble-explode') {
c.labels.forEach((text,i)=>{
const part=add('g',{'data-part':i});
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
// Static labels stay outside the motion wrappers, including while assembled.
label(80+i*120,195,text);
const x=(i-1)*c.distance;
tl.add(part,{translateX:[0,x],duration:c.duration},0)
.add(part,{translateX:[x,x],duration:400},c.duration)
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
});
}
const range=host.querySelector('input'),out=host.querySelector('output');
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
tl.onUpdate=sync;
tl.onComplete=()=>{sync();out.textContent='Complete';};
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
media.addEventListener('change',settle,{signal});
window.addEventListener('beforeprint',settle,{signal});
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
window.addEventListener('pagehide',settle,{signal});
settle();
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
}
const main=document.querySelector('main'), selected=document.body.dataset.example;
if(selected !== undefined) {
const config=window.animeExamples.find(c=>c.id===selected);
if(!config)throw new Error('Unknown example ID: '+selected);
if(window.self!==window.top)document.body.classList.add('embedded');
const host=document.createElement('article');host.id=config.id;main.append(host);
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
} else {
const group=document.body.dataset.gallery;
if(!['all','elements','compositions'].includes(group))throw new Error('Unknown gallery: '+group);
for(const name of ['elements','compositions']) {
if(group!=='all'&&group!==name)continue;
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=name==='elements'?'Elements':'Compositions';heading.append(link);section.append(heading);
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
for(const config of window.animeExamples.filter(c=>c.kind===(name==='elements'?'element':'composition'))) {
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
card.append(frame,open);grid.append(card);
}
main.append(section);
}
}
})();
</script></body></html>
+22 -5
View File
@@ -1,13 +1,30 @@
{
"file": "animejs-gallery-0.4.0.html",
"sha256": "eda1a94fc4a55642a90a42e70d7b5cbea3b2f2b73b818a0b941b61103a154d75",
"package": "animejs-gallery-0.5.0",
"entry": "index.html",
"anime": "4.5.0",
"examples": 5,
"files": {
"compositions/index.html": "c9df8e06c09c21bd58d916c27fe4ef9106135805b4b6742fa1479b465b1f6221",
"elements/index.html": "171c0e4f770671feb3dcd5357c8b8c9b463cba3b95a12b489b78652aac4fd41e",
"examples/assemble-explode.html": "0aa36b3f6470df88f3929d3ca86cbe938ed97db25502be05d1edd8de16ef751c",
"examples/dependency-flow.html": "489bd1f936b52ae93455a39114cc5c3db5793bd58e314c81ea23294ef70d1214",
"examples/motion-path.html": "3df4525a09f4518b61378ecd390145a5db1f9498f37e59aa2875389b6239a898",
"examples/spatial-stagger.html": "85f00c49581f161ff6550267b72d664053cc184b9fc45a9b20bc184d57807c43",
"examples/svg-draw.html": "05cfc79d7156c97b6709d99769247cf6fcb2e39b65c42eb40f2dd9850c9db84c",
"index.html": "115ab41febd9f636b208e75a65db55a55d05eb3a571f5b76993e2dc63fd9e38c"
},
"inputs": {
"assets/animejs/compositions/index.html": "7c25b08d5da815f63fce56a022dd0f9b4e86e96a5b3c195a19dd2c0b41cd0c08",
"assets/animejs/elements/index.html": "34d6f73d2706d6f0cdc7b97bd42d163c7a610f0e104ff9c6282dcb7aa26e8412",
"assets/animejs/examples/assemble-explode.html": "246ea1e7892fba82c81032552b0f14e718778c7a66aeb861b3b12821044fe96e",
"assets/animejs/examples/dependency-flow.html": "3549126aaad2b266c97d8825c79774944b1d5a013b6d1cad7ed24cfe41ccba24",
"assets/animejs/examples/motion-path.html": "b854b0485962e18262f25809199647c27a9329f21e008dd64823b1b15ddce44a",
"assets/animejs/examples/spatial-stagger.html": "e6c327bb079a17afb0392ad20098544c32785d0389181bea96bd80efef88a974",
"assets/animejs/examples/svg-draw.html": "167c02fb2cddcd79059337231ccfa84ef5177dc3f802ec57ed012b028a2fe979",
"assets/animejs/examples.js": "37d68cac1e1f38b1974811f9ba3527cf4a92b1c98509e8353e39f7e88b94512e",
"assets/animejs/gallery.css": "7bbf68e24e5d001b636b84faf76d682f1596618d7f6798866e19851d728ed04c",
"assets/animejs/gallery.js": "1e9cd1ee35e7584f2082eaddf2cc8def0e480048f9a85cb7578aa182682c0d88",
"assets/animejs/index.html": "d6d49f9d116d0a978ff0600dd78ed7ffe076fe5fbac364a73fc14243dd7907bc",
"assets/animejs/gallery.css": "6771a35ce56016588a78263ac5371b803707867094fe2d4ac0a9cd5f8eb5ec06",
"assets/animejs/gallery.js": "6c5ea5d54fa635b2680c8e1459b5774272f0288251a06ae4d10551c5e4cf202c",
"assets/animejs/index.html": "030b0ed727ea4eb9888e16232f275ed96241d1558151f89245d30c2c65458eb6",
"assets/vendor/anime.umd.min.js": "8d5b3a58a1f64023a04a4cedeef135a2263b18da04b35177ac13438a0bad033b",
"assets/vendor/anime-LICENSE.md": "3f3e835a9952cfc2a6ca836fb95b3257c981f24fa3ef51249055acb09931897b"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+68 -3
View File
@@ -1,7 +1,17 @@
{
"sha256": "eda1a94fc4a55642a90a42e70d7b5cbea3b2f2b73b818a0b941b61103a154d75",
"files": {
"compositions/index.html": "c9df8e06c09c21bd58d916c27fe4ef9106135805b4b6742fa1479b465b1f6221",
"elements/index.html": "171c0e4f770671feb3dcd5357c8b8c9b463cba3b95a12b489b78652aac4fd41e",
"examples/assemble-explode.html": "0aa36b3f6470df88f3929d3ca86cbe938ed97db25502be05d1edd8de16ef751c",
"examples/dependency-flow.html": "489bd1f936b52ae93455a39114cc5c3db5793bd58e314c81ea23294ef70d1214",
"examples/motion-path.html": "3df4525a09f4518b61378ecd390145a5db1f9498f37e59aa2875389b6239a898",
"examples/spatial-stagger.html": "85f00c49581f161ff6550267b72d664053cc184b9fc45a9b20bc184d57807c43",
"examples/svg-draw.html": "05cfc79d7156c97b6709d99769247cf6fcb2e39b65c42eb40f2dd9850c9db84c",
"index.html": "115ab41febd9f636b208e75a65db55a55d05eb3a571f5b76993e2dc63fd9e38c"
},
"runs": [
{
"variant": "source",
"reducedMotion": "no-preference",
"examples": [
"svg-draw",
@@ -10,18 +20,22 @@
"dependency-flow",
"assemble-explode"
],
"hierarchy": true,
"exactIframeReuse": true,
"navigation": true,
"deterministicSeek": true,
"reducedMotionSeekSuppressed": false,
"keyboardControls": true,
"replay": true,
"finalVisibility": true,
"liveReducedMotion": true,
"printSettle": true,
"printAndDepartureSettle": true,
"cleanupAndRemount": true,
"errors": [],
"network": []
},
{
"variant": "source",
"reducedMotion": "reduce",
"examples": [
"svg-draw",
@@ -30,13 +44,64 @@
"dependency-flow",
"assemble-explode"
],
"hierarchy": true,
"exactIframeReuse": true,
"navigation": true,
"deterministicSeek": false,
"reducedMotionSeekSuppressed": true,
"keyboardControls": true,
"replay": true,
"finalVisibility": true,
"liveReducedMotion": true,
"printSettle": true,
"printAndDepartureSettle": true,
"cleanupAndRemount": true,
"errors": [],
"network": []
},
{
"variant": "dist",
"reducedMotion": "no-preference",
"examples": [
"svg-draw",
"motion-path",
"spatial-stagger",
"dependency-flow",
"assemble-explode"
],
"hierarchy": true,
"exactIframeReuse": true,
"navigation": true,
"deterministicSeek": true,
"reducedMotionSeekSuppressed": false,
"keyboardControls": true,
"replay": true,
"finalVisibility": true,
"liveReducedMotion": true,
"printAndDepartureSettle": true,
"cleanupAndRemount": true,
"errors": [],
"network": []
},
{
"variant": "dist",
"reducedMotion": "reduce",
"examples": [
"svg-draw",
"motion-path",
"spatial-stagger",
"dependency-flow",
"assemble-explode"
],
"hierarchy": true,
"exactIframeReuse": true,
"navigation": true,
"deterministicSeek": false,
"reducedMotionSeekSuppressed": true,
"keyboardControls": true,
"replay": true,
"finalVisibility": true,
"liveReducedMotion": true,
"printAndDepartureSettle": true,
"cleanupAndRemount": true,
"errors": [],
"network": []
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "presentation-html-skill",
"version": "0.4.0",
"version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "presentation-html-skill",
"version": "0.4.0",
"version": "0.5.0",
"dependencies": {
"@fontsource/work-sans": "5.2.8",
"animejs": "4.5.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "presentation-html-skill",
"version": "0.4.0",
"version": "0.5.0",
"private": true,
"scripts": {
"build": "python3 scripts/build.py",
+7 -1
View File
@@ -9,10 +9,16 @@ Two bounded multi-part explanations. [Editable configurations](../assets/animejs
## Run, reuse and verify
Build through `terminal` with `python3 scripts/build-gallery.py`, then open `dist/animejs-gallery-0.4.0.html`. Each card provides Play, Replay, Settle and keyboard-operable seeking. The [source entry](../assets/animejs/index.html) also runs with its sibling files and shared vendor intact.
Build through `terminal` with `python3 scripts/build-gallery.py`, then open `dist/animejs-gallery-0.5.0/index.html`. Each card provides Play, Replay, Settle and keyboard-operable seeking. The [source entry](../assets/animejs/index.html) also runs with its sibling files and shared vendor intact.
The [element registry](registry-animejs-elements.md#lifecycle-and-accessibility-contract) owns the shared lifecycle/accessibility contract and [source, provenance and test coverage](registry-animejs-elements.md#provenance-and-verified-coverage). Do not duplicate or weaken that contract for compositions: finite paused ownership, deterministic seeking, readable reduced-motion state, cleanup and offline operation all apply.
Each composition owns one timeline. The assembly's hold is an explicit constant-value tween, not an empty timeline gap: direct backward seeking from the final frame into that hold must reproduce the same separated positions as forward seeking. Keep this regression test when changing timing. Do not layer a second animation owner onto the same property. When embedding in a deck, wire its controller's `settle()` to slide departure and `destroy()` to teardown; gallery lifecycle does not automatically bind to Reveal.
The [browser test](../scripts/test-gallery.mjs) exercises both compositions alongside the three elements in normal and reduced motion, with artifact-hash-bound evidence. Retest changed geometry and labels, including intermediate frames. Cross-browser, screen-reader and exported-PDF acceptance are not established by these Chromium tests.
## Direct runnable pages
- [Registry gallery](../dist/animejs-gallery-0.5.0/compositions/index.html) · [Editable registry gallery](../assets/animejs/compositions/index.html)
- [Explain a dependency](../dist/animejs-gallery-0.5.0/examples/dependency-flow.html) · [Editable example](../assets/animejs/examples/dependency-flow.html)
- [Separate, inspect, reassemble](../dist/animejs-gallery-0.5.0/examples/assemble-explode.html) · [Editable example](../assets/animejs/examples/assemble-explode.html)
+9 -2
View File
@@ -21,9 +21,9 @@ python3 scripts/build-gallery.py
CHROMIUM_EXECUTABLE=/path/to/chrome node scripts/test-gallery.mjs
```
The dependency-free Python builder embeds the shared pinned Anime **4.5.0** UMD, CSS, configuration, runtime and Anime license into `dist/animejs-gallery-0.4.0.html`. It emits `dist/animejs-gallery-manifest.json` with artifact and input SHA-256 hashes. No CDN, external fonts, runtime fetches, bundler or Reveal dependency. Tests use the repository's existing Playwright dependency.
The dependency-free Python builder embeds the shared pinned Anime **4.5.0** UMD, CSS, configuration, runtime and Anime license into each `dist/animejs-gallery-0.5.0/examples/<id>.html` individual page. The overall and registry galleries reuse those exact pages via relative iframe sources and open-example links; galleries do not run their own animation renderer. It emits `dist/animejs-gallery-manifest.json` with artifact and input SHA-256 hashes. No CDN, external fonts, runtime fetches, bundler or Reveal dependency. Tests use the repository's existing Playwright dependency.
For reuse, load vendor → configurations → runtime with a `<main>` present, or extract `mount(host, config)` and the relevant recipe branch into your owner. The published `animeGallery.mount` returns `play()`, `replay()`, `seek(milliseconds)`, `settle()`, `state()`, `destroy()`. Keep recipe IDs unchanged when tuning configuration. `kind` is catalog metadata, not a renderer selector. Timing is in milliseconds; use positive finite durations, valid SVG paths inside the 400×220 viewBox, and short labels. This is an editable reference, not an untrusted-config validator.
For reuse, load vendor → configurations → runtime with a `<main>` present and `data-example="<registered-id>"` on the body (unknown IDs throw; query strings do not select examples), or extract `mount(host, config)` and the relevant recipe branch into your owner. The published `animeGallery.mount` returns `play()`, `replay()`, `seek(milliseconds)`, `settle()`, `state()`, `destroy()`. Keep recipe IDs unchanged when tuning configuration. `kind` is catalog metadata, not a renderer selector. Timing is in milliseconds; use positive finite durations, valid SVG paths inside the 400×220 viewBox, and short labels. This is an editable reference, not an untrusted-config validator.
## Lifecycle and accessibility contract
@@ -36,3 +36,10 @@ Reduced motion forces the readable final state for play, replay and seek. Native
All five recipes and gallery code are original examples written for this package, not copied from showcase or peer source. Anime.js itself remains the existing vendored dependency, with its license embedded by the builder; see [vendor license](../assets/vendor/anime-LICENSE.md). No third-party artwork or remote assets.
[Browser test](../scripts/test-gallery.mjs) exercises all five recipes in normal and reduced-motion offline Chromium contexts: positive finite durations; normal-motion deterministic backward/forward seeking and clamping; real Enter/Space play/replay, range-key seeking, pointer settle; actual intermediate playback; natural completion; final SVG node visibility/bounds; live reduced-motion and print settlement; destroy/remount; no network attempts or console/page errors. Reduced motion tests assert final-state seek suppression rather than animated intermediate frames. Outputs: `evidence/animejs-gallery-test-report.json` and full-page normal/reduced screenshots. The report binds results to the built HTML hash. This does not establish cross-browser, screen-reader, production-slide-navigation or PDF-export acceptance. Test changed configurations and inspect their intermediate frames before adopting them.
## Direct runnable pages
- [Registry gallery](../dist/animejs-gallery-0.5.0/elements/index.html) · [Editable registry gallery](../assets/animejs/elements/index.html)
- [Draw a connection](../dist/animejs-gallery-0.5.0/examples/svg-draw.html) · [Editable example](../assets/animejs/examples/svg-draw.html)
- [Follow a route](../dist/animejs-gallery-0.5.0/examples/motion-path.html) · [Editable example](../assets/animejs/examples/motion-path.html)
- [Propagate from the center](../dist/animejs-gallery-0.5.0/examples/spatial-stagger.html) · [Editable example](../assets/animejs/examples/spatial-stagger.html)
+24 -14
View File
@@ -1,7 +1,6 @@
"""Dependency-free, offline gallery packager. Run from any directory."""
"""Build an offline hierarchy; individual pages embed every runtime dependency."""
from pathlib import Path
import hashlib
import html
import json
import re
@@ -10,18 +9,29 @@ source = root / 'assets/animejs'
vendor = root / 'assets/vendor/anime.umd.min.js'
entries = [entry for entry in json.loads((root / 'vendor-manifest.json').read_text(encoding='utf-8'))
if entry['path'] == 'assets/vendor/anime.umd.min.js']
if len(entries) != 1 or hashlib.sha256(vendor.read_bytes()).hexdigest() != entries[0]['sha256']:
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
if len(entries) != 1 or digest(vendor) != entries[0]['sha256']:
raise ValueError('Missing, duplicate or mismatched Anime vendor fingerprint')
text = (source / 'index.html').read_text()
text = text.replace('<link rel="stylesheet" href="gallery.css">', '<style>' + (source / 'gallery.css').read_text() + '</style>')
for name, file in [('../vendor/anime.umd.min.js', vendor), ('examples.js', source / 'examples.js'), ('gallery.js', source / 'gallery.js')]:
js = re.sub(r'//# sourceMappingURL=.*', '', file.read_text())
text = text.replace(f'<script src="{name}"></script>', '<script>' + js.replace('</script', r'<\/script') + '</script>')
text = text.replace('ANIME_LICENSE', html.escape((root / 'assets/vendor/anime-LICENSE.md').read_text()))
out = root / 'dist/animejs-gallery-0.4.0.html'
out.parent.mkdir(exist_ok=True)
out.write_text(text)
inputs = [*sorted(source.glob('*')), vendor, root / 'assets/vendor/anime-LICENSE.md']
report = {'file': out.name, 'sha256': hashlib.sha256(out.read_bytes()).hexdigest(), 'anime':'4.5.0', 'examples':5, 'inputs':{str(p.relative_to(root)):hashlib.sha256(p.read_bytes()).hexdigest() for p in inputs if p.is_file()}}
out = root / 'dist/animejs-gallery-0.5.0'
files = {}
for page in sorted(source.rglob('*.html')):
text = page.read_text(encoding='utf-8')
def style(match):
return '<style>' + (page.parent / match[1]).read_text(encoding='utf-8') + '</style>'
def script(match):
js = re.sub(r'//# sourceMappingURL=.*', '', (page.parent / match[1]).read_text(encoding='utf-8'))
return '<script>' + js.replace('</script', r'<\/script') + '</script>'
text = re.sub(r'<link rel="stylesheet" href="([^"]+)">', style, text)
text = re.sub(r'<script src="([^"]+)"></script>', script, text)
if 'ANIME_LICENSE' in text:
raise ValueError('Unresolved license placeholder')
target = out / page.relative_to(source)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding='utf-8')
files[str(target.relative_to(out))] = digest(target)
inputs = [*sorted(source.rglob('*')), vendor, root / 'assets/vendor/anime-LICENSE.md']
report = {'package': out.name, 'entry': 'index.html', 'anime': '4.5.0', 'examples': 5,
'files': files, 'inputs': {str(p.relative_to(root)): digest(p) for p in inputs if p.is_file()}}
(root / 'dist/animejs-gallery-manifest.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
+49 -39
View File
@@ -1,60 +1,70 @@
import {chromium} from '@playwright/test';
import fs from 'node:fs';import path from 'node:path';import {pathToFileURL} from 'node:url';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const root=path.resolve(import.meta.dirname,'..'),file=path.join(root,'dist/animejs-gallery-0.4.0.html');
const report={sha256:crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),runs:[]};
assert.equal(report.sha256,JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json'))).sha256);
const root=path.resolve(import.meta.dirname,'..'),manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json')));
for(const [file,hash] of Object.entries(manifest.files))assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root,'dist',manifest.package,file))).digest('hex'),hash);
const report={files:manifest.files,runs:[]},ids=['svg-draw','motion-path','spatial-stagger','dependency-flow','assemble-explode'];
const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
fs.mkdirSync(path.join(root,'evidence'),{recursive:true});
try {for(const reducedMotion of ['no-preference','reduce']) {
try{for(const variant of ['source','dist'])for(const reducedMotion of ['no-preference','reduce']){
const ctx=await browser.newContext({offline:true,reducedMotion,viewport:{width:1200,height:1000}}),page=await ctx.newPage(),errors=[],network=[];
page.on('pageerror',e=>errors.push(String(e)));page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});page.on('request',r=>{if(!r.url().startsWith('file:')&&!r.url().startsWith('data:'))network.push(r.url());});
await page.goto(pathToFileURL(file).href);
const ids=await page.evaluate(()=>Object.keys(animeGallery.controllers));assert.equal(ids.length,5);
assert.equal(new Set(ids).size,ids.length,'unique recipe IDs');
const base=path.join(root,variant==='source'?'assets/animejs':'dist/'+manifest.package),url=p=>pathToFileURL(path.join(base,p)).href;
const shot=async name=>{const size=page.viewportSize();await page.setViewportSize({width:size.width,height:await page.evaluate(()=>document.documentElement.scrollHeight)});await page.evaluate(()=>scrollTo(0,0));await page.waitForTimeout(100);await page.screenshot({path:path.join(root,`evidence/animejs-${variant}-${name}.png`),fullPage:true});await page.setViewportSize(size);};
for(const group of ['all','elements','compositions']){
await page.goto(url(group==='all'?'index.html':group+'/index.html'));
const expected=group==='all'?ids:group==='elements'?ids.slice(0,3):ids.slice(3);
assert.deepEqual(await page.locator('iframe').evaluateAll(f=>f.map(e=>e.dataset.example)),expected);
assert.equal(await page.evaluate(()=>typeof window.animeGallery),'undefined');
for(const id of expected){
const f=page.locator(`iframe[data-example="${id}"]`),target=url('examples/'+id+'.html');
assert.equal(await f.evaluate(e=>e.src),target);
assert.equal(await page.locator(`a.open-example[href$="/${id}.html"]`).evaluate(e=>e.href),target);
const inner=await(await f.elementHandle()).contentFrame();await inner.waitForFunction(()=>window.animeGallery);
assert.deepEqual(await inner.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);assert.ok(await f.isVisible());assert.ok(await inner.locator('article h2').isVisible());assert.ok(await inner.locator('svg').isVisible());
}
if(group==='all')for(const g of ['elements','compositions'])assert.equal(await page.locator(`section[data-registry="${g}"] h2 a`).evaluate(e=>e.href),url(g+'/index.html'));
if(reducedMotion==='no-preference')await shot(group);
await page.locator('a.open-example').first().click();assert.equal(await page.locator('body').getAttribute('data-example'),expected[0]);
await page.getByRole('link',{name:'All examples',exact:true}).click();assert.equal(page.url(),url('index.html'));
}
const configs=await page.evaluate(()=>animeExamples.map(({id,kind})=>({id,kind})));
for(const [group,kind] of [['elements','element'],['compositions','composition']]){
const registry=path.join(root,`references/registry-animejs-${group}.md`),text=fs.readFileSync(registry,'utf8');
const listed=[...text.matchAll(/^\| `([^`]+)` \|/gm)].map(m=>m[1]);
assert.deepEqual(listed.sort(),configs.filter(c=>c.kind===kind).map(c=>c.id).sort(),'registry matches executable '+group);
for(const match of text.matchAll(/\]\(([^)]+)\)/g)){
const target=match[1].split('#')[0];if(target&&!target.includes('://'))assert.ok(fs.existsSync(path.resolve(path.dirname(registry),target)),'missing registry target '+target);
}
assert.deepEqual([...text.matchAll(/^\| `([^`]+)` \|/gm)].map(m=>m[1]).sort(),configs.filter(c=>c.kind===kind).map(c=>c.id).sort());
for(const match of text.matchAll(/\]\(([^)]+)\)/g)){const target=match[1].split('#')[0];if(target&&!target.includes('://'))assert.ok(fs.existsSync(path.resolve(path.dirname(registry),target)),target);}
}
for(const id of ids){
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg});
const state=()=>call('state');
const visual=()=>page.locator('#'+id+' svg').evaluate(e=>e.outerHTML);
await page.goto(url('examples/'+id+'.html'));
assert.deepEqual(await page.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);
assert.equal(await page.locator('h1').textContent(),await page.evaluate(id=>animeExamples.find(c=>c.id===id).title,id));
assert.ok(!(await page.content()).includes('ANIME_LICENSE'));
const group=ids.indexOf(id)<3?'elements':'compositions';await page.getByRole('link',{name:group[0].toUpperCase()+group.slice(1),exact:true}).click();assert.equal(page.url(),url(group+'/index.html'));await page.locator(`a.open-example[href$="/${id}.html"]`).click();
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg}),state=()=>call('state'),visual=()=>page.locator('#'+id+' svg').evaluate(e=>e.outerHTML);
const final=await visual(),s=await state();assert.ok(Number.isFinite(s.duration)&&s.duration>0);assert.equal(s.time,s.duration);
if(reducedMotion==='no-preference'){
if(id==='assemble-explode'){
await call('settle');await call('seek',s.duration*.45);const hold=await visual();
const offsets=await page.locator('#assemble-explode [data-part]').evaluateAll(parts=>parts.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41));
assert.deepEqual(offsets,[-105,0,105],'backward seek into hold must remain exploded');
await call('seek',0);await call('seek',s.duration*.45);assert.equal(await visual(),hold,'hold state independent of seek direction');
await call('seek',s.duration*.45);const hold=await visual();assert.deepEqual(await page.locator('[data-part]').evaluateAll(parts=>parts.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41)),[-105,0,105]);
await shot(id+'-hold');await call('seek',0);await call('seek',s.duration*.45);assert.equal(await visual(),hold);
}
await call('seek',s.duration*.3);const earlier=await visual();await call('seek',s.duration*.8);const later=await visual();assert.notEqual(earlier,later);
await call('seek',s.duration*.3);const earlier=await visual();await shot(id+'-motion');await call('seek',s.duration*.8);const later=await visual();assert.notEqual(earlier,later);
await call('seek',s.duration*.3);assert.equal(await visual(),earlier);await call('seek',s.duration*.8);assert.equal(await visual(),later);
await call('seek',-100);assert.equal((await state()).time,0);await call('seek',s.duration+100);assert.equal((await state()).time,s.duration);
await page.locator('#'+id+' input').focus();await page.keyboard.press('Home');assert.equal((await state()).time,0);
await page.keyboard.press('ArrowRight');assert.ok((await state()).time>0);
}else {await call('seek',0);assert.equal(await visual(),final);}
await page.locator('input').focus();await page.keyboard.press('Home');assert.equal((await state()).time,0);await page.keyboard.press('ArrowRight');assert.ok((await state()).time>0);
}else{await call('seek',0);assert.equal(await visual(),final);}
for(const action of ['play','replay']){
await page.locator(`#${id} [data-action="${action}"]`).focus();await page.keyboard.press(action==='play'?'Enter':'Space');
await page.locator(`[data-action="${action}"]`).focus();await page.keyboard.press(action==='play'?'Enter':'Space');
if(reducedMotion==='no-preference'){await page.waitForTimeout(90);const running=await state();assert.ok(running.time>0&&running.time<running.duration);assert.notEqual(await visual(),final);}
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
assert.equal(await visual(),final);
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);assert.equal(await visual(),final);
}
await call('replay');await page.locator(`#${id} [data-action="settle"]`).click();assert.equal(await visual(),final);assert.equal((await state()).paused,true);
const readable=await page.locator('#'+id+' svg').evaluate(svg=>[...svg.querySelectorAll('text,rect,circle')].every(e=>{const b=e.getBoundingClientRect(),r=svg.getBoundingClientRect();return Number(getComputedStyle(e).opacity)>0&&b.width>0&&b.left>=r.left-1&&b.right<=r.right+1&&b.top>=r.top-1&&b.bottom<=r.bottom+1;}));assert.ok(readable,id+' visible and within SVG');
await call('replay');await page.locator('[data-action="settle"]').click();assert.equal(await visual(),final);assert.equal((await state()).paused,true);
assert.ok(await page.locator('svg').evaluate(svg=>[...svg.querySelectorAll('text,rect,circle')].every(e=>{const b=e.getBoundingClientRect(),r=svg.getBoundingClientRect();return Number(getComputedStyle(e).opacity)>0&&b.width>0&&b.left>=r.left-1&&b.right<=r.right+1&&b.top>=r.top-1&&b.bottom<=r.bottom+1;})),id+' bounds');
await shot(id+'-'+reducedMotion);
await call('replay');await page.emulateMedia({reducedMotion:'reduce'});await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
await page.emulateMedia({reducedMotion:'no-preference'});
for(const event of ['beforeprint','pagehide']){await call('replay');await page.evaluate(event=>dispatchEvent(new Event(event)),event);assert.equal((await state()).paused,true);assert.equal((await state()).time,s.duration);}
await call('replay');await call('destroy');await call('destroy');assert.ok((await state()).destroyed);assert.equal(await page.locator('#'+id+' > *').count(),0);
await page.evaluate(id=>{animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);assert.equal(await page.locator('article button').count(),3);assert.equal(await visual(),final);
await page.emulateMedia({reducedMotion});
}
await page.screenshot({path:path.join(root,`evidence/animejs-gallery-${reducedMotion}.png`),fullPage:true});
await page.evaluate(()=>Object.values(animeGallery.controllers).forEach(c=>c.replay()));await page.emulateMedia({reducedMotion:'reduce'});
await page.waitForFunction(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;}));
assert.ok(await page.evaluate(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;})));
await page.emulateMedia({reducedMotion:'no-preference'});await page.evaluate(()=>{Object.values(animeGallery.controllers).forEach(c=>c.replay());dispatchEvent(new Event('beforeprint'));});
assert.ok(await page.evaluate(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;})));
await page.evaluate(()=>{for(const [id,c] of Object.entries(animeGallery.controllers)){c.replay();c.destroy();c.destroy();if(!c.state().destroyed||document.getElementById(id).children.length)throw Error('cleanup failed');}for(const config of animeExamples)animeGallery.controllers[config.id]=animeGallery.mount(document.getElementById(config.id),config);});
assert.equal(await page.locator('article button').count(),15);
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);report.runs.push({reducedMotion,examples:ids,deterministicSeek:reducedMotion==='no-preference',reducedMotionSeekSuppressed:reducedMotion==='reduce',keyboardControls:true,replay:true,finalVisibility:true,liveReducedMotion:true,printSettle:true,cleanupAndRemount:true,errors,network});await ctx.close();
}fs.writeFileSync(path.join(root,'evidence/animejs-gallery-test-report.json'),JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report,null,2));
}finally{await browser.close();}
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);report.runs.push({variant,reducedMotion,examples:ids,hierarchy:true,exactIframeReuse:true,navigation:true,deterministicSeek:reducedMotion==='no-preference',reducedMotionSeekSuppressed:reducedMotion==='reduce',keyboardControls:true,replay:true,finalVisibility:true,liveReducedMotion:true,printAndDepartureSettle:true,cleanupAndRemount:true,errors,network});await ctx.close();
}fs.writeFileSync(path.join(root,'evidence/animejs-gallery-test-report.json'),JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report,null,2));}finally{await browser.close();}