feat: add explained assembly pattern with offline example and QA
This commit is contained in:
1 parent
50eab114ae
commit
563aaf4757
17 files changed
+341
-3
No files matched your search
@@ -30,6 +30,12 @@ Python builds with no network and no npm installation. npm/Chromium are needed o
|
|||||||
|
|
||||||
Use six slides as examples, not a fixed narrative. Copy the package to a deck workspace before editing. Adapt test inventory when slide/node counts change. Work Sans is optional example typography. Channel-specific preferences and authority remain outside this global skill.
|
Use six slides as examples, not a fixed narrative. Copy the package to a deck workspace before editing. Adapt test inventory when slide/node counts change. Work Sans is optional example typography. Channel-specific preferences and authority remain outside this global skill.
|
||||||
|
|
||||||
|
## Explanatory mechanism example
|
||||||
|
|
||||||
|
For architecture decomposition, modular products and inspect/reassemble interactions, first read [the mechanism reference](references/explanatory-mechanisms.md). It explains when to choose the pattern, what was observed in the Anime.js showcase, and what must not be inferred or copied.
|
||||||
|
|
||||||
|
`assets/mechanism.html` is an original editable SVG example. Build it with `python3 scripts/build-mechanism.py`, then open `dist/mechanism-demo-0.2.0.html`. All four state controls and all three components are covered by `scripts/test-mechanism.mjs`; `npm test` runs both the original deck and this new example.
|
||||||
|
|
||||||
## Licenses and scope
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: presentation-html
|
name: presentation-html
|
||||||
description: Build offline HTML decks with deliberate motion.
|
description: Build offline HTML decks with deliberate motion.
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
author: RootAtSkic, Hermes Agent
|
author: RootAtSkic, Hermes Agent
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos, windows]
|
platforms: [linux, macos, windows]
|
||||||
@@ -30,6 +30,10 @@ Build editable, offline-first browser presentations using Reveal.js for navigati
|
|||||||
- Read [research](references/research.md), [example catalog](references/examples.md) and [QA contract](references/qa.md) before changing the starter.
|
- Read [research](references/research.md), [example catalog](references/examples.md) and [QA contract](references/qa.md) before changing the starter.
|
||||||
- Optional design/review skills may supplement this workflow; channel instructions remain channel-owned.
|
- Optional design/review skills may supplement this workflow; channel instructions remain channel-owned.
|
||||||
|
|
||||||
|
## Read on demand: explanatory mechanisms
|
||||||
|
|
||||||
|
Before designing an exploded view, modular assembly, product-internals explanation or reversible inspect/reassemble interaction, read [explanatory mechanisms](references/explanatory-mechanisms.md). It separates inspected Anime.js/Awwwards visual references from unverified implementation claims, links the original runnable `assets/mechanism.html` example, and defines adaptation and QA checks. Use it when motion must explain how parts form a whole—not to decorate tables. Build with `python3 scripts/build-mechanism.py`; test with `node scripts/test-mechanism.mjs` in the copied package.
|
||||||
|
|
||||||
## How to Run
|
## How to Run
|
||||||
|
|
||||||
Copy this complete package into a new working directory. Never edit an installed skill to create a client deck. Use `terminal` with that directory as `workdir`:
|
Copy this complete package into a new working directory. Never edit an installed skill to create a client deck. Use `terminal` with that directory as `workdir`:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>One system, three responsibilities</title>
|
||||||
|
<style>
|
||||||
|
/* FONT_CSS */
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:#101e25;color:#f4f3ed;font-family:'Work Sans',sans-serif}main{max-width:1120px;margin:auto;padding:40px 36px}.eyebrow{color:#75d5c6;letter-spacing:.15em;font-size:13px;text-transform:uppercase}h1{font-size:44px;letter-spacing:-.04em;margin:12px 0}p{line-height:1.5;color:#c9d2d5}.intro{max-width:750px}.controls{display:flex;gap:10px;flex-wrap:wrap;margin:24px 0}button{font:inherit;color:inherit;border:1px solid #74868e;background:transparent;padding:11px 16px;cursor:pointer}button[aria-pressed=true]{background:#75d5c6;color:#101e25;border-color:#75d5c6}button:focus-visible,[role=button]:focus-visible{outline:3px solid #ffd17a;outline-offset:5px}svg{display:block;width:100%;height:auto;overflow:visible;background:#152831;border:1px solid #425861}.part{cursor:pointer;outline:none}.part .shell{fill:#26434e;stroke:#9ac0cb;stroke-width:2}.part[aria-pressed=true] .shell{fill:#34675f;stroke:#75d5c6;stroke-width:3}.part:hover .shell{stroke:#fff}svg text{fill:#f4f3ed;font-family:inherit;text-anchor:middle;font-size:20px}svg .hint{fill:#aec3cb;font-size:14px}.detail{border-left:3px solid #75d5c6;padding:12px 18px;min-height:83px;margin:20px 0}.detail strong{display:block;margin-bottom:5px}.foot{font-size:12px;color:#aec3cb}@media(max-width:600px){main{padding:24px 16px}h1{font-size:32px}}@media print{body{background:white;color:#101e25}.controls{display:none}main{padding:0}.detail,p,.foot{color:#101e25}}
|
||||||
|
</style></head><body><main>
|
||||||
|
<div class="eyebrow">Explanatory motion · original SVG example</div><h1>One system, three responsibilities.</h1><p class="intro">Keep the same parts in view. Separate them to explain their roles, inspect a component, then recombine the system.</p>
|
||||||
|
<div class="controls" aria-label="Assembly controls"><button id="assemble" aria-pressed="true">1 · Assemble</button><button id="explode" aria-pressed="false">2 · Explode</button><button id="inspect" aria-pressed="false">3 · Inspect</button><button id="reassemble" aria-pressed="false">4 · Reassemble</button></div>
|
||||||
|
<svg viewBox="0 0 1040 370" role="group" aria-label="Three-part service assembly">
|
||||||
|
<path d="M135 185H905" stroke="#58727c" stroke-dasharray="5 8"/><text x="520" y="40" class="hint" id="diagram-state">Assembled · a single system</text>
|
||||||
|
<g class="part" data-index="2" role="button" tabindex="0" aria-label="Inspect storage" aria-pressed="false" transform="translate(580 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><path d="M-65 -20H65M-65 0H65M-65 20H65" stroke="#75d5c6" stroke-width="4"/><text y="-55">Storage</text><text y="65" class="hint">Retain the outcome</text></g>
|
||||||
|
<g class="part" data-index="1" role="button" tabindex="0" aria-label="Inspect processing" aria-pressed="false" transform="translate(520 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><path d="M-45 0L0 -30L45 0L0 30Z" fill="none" stroke="#75d5c6" stroke-width="4"/><text y="-55">Processing</text><text y="65" class="hint">Apply the rules</text></g>
|
||||||
|
<g class="part" data-index="0" role="button" tabindex="0" aria-label="Inspect interface" aria-pressed="false" transform="translate(460 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><circle r="27" fill="none" stroke="#75d5c6" stroke-width="4"/><text y="-55">Interface</text><text y="65" class="hint">Receive the intent</text></g>
|
||||||
|
<text x="520" y="338" class="hint">Explode to reveal all labels · select a part for its responsibility</text></svg>
|
||||||
|
<div class="detail" aria-live="polite"><strong id="detail-title">Assembled system</strong><span id="detail-body">Three stable components share one purpose. Explode the assembly to reveal each responsibility.</span></div>
|
||||||
|
<p class="foot">Original abstract 2D assembly, not a reproduction of the Anime.js mechanism. No third-party artwork. Motion respects reduced-motion preferences. Keyboard: Tab, Enter and Space.</p>
|
||||||
|
</main><script>/* ANIME_JS */</script><script>
|
||||||
|
'use strict';
|
||||||
|
const parts=[...document.querySelectorAll('.part')].sort((a,b)=>a.dataset.index-b.dataset.index),buttons=[...document.querySelectorAll('.controls button')],mq=matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
const info=[['Interface','Receives intent and presents the result. It does not own persistence.'],['Processing','Applies rules and coordinates the operation. It does not own the user interface.'],['Storage','Retains the outcome for later retrieval. It does not decide the business rules.']];
|
||||||
|
const model=[{x:460},{x:520},{x:580}];let animation=null,expanded=false,selected=-1;
|
||||||
|
function draw(){parts.forEach((p,i)=>p.setAttribute('transform',`translate(${model[i].x} 185)`));}
|
||||||
|
function settle(){if(animation){animation.complete();animation=null;}}
|
||||||
|
function move(open,immediate=false){settle();expanded=open;parts.forEach((p,i)=>p.querySelectorAll('text').forEach(t=>t.style.opacity=open||i===0?'1':'0'));const target=open?[200,520,840]:[460,520,580];if(mq.matches||immediate){model.forEach((m,i)=>m.x=target[i]);draw();}else{animation=anime.animate(model,{x:(_,i)=>target[i],duration:650,ease:'inOutCubic',onUpdate:draw,onComplete:()=>{animation=null;}});}document.querySelector('#diagram-state').textContent=open?'Exploded · inspect each responsibility':'Assembled · a single system';}
|
||||||
|
function activate(id){buttons.forEach(b=>b.setAttribute('aria-pressed',String(b.id===id)));}
|
||||||
|
function detail(i){selected=i;parts.forEach((p,j)=>p.setAttribute('aria-pressed',String(i===j)));document.querySelector('#detail-title').textContent=i<0?'Assembled system':info[i][0];document.querySelector('#detail-body').textContent=i<0?'Three stable components share one purpose. Explode the assembly to reveal each responsibility.':info[i][1];}
|
||||||
|
function inspect(i){activate('inspect');move(true);detail(i);}
|
||||||
|
buttons.forEach(b=>b.addEventListener('click',()=>{activate(b.id);if(b.id==='inspect'){inspect(selected<0?1:selected);}else{move(b.id==='explode');detail(-1);if(b.id==='explode'){document.querySelector('#detail-title').textContent='Exploded system';document.querySelector('#detail-body').textContent='The same components, separated without changing identity. Select any component to inspect its role.';}}}));
|
||||||
|
parts.forEach((p,i)=>{p.addEventListener('click',()=>inspect(i));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();e.stopPropagation();inspect(i);}});});
|
||||||
|
mq.addEventListener('change',settle);document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();});window.addEventListener('pagehide',settle);window.addEventListener('beforeprint',()=>{move(true,true);});move(false,true);window.mechanismQA={settle,state:()=>({expanded,selected,active:!!animation,positions:model.map(m=>m.x)})};
|
||||||
|
</script><!-- LICENSES --></body></html>
|
||||||
Vendored
+143
@@ -0,0 +1,143 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>One system, three responsibilities</title>
|
||||||
|
<style>
|
||||||
|
@font-face{font-family:'Work Sans';font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAE28ABAAAAAAvJAAAE1aAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmobgZoAHI1wBmA/U1RBVEgAhTYRCAqBoWyBgTYLhSwAATYCJAOKVAQgBYRUB498DAcbmqglbJtWzOA8ABjKnsefTDf3yN1SZxItkmYjYtg4AHP7w7P/P+NAjTF8sAM0y1pTeAgFIpJMoUiC6ipTQ2eVRu9RKhOlnJlOnpfSXPNes9uJ1vNcKrMKFxcO6zorzHhVwTLZ9Fr8Fxdcln9amKrDJy4tv/6Jv65v77SpwIlQuOCkxdJFHLOWNsLH0AgCJ/FzhymcoW+3dQ2P5rhVph+4CoxdD1E11qmX5/ns/r99qvrygx5L0aT2yiFkYChH1Ovjac5+3u5GdhNiLCEkIQkhJUETgofgAVpq2AFFNUARrTgVo4VSRY5S1d/PubVUFOq0V7GjogxP0/kvBWrgrSdN0qQRa+wijVzukpMkF7loU9qmYtQUKe4NUqRsbHToDMaQ/Q2ZKR+TmcCGTRT+n//D/3eufe5vLcW6YIbHMGKhEHbwZkF+Z1Cv8jrAw1Qex+qFvtg6h+QKzdtP6czS1U9F//XPF1WK6qkLEjhAh/axLiebRZZkWbjaXeGCDPl7zSX4M3HMabU4waVc/j/+VO8HoeUQy0FhSCZhgVeaTCAT4BcHzTBshWFqsxEoPq40muARxm4/LLIAA4wk0WuAf4GA/ze3Wbo7xFIY5BGqfszyHpkKlTxXRCOZELn/38yfgVCZCU9jVfErAbKhrLHn8MQryi/5rYi/iks2C8pK5pqmQ6YH/r9SZBvuwYfQN6qyoopgvJ8kxoWnnv4mLs+bWQ8orX+F8tKc+QkzIV8Y+7WrHTAUCHLJ/+9Ps/ZdfysjTVBBbxZ19jiA9nIZAGiXqi2qp/ee9PT/0xvJX/KMpB/NaL4niWRIDPGJ7JD+tzfzLdsTIM0sEAclOzAQUgioWgLsqGm2BCwbqrgomq1aqrdeHr7X9Vmq466ukcZRRy5n88IpR+0gIgwD/VYP3C/LwJ8M/MxtlvmDCSMAWEQtYrQhYLxBtPD/33JaZi51IOVjXCTCcWLVsj/9pdVZtyhsShVCtaJjrIpV01mWnZ0PrUskxuaES4JzrZxQ0B0eIz7CIVEW+FzYq98o6XE7U6N+1MUsQjms7euTamFecs8iw+RhRMRtTNN0RAb3Or/MYW7kf9/1SmvLO18hLBJkCEMIQbzy8qd/QGIcmyrtxZVB7eebq7mUD9i3cZGSFF2U+ngNUlqG0+2HizlmLZ7KdwcXGB4A28GwITyIQC+kzwAsppjhCY8dHksweKaHhicxQTgEAxYCoyFkTAmOOeFBY+f4e4RugFggYEgnEbbDTnscgHfGsRechYcAtOMqEBj89BPPO4vmd8iQ/1RxmmiQoyxZtCA3fqfflcnf+nExCSAUBGnUCInHRQoVyMiDCxUpKRqKoq6AQD05AP/RodwCteN/TryxKd++YfcgQX+ih9G1DkXvn6a5IQCvnBzEp4QQEGh1EOjd1xtgnHKwod5uXFgvlxjWfxUaEXihqb9U1UxpCAIUVOKBFAqz9pXNMv41KDwvD/t+GVWlip+6YhZJOkuYN1KxO/YiulN/bmllwwQJ8NH1Bp5VerBt61r7Vmwx85q75tXOuuLlLn3Jhlw5jSpPXOTM859hbtOUqpBLA5KMaFzmHxXWSsW+c8wnAm35dCXroMOqXAgw0/7Vo+6v99+reWKfW+x0jctc6GwbnRlkl+Vh3oVmm2aiU4wxXJO+auilbs2qlVMKi4nKRF8rkGIQURKfZlQRb+IFHqXRWtX42qrLK23dqd93qiet/5Etda+dHWhPN3V9V7Wt89sq9aqUypc5lNL4xtXS0Ab6OmM96nJQC6WNKQ/sAqv48Mtf8YwHuJgoNFljycrdRNBsP+7xZv1lbxrHbvrZRhftrJjHIac9xtCsllWxuXKn00lmGhOJ/OPN+GPgT35CgwIJonFxLkMH0wVUeWfslz8JiB09UupZzUJm7Ot177M/O22UudfUgjXtZ3IjnVkzLrYsCzObRuMVykIRlu5T48da7/UKvYyN7tsVyBaga4kmFbSWlT+ArcQ0ONaBMGibgeHowZAC8HLuB/AAAVEZyzXQQU8cPMoD/cb8OrEApZUUm37YelStJ4ZkSLTErpYgRtZ5AzYPchetPMoo0c3u+KOXCVEo2XXAdfsFaIF+DJ7SXy81jW7sjj+umEigt+93UkL4WRxbkdO3X0hOzoZwxhlnnM0AZGBlb9Bwr3yZRa1rSz0gDe1E6oQvdRj61mFrmsoTTdDQgZ5cCZTcutwAOCid2C6gjWExvt12VCYWgmMBbWqDWhDcCkxZS0xfQ+QFBQG9hdZ0+eUUVi4uwjxJwOU2K5vShSZLAk2jIytqEhLwQcFDHpRvAvKABUuVX4sQfQn0yuDQC3jZQoFiiEoi1Xh4Cm7AkoCS3FZDM91YSxT45iEBd9TaaqC6JO8Dzh0N5AFxz6EVEkd6SJ3I1QAe2BAb+jrD9AolWZ9ksklWOZhDOpXKGCPqVU/4yId4yENNakDRolVctW7YPElgEhUGfyQTYEt/qCLwROkVjgK28+oWM/CX60CsPEzUGQcbO156rRFAQ6l7e4mdbggSG77uL7MARkMwoA+b8g8CTP9ffBQC+ABgAhIEgMYeBw+TcP5TjwZAJCveCAAAokCm1d9DZgWTZIt91rsMclLliOHWZWggSiNIFNTSqUj5V8l2GAZpFkVEhRyFEtPwUCa+ZtULfusmUBguNTR8LPSK1ZpNyAvoaMRau9Ch6o2bBTJ6UpgCAOR8IB5nD3dD/qhOd7WLLcS75aEQ4u1SNJacYcH6BbxyunIc+OeHUj1rNwyUFg95MqyzDM90Gr0NHo+PBAC8SGRdW4mch1gJhcMMXF9Fr242ZvT5ByfJAboMhLvfuK/P3MAhUbM+QmmppcGVkY13I+nJmfOKG3LtAy8o7/oNaNgZSx+EUQ3GniGr6rm5gdw4gJTgyNFtZnUhE1RZR5J1ldQyHgLKAHYOcR4Wlc1LFHpO8n0NiKV/lZWa1vo8okEP/5kPay5hQpxCO4IPaQpkd1rwsKBukQQFErF0oQbAy6TXQ5+UCSQ0UVqrYY4f0hWbnlwJbRGcw5YiKDOr2nVr27C/QTwKVtnwsTa62L6ALT59hlvSaXHa27rzbYEZPehrOMz3ys9b7jsAFPVyBh+ZkOoaStntzBoaNzEPANrXp2EZKYzBQUfcFDtHE/cWoYhNLTer10xz43hji1t3ROs91BmVZ03hwaOQD8WGjtRzYMILui3DhlENjf4CiMSpH6MLwzpzT4BuNtlsaTi7h73uvhBU5WpL8kHlnXGl9/qtFvSXx83YkP4QiXGdvrVsQjIkKAau1gcnHn0L8UhCHg/buzmlbniJ83xHfEFEa+FFvx65QUG9SgDyOr+fk6rJlTORQyCj56VzCQeHTAw3gNP8MJNHD/dnF4tQLUusE0+H1PU0ULu9n5xT/UmvGY4XQTA7klRri321RkINJQbqG0pBHwZouFPUwTpGn9qkfeQUaL7FhkFbMM4NXhMhB9u2vmBtc+PgDLLIYeEjMZ40QZPTxgk9qNUn55ocKlWiVtEzdR90ooMd6jnPhwKAS3j+OPhOmGkRp/JDjtda83XWn0V5chWs/UkRVHpqppJsYAGwxazfGVfKvlMCZxQgh0V1s1F/7ZDfwupgw1iH2H+eQVw07SVGqyNR3GLzzbPa1wnQnEA3BoqebIzIZNIsWGgPsekn9nXkjwWk1DUtzRS4XHq+RnelBoxBqKpRJAYdMg4noiUA8S/6Ga92fEpQ+NH44KjtGXJO2d6gajFEW1H57HTrcF8Z9rO7vFczIRgGsAM0DHHyQeSgKC2cP+3uDcyDwQdRG+yPAMp5aAKk79Ek7PBOUWdxBr8jzjlpkYNM35X+tOvriC9INSIViLXP2duBkrNRNbnSz0M7o/Z60em2Bc4z4mMbPd6caALEw655ySbSvUNPFRBGje13nahXv7tE3P3MExawzw+srn/wS0vG/VX/P2mhgJDAKDwkAZoNMTcinvxoBQhgECSYt0hRfMWJ4y9BggDJkgVKkSJImgImRZaKsVy7Mut1qbHZZo26DWiyyy7z7HHYfGcMWeayy1YYdtNKb0uD9gQR+DxRRPqjQ9uePHIDGeJtR77xtQtmBoSNQCIpUSgYDi6cDR6MgB2MmBjOnhzGkSMVBTWMMzc4dx4wnjzhvHjB6OlxGBiIePPDECAALYgJX7BgYmZmmAiRMPHiYRIlwqTJwZQrFyVPAaRIGaRcOVsVKtixsqJUqkdq0IClUSNKkyYCzVqxzLIQssgiLIstRlliCaRNG5alluJZZhnKcssJrbAGW7t2Eut1srHBBg66dCFttImjzTZj2Worlm22EerWy0afAbZ22UVvt9109tjDy157edpnnwn220/jgAM8HHSQu0MOcXbYYVqnnWZwxhlGQ4Z4u+wytWEj7F13g5ObbrL31kdcn4zDtACBFSGEwC0MYeAajnBwi0AESGMgBshiIiZIYyEWyGIjEhQJIoAiiggndGguySOnzhBv8nzjSwGTDoVg4iAESAOzVIwDAjDGU5hASUlGVcFNMIsquPDG7ZFjeyEVeRomyghxxpRgmGTjShm1lyYbW46x5I7i8uQTK5AQEFi4Lu8IwhlJoJYugFtoYVg0juMgERrQF8hayWYbV7bsym2PwqEIc/W5kfpNYruRBkxil3wrPhhxxw1vDOjkWfHHDy0KXFDAZy5roccLPSqcUOGEBjE8PNCi3tOsuww5eBns9+VhCV2v2a2W6WXZX7uylw8d3r09n5Jr134fshPTd+gh+jBdSxtpHt3Y/jPp2aRR2+MRRorrLytFXwppwdf8b8cNvLine truncated
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:#101e25;color:#f4f3ed;font-family:'Work Sans',sans-serif}main{max-width:1120px;margin:auto;padding:40px 36px}.eyebrow{color:#75d5c6;letter-spacing:.15em;font-size:13px;text-transform:uppercase}h1{font-size:44px;letter-spacing:-.04em;margin:12px 0}p{line-height:1.5;color:#c9d2d5}.intro{max-width:750px}.controls{display:flex;gap:10px;flex-wrap:wrap;margin:24px 0}button{font:inherit;color:inherit;border:1px solid #74868e;background:transparent;padding:11px 16px;cursor:pointer}button[aria-pressed=true]{background:#75d5c6;color:#101e25;border-color:#75d5c6}button:focus-visible,[role=button]:focus-visible{outline:3px solid #ffd17a;outline-offset:5px}svg{display:block;width:100%;height:auto;overflow:visible;background:#152831;border:1px solid #425861}.part{cursor:pointer;outline:none}.part .shell{fill:#26434e;stroke:#9ac0cb;stroke-width:2}.part[aria-pressed=true] .shell{fill:#34675f;stroke:#75d5c6;stroke-width:3}.part:hover .shell{stroke:#fff}svg text{fill:#f4f3ed;font-family:inherit;text-anchor:middle;font-size:20px}svg .hint{fill:#aec3cb;font-size:14px}.detail{border-left:3px solid #75d5c6;padding:12px 18px;min-height:83px;margin:20px 0}.detail strong{display:block;margin-bottom:5px}.foot{font-size:12px;color:#aec3cb}@media(max-width:600px){main{padding:24px 16px}h1{font-size:32px}}@media print{body{background:white;color:#101e25}.controls{display:none}main{padding:0}.detail,p,.foot{color:#101e25}}
|
||||||
|
</style></head><body><main>
|
||||||
|
<div class="eyebrow">Explanatory motion · original SVG example</div><h1>One system, three responsibilities.</h1><p class="intro">Keep the same parts in view. Separate them to explain their roles, inspect a component, then recombine the system.</p>
|
||||||
|
<div class="controls" aria-label="Assembly controls"><button id="assemble" aria-pressed="true">1 · Assemble</button><button id="explode" aria-pressed="false">2 · Explode</button><button id="inspect" aria-pressed="false">3 · Inspect</button><button id="reassemble" aria-pressed="false">4 · Reassemble</button></div>
|
||||||
|
<svg viewBox="0 0 1040 370" role="group" aria-label="Three-part service assembly">
|
||||||
|
<path d="M135 185H905" stroke="#58727c" stroke-dasharray="5 8"/><text x="520" y="40" class="hint" id="diagram-state">Assembled · a single system</text>
|
||||||
|
<g class="part" data-index="2" role="button" tabindex="0" aria-label="Inspect storage" aria-pressed="false" transform="translate(580 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><path d="M-65 -20H65M-65 0H65M-65 20H65" stroke="#75d5c6" stroke-width="4"/><text y="-55">Storage</text><text y="65" class="hint">Retain the outcome</text></g>
|
||||||
|
<g class="part" data-index="1" role="button" tabindex="0" aria-label="Inspect processing" aria-pressed="false" transform="translate(520 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><path d="M-45 0L0 -30L45 0L0 30Z" fill="none" stroke="#75d5c6" stroke-width="4"/><text y="-55">Processing</text><text y="65" class="hint">Apply the rules</text></g>
|
||||||
|
<g class="part" data-index="0" role="button" tabindex="0" aria-label="Inspect interface" aria-pressed="false" transform="translate(460 185)"><rect class="shell" x="-105" y="-100" width="210" height="200" rx="8"/><circle r="27" fill="none" stroke="#75d5c6" stroke-width="4"/><text y="-55">Interface</text><text y="65" class="hint">Receive the intent</text></g>
|
||||||
|
<text x="520" y="338" class="hint">Explode to reveal all labels · select a part for its responsibility</text></svg>
|
||||||
|
<div class="detail" aria-live="polite"><strong id="detail-title">Assembled system</strong><span id="detail-body">Three stable components share one purpose. Explode the assembly to reveal each responsibility.</span></div>
|
||||||
|
<p class="foot">Original abstract 2D assembly, not a reproduction of the Anime.js mechanism. No third-party artwork. Motion respects reduced-motion preferences. Keyboard: Tab, Enter and Space.</p>
|
||||||
|
</main><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>
|
||||||
|
'use strict';
|
||||||
|
const parts=[...document.querySelectorAll('.part')].sort((a,b)=>a.dataset.index-b.dataset.index),buttons=[...document.querySelectorAll('.controls button')],mq=matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
const info=[['Interface','Receives intent and presents the result. It does not own persistence.'],['Processing','Applies rules and coordinates the operation. It does not own the user interface.'],['Storage','Retains the outcome for later retrieval. It does not decide the business rules.']];
|
||||||
|
const model=[{x:460},{x:520},{x:580}];let animation=null,expanded=false,selected=-1;
|
||||||
|
function draw(){parts.forEach((p,i)=>p.setAttribute('transform',`translate(${model[i].x} 185)`));}
|
||||||
|
function settle(){if(animation){animation.complete();animation=null;}}
|
||||||
|
function move(open,immediate=false){settle();expanded=open;parts.forEach((p,i)=>p.querySelectorAll('text').forEach(t=>t.style.opacity=open||i===0?'1':'0'));const target=open?[200,520,840]:[460,520,580];if(mq.matches||immediate){model.forEach((m,i)=>m.x=target[i]);draw();}else{animation=anime.animate(model,{x:(_,i)=>target[i],duration:650,ease:'inOutCubic',onUpdate:draw,onComplete:()=>{animation=null;}});}document.querySelector('#diagram-state').textContent=open?'Exploded · inspect each responsibility':'Assembled · a single system';}
|
||||||
|
function activate(id){buttons.forEach(b=>b.setAttribute('aria-pressed',String(b.id===id)));}
|
||||||
|
function detail(i){selected=i;parts.forEach((p,j)=>p.setAttribute('aria-pressed',String(i===j)));document.querySelector('#detail-title').textContent=i<0?'Assembled system':info[i][0];document.querySelector('#detail-body').textContent=i<0?'Three stable components share one purpose. Explode the assembly to reveal each responsibility.':info[i][1];}
|
||||||
|
function inspect(i){activate('inspect');move(true);detail(i);}
|
||||||
|
buttons.forEach(b=>b.addEventListener('click',()=>{activate(b.id);if(b.id==='inspect'){inspect(selected<0?1:selected);}else{move(b.id==='explode');detail(-1);if(b.id==='explode'){document.querySelector('#detail-title').textContent='Exploded system';document.querySelector('#detail-body').textContent='The same components, separated without changing identity. Select any component to inspect its role.';}}}));
|
||||||
|
parts.forEach((p,i)=>{p.addEventListener('click',()=>inspect(i));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();e.stopPropagation();inspect(i);}});});
|
||||||
|
mq.addEventListener('change',settle);document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();});window.addEventListener('pagehide',settle);window.addEventListener('beforeprint',()=>{move(true,true);});move(false,true);window.mechanismQA={settle,state:()=>({expanded,selected,active:!!animation,positions:model.map(m=>m.x)})};
|
||||||
|
</script><!-- THIRD-PARTY NOTICES
|
||||||
|
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 "Software"), 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 "AS IS", 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.
|
||||||
|
|
||||||
|
Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) WorkSans-Italic[wght].ttf: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
—————————————————————————————-
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
—————————————————————————————-
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting — in part or in whole — any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
--></body></html>
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"file": "mechanism-demo-0.2.0.html",
|
||||||
|
"sha256": "558fe5c1d0a684edaf42fcbf825a586ee62a8335154ecdb34a96b42b498af3e4",
|
||||||
|
"interactive_components": 3,
|
||||||
|
"controls": 4,
|
||||||
|
"anime": "4.5.0"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"sha256": "558fe5c1d0a684edaf42fcbf825a586ee62a8335154ecdb34a96b42b498af3e4",
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"reducedMotion": "no-preference",
|
||||||
|
"components": 3,
|
||||||
|
"componentActions": 9,
|
||||||
|
"controls": 4,
|
||||||
|
"interruption": true,
|
||||||
|
"print": true,
|
||||||
|
"clipped": [],
|
||||||
|
"errors": [],
|
||||||
|
"network": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reducedMotion": "reduce",
|
||||||
|
"components": 3,
|
||||||
|
"componentActions": 9,
|
||||||
|
"controls": 4,
|
||||||
|
"interruption": true,
|
||||||
|
"print": true,
|
||||||
|
"clipped": [],
|
||||||
|
"errors": [],
|
||||||
|
"network": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Mechanism visual review
|
||||||
|
|
||||||
|
Self-review, not independent QA. Inspected assembled, exploded and selected states at 1280×900. The exploded layout has three readable components, clear spacing, a separate responsibility detail panel and visible keyboard focus. The first assembled render exposed fragments of rear labels; corrected by hiding rear text in the assembled state while preserving component identities and accessible names. Rebuilt, reran both normal/reduced-motion suites and re-inspected the corrected assembled image.
|
||||||
|
|
||||||
|
The unchanged six-slide starter also passed its full browser regression after fixing a test race: wait for both fullscreen state and the asynchronous fullscreenchange label update. This changes the assertion timing, not application behavior.
|
||||||
|
|
||||||
|
Exact mechanism artifact identity and tested scope are in mechanism-test-report.json. No remote CI, cross-browser or screen-reader certification is claimed.
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "presentation-html-skill",
|
"name": "presentation-html-skill",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {"build":"python3 scripts/build.py", "test":"npm run build && node scripts/test.mjs"},
|
"scripts": {"build":"python3 scripts/build.py", "test":"npm run build && node scripts/test.mjs && python3 scripts/build-mechanism.py && node scripts/test-mechanism.mjs"},
|
||||||
"devDependencies": {"@playwright/test":"1.64.1"},
|
"devDependencies": {"@playwright/test":"1.64.1"},
|
||||||
"dependencies": {"reveal.js":"6.0.2","animejs":"4.5.0","@fontsource/work-sans":"5.2.8"}
|
"dependencies": {"reveal.js":"6.0.2","animejs":"4.5.0","@fontsource/work-sans":"5.2.8"}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@ Inspected 2026-10-21. “Established” identifies official showcase material or
|
|||||||
| Anime.js v4 logo animation / Julian Garnier | [v4.5.0 example](https://github.com/juliangarnier/anime/tree/v4.5.0/examples/animejs-v4-logo-animation) | Timeline orchestration produces staged hierarchy | MIT code does not imply trademark endorsement. Logo not copied. Original three-step process timeline, slide 4. |
|
| Anime.js v4 logo animation / Julian Garnier | [v4.5.0 example](https://github.com/juliangarnier/anime/tree/v4.5.0/examples/animejs-v4-logo-animation) | Timeline orchestration produces staged hierarchy | MIT code does not imply trademark endorsement. Logo not copied. Original three-step process timeline, slide 4. |
|
||||||
| Inventing on Principle / Bret Victor | [author link](https://worrydream.com/InventingOnPrinciple/) redirected to [Vimeo](https://vimeo.com/906418692) | Reference candidate for direct manipulation and explaining ideas through working demonstrations | No reuse license established; link only, no video or slide copying. Only redirect/page availability was verified here, not a new full-video review. Not claimed as a Reveal/Anime implementation. |
|
| Inventing on Principle / Bret Victor | [author link](https://worrydream.com/InventingOnPrinciple/) redirected to [Vimeo](https://vimeo.com/906418692) | Reference candidate for direct manipulation and explaining ideas through working demonstrations | No reuse license established; link only, no video or slide copying. Only redirect/page availability was verified here, not a new full-video review. Not claimed as a Reveal/Anime implementation. |
|
||||||
|
|
||||||
|
## Explanatory assembly reference
|
||||||
|
|
||||||
|
Read [explanatory mechanisms](explanatory-mechanisms.md) when choosing an assemble/explode/inspect/reassemble pattern. It records the inspected Anime.js toolbox and modularity recordings, attribution and reuse limits. `assets/mechanism.html` is the original runnable counterpart; showcase artwork remains link-only.
|
||||||
|
|
||||||
## Original reusable examples in assets
|
## Original reusable examples in assets
|
||||||
|
|
||||||
`assets/starter.html` and `assets/app.js` are MIT original code, not reproductions of showcase layouts:
|
`assets/starter.html` and `assets/app.js` are MIT original code, not reproductions of showcase layouts:
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Explanatory mechanisms: assemble, explode, inspect, reassemble
|
||||||
|
|
||||||
|
## When to read this reference
|
||||||
|
|
||||||
|
Read **before storyboarding or coding** when an audience needs to understand how parts form a whole: architecture decomposition, product internals, modularity, ownership boundaries, or a user request for a mechanical/exploded-view animation. Also read when a simple stagger feels decorative rather than explanatory. Do not load it merely to add motion to a factual table.
|
||||||
|
|
||||||
|
## Evidence and source boundaries
|
||||||
|
|
||||||
|
The following Anime.js showcase recordings were inspected on 2026-10-21 via Awwwards after the live Anime.js homepage timed out. These are observations of recorded visuals, **not verification of live scroll/input behavior, implementation source, or the 3D rendering engine**.
|
||||||
|
|
||||||
|
| Reference | Observed lesson | Use when |
|
||||||
|
|---|---|---|
|
||||||
|
| [The complete animator’s toolbox](https://www.awwwards.com/inspiration/the-complete-animator-s-toolbox-anime-js) | A shaded mechanical assembly opens into an exploded arrangement and a technical drawing with callouts. Preserve object identity while changing explanatory depth. | Explain internals of a system already introduced as a whole. |
|
||||||
|
| [Lightweight and modular](https://www.awwwards.com/inspiration/lightweight-and-modular-anime-js) | Separated parts become a compact assembly beside a bundle-size display. Motion connects modular composition to an outcome. | Explain optional modules, assembly, or contribution to a total. |
|
||||||
|
| [Scroll scrubber](https://www.awwwards.com/inspiration/scroll-scrubber-anime-js) | Navigation reference listed on the showcase; live interaction was not tested in this research. | Investigate reversible progress control; verify actual input behavior separately before adopting it. |
|
||||||
|
| [Anime.js showcase](https://www.awwwards.com/sites/anime-js) | Credits Julian Garnier; records Site of the Day, May 6, 2025. | Recover attribution and the recording catalog, not implementation evidence. |
|
||||||
|
|
||||||
|
No license to redistribute the showcase artwork or recordings was established. Keep these as links and observations; do not package downloaded videos, screenshots, logo paths, or reconstructed branded art as reusable skill assets. The MIT library license is not permission to copy the showcase's artwork.
|
||||||
|
|
||||||
|
## Original reusable asset
|
||||||
|
|
||||||
|
- Editable source: [`assets/mechanism.html`](../assets/mechanism.html).
|
||||||
|
- Offline builder: [`scripts/build-mechanism.py`](../scripts/build-mechanism.py).
|
||||||
|
- Runnable output: `dist/mechanism-demo-0.2.0.html`.
|
||||||
|
- Browser acceptance suite: `scripts/test-mechanism.mjs`.
|
||||||
|
|
||||||
|
This is an **original abstract 2D SVG assembly**, not a reproduction of the photographed/shaded mechanism and not a 3D renderer. Interface, Processing and Storage keep their DOM identity as they separate and recombine. Four native buttons expose assemble → explode → inspect → reassemble. All three components support click, Enter and Space; their details explain responsibility boundaries. Original source is covered by the package MIT license. Anime.js and font notices are embedded in the build.
|
||||||
|
|
||||||
|
Use `terminal` in the copied package directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/build-mechanism.py
|
||||||
|
node scripts/test-mechanism.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse the component groups, stable coordinates, state transitions and lifecycle cleanup together—not only the animation call. Set `CHROMIUM_EXECUTABLE` if the package's Playwright browser is not installed. Never copy machine-local browser paths into the shared skill.
|
||||||
|
|
||||||
|
## Adaptation contract
|
||||||
|
|
||||||
|
1. Write the explanation first: whole → parts → responsibility → whole. Use the original assembly only where its three responsibilities actually fit the subject.
|
||||||
|
2. Maintain stable component IDs, color and relative order. Do not replace the object with an unrelated diagram mid-transition.
|
||||||
|
3. Use callouts outside moving geometry for lengthy details. Assembled overlap is intentional; exploded labels must all be readable.
|
||||||
|
4. Interruption is a supported action: settle the previous animation before starting another. Repeated clicks must converge on the last selected state.
|
||||||
|
5. Prefer explicit state buttons for a talk. If a scrubber is justified, add a keyboard-accessible range input and test reversible progress; **the included example has no scrubber**.
|
||||||
|
6. On reduced-motion changes, page departure and print, settle motion. Print expands all components. On Reveal `slidechanged`, explicitly settle the outgoing mechanism; do not let Reveal Auto-Animate and Anime own the same transform.
|
||||||
|
7. Reveal owns presentation steps and sizing; Anime coordinates part movement. The bundled mechanism is standalone so it can be studied without Reveal. When integrating, preserve the host deck's fullscreen, navigation, offline packaging and QA contract.
|
||||||
|
8. For genuine perspective, occlusion and camera orbit, select and validate a separate 3D layer. Do not claim Anime alone renders 3D from an observed video.
|
||||||
|
|
||||||
|
## Acceptance and limits
|
||||||
|
|
||||||
|
Test all four controls, all three parts by mouse/Enter/Space, intermediate motion, reverse/repeated input, reduced motion and runtime preference changes, print expansion, offline requests, console errors and exploded text bounds. Inspect assembled, exploded and selected screenshots personally. Hash the exact tested output.
|
||||||
|
|
||||||
|
The standalone demo is not a full slide deck. Its tests do not prove Reveal integration, live scrubbing, Safari/Firefox, screen-reader output, projector readability or a native 3D implementation. Run the full deck suite after embedding it.
|
||||||
@@ -32,6 +32,10 @@ Use Reveal only for slide navigation and sizing; use Anime only for bounded cont
|
|||||||
|
|
||||||
The approved prior channel deck supplied preferences, not reusable proprietary content: editable diagrams, calm motion, Work Sans, visible fullscreen, offline packaging, exhaustive node testing. Neither its fifteen slides nor its 46-node acceptance scope is imposed on future decks. The historical artifact's embedded Anime bundle identifies itself as 3.2.1 despite prior context describing 3.2.2; the new package does not reuse that bundle.
|
The approved prior channel deck supplied preferences, not reusable proprietary content: editable diagrams, calm motion, Work Sans, visible fullscreen, offline packaging, exhaustive node testing. Neither its fifteen slides nor its 46-node acceptance scope is imposed on future decks. The historical artifact's embedded Anime bundle identifies itself as 3.2.1 despite prior context describing 3.2.2; the new package does not reuse that bundle.
|
||||||
|
|
||||||
|
## Explanatory mechanism follow-up
|
||||||
|
|
||||||
|
For the subsequently inspected Awwwards recordings of the Anime.js homepage, see [explanatory mechanisms](explanatory-mechanisms.md). Read it before selecting mechanical assembly or exploded-view motion. Recorded visuals establish the explanatory sequence, not the rendering engine or live input behavior.
|
||||||
|
|
||||||
## Failed candidates and limits
|
## Failed candidates and limits
|
||||||
|
|
||||||
`revealjs.com/examples/` and `slides.com/news/introducing-auto-animate` returned 404; neither is cited as a successful source. Browser Use could not start its isolated daemon; local Playwright Chromium was used for real rendering and interaction QA instead. Anime site availability is an explicit research caveat, not hidden by invented page summaries.
|
`revealjs.com/examples/` and `slides.com/news/introducing-auto-animate` returned 404; neither is cited as a successful source. Browser Use could not start its isolated daemon; local Playwright Chromium was used for real rendering and interaction QA instead. Anime site availability is an explicit research caveat, not hidden by invented page summaries.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""Build the original assembly example offline from licensed local assets."""
|
||||||
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
for entry in json.loads((root / 'vendor-manifest.json').read_text()):
|
||||||
|
assert hashlib.sha256((root / entry['path']).read_bytes()).hexdigest() == entry['sha256']
|
||||||
|
vendor = root / 'assets/vendor'
|
||||||
|
text = (root / 'assets/mechanism.html').read_text()
|
||||||
|
js = re.sub(r'//# sourceMappingURL=.*', '', (vendor / 'anime.umd.min.js').read_text())
|
||||||
|
text = text.replace('/* ANIME_JS */', js)
|
||||||
|
fonts = ''
|
||||||
|
for file, weight in [('work-sans.woff2', 400), ('work-sans-bold.woff2', 700)]:
|
||||||
|
data = base64.b64encode((vendor / file).read_bytes()).decode()
|
||||||
|
fonts += f"@font-face{{font-family:'Work Sans';font-weight:{weight};src:url(data:font/woff2;base64,{data}) format('woff2');}}"
|
||||||
|
text = text.replace('/* FONT_CSS */', fonts)
|
||||||
|
notices = '\n'.join((vendor / f).read_text() for f in ['anime-LICENSE.md', 'work-sans-LICENSE'])
|
||||||
|
text = text.replace('<!-- LICENSES -->', '<!-- THIRD-PARTY NOTICES\n' + notices.replace('--', '—') + '\n-->')
|
||||||
|
out = root / 'dist/mechanism-demo-0.2.0.html'
|
||||||
|
out.parent.mkdir(exist_ok=True)
|
||||||
|
out.write_text(text)
|
||||||
|
report = {'file': out.name, 'sha256': hashlib.sha256(out.read_bytes()).hexdigest(), 'interactive_components': 3, 'controls': 4, 'anime': '4.5.0'}
|
||||||
|
(root / 'dist/mechanism-manifest.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||||
|
print(json.dumps(report))
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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/mechanism-demo-0.2.0.html');
|
||||||
|
const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
|
||||||
|
const report={sha256:crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),runs:[]};
|
||||||
|
fs.mkdirSync(path.join(root,'evidence'),{recursive:true});
|
||||||
|
try{for(const reducedMotion of ['no-preference','reduce']){
|
||||||
|
const ctx=await browser.newContext({offline:true,reducedMotion,viewport:{width:1280,height:900}}),p=await ctx.newPage(),errors=[],network=[];
|
||||||
|
p.on('pageerror',e=>errors.push(String(e)));p.on('console',m=>{if(m.type()==='error')errors.push(m.text());});p.on('request',r=>{if(/^https?:/.test(r.url()))network.push(r.url());});
|
||||||
|
await p.goto(pathToFileURL(file).href);await p.evaluate(()=>document.fonts.ready);
|
||||||
|
const state=()=>p.evaluate(()=>mechanismQA.state());const settled=()=>p.waitForFunction(()=>!mechanismQA.state().active);
|
||||||
|
assert.deepEqual((await state()).positions,[460,520,580]);
|
||||||
|
if(reducedMotion==='no-preference')await p.screenshot({path:path.join(root,'evidence/mechanism-assembled.png')});
|
||||||
|
await p.locator('#explode').click();if(reducedMotion==='no-preference'){await p.waitForTimeout(150);const s=await state();assert.ok(s.positions[0]<460&&s.positions[0]>200,'actual intermediate motion');}await settled();assert.deepEqual((await state()).positions,[200,520,840]);
|
||||||
|
const clipped=await p.evaluate(()=>{const r=document.querySelector('svg').getBoundingClientRect();return [...document.querySelectorAll('svg text')].filter(e=>{const b=e.getBoundingClientRect();return b.left<r.left||b.right>r.right||b.top<r.top||b.bottom>r.bottom;}).map(e=>e.textContent);});assert.deepEqual(clipped,[]);
|
||||||
|
if(reducedMotion==='no-preference')await p.screenshot({path:path.join(root,'evidence/mechanism-exploded.png')});
|
||||||
|
for(let i=0;i<3;i++){const n=p.locator(`[data-index="${i}"]`);for(const action of ['click','Enter','Space']){if(action==='click')await n.click();else{await n.focus();await p.keyboard.press(action);}await settled();assert.equal((await state()).selected,i);assert.equal(await n.getAttribute('aria-pressed'),'true');assert.equal(await p.locator('#detail-title').textContent(),['Interface','Processing','Storage'][i]);}}
|
||||||
|
if(reducedMotion==='no-preference')await p.screenshot({path:path.join(root,'evidence/mechanism-inspected.png')});
|
||||||
|
for(const id of ['reassemble','assemble']){await p.locator('#'+id).click();await settled();assert.deepEqual((await state()).positions,[460,520,580]);assert.equal((await state()).selected,-1);}
|
||||||
|
await p.locator('#inspect').focus();await p.keyboard.press('Enter');await settled();assert.equal((await state()).selected,1);
|
||||||
|
await p.evaluate(()=>{for(const id of ['assemble','explode','reassemble','explode','assemble'])document.getElementById(id).click();});await settled();assert.deepEqual((await state()).positions,[460,520,580]);
|
||||||
|
await p.locator('#explode').click();await p.emulateMedia({reducedMotion:'reduce'});await settled();assert.deepEqual((await state()).positions,[200,520,840]);
|
||||||
|
await p.locator('#assemble').click();await p.emulateMedia({media:'print'});await p.evaluate(()=>dispatchEvent(new Event('beforeprint')));assert.deepEqual((await state()).positions,[200,520,840]);assert.equal(await p.locator('.controls').isVisible(),false);
|
||||||
|
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);report.runs.push({reducedMotion,components:3,componentActions:9,controls:4,interruption:true,print:true,clipped,errors,network});await ctx.close();
|
||||||
|
}fs.writeFileSync(path.join(root,'evidence/mechanism-test-report.json'),JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report,null,2));}finally{await browser.close();}
|
||||||
+1
-1
@@ -26,7 +26,7 @@ for(const reducedMotion of ['no-preference','reduce']){
|
|||||||
}
|
}
|
||||||
assert.equal(testedNodes,6);await page.evaluate(()=>Reveal.slide(0));await page.keyboard.press('ArrowRight');assert.equal(await page.evaluate(()=>Reveal.getIndices().h),1);await page.keyboard.press('ArrowLeft');assert.equal(await page.evaluate(()=>Reveal.getIndices().h),0);
|
assert.equal(testedNodes,6);await page.evaluate(()=>Reveal.slide(0));await page.keyboard.press('ArrowRight');assert.equal(await page.evaluate(()=>Reveal.getIndices().h),1);await page.keyboard.press('ArrowLeft');assert.equal(await page.evaluate(()=>Reveal.getIndices().h),0);
|
||||||
await page.keyboard.press('Escape');assert.equal(await page.evaluate(()=>Reveal.isOverview()),true);await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');assert.equal(await page.evaluate(()=>Reveal.isOverview()),true);await page.keyboard.press('Escape');
|
||||||
await page.locator('#fullscreen').click();await page.waitForFunction(()=>!!document.fullscreenElement);assert.equal(await page.locator('#fullscreen').getAttribute('aria-pressed'),'true');await page.keyboard.press('f');await page.waitForFunction(()=>!document.fullscreenElement);
|
await page.locator('#fullscreen').click();await page.waitForFunction(()=>!!document.fullscreenElement && document.querySelector('#fullscreen').getAttribute('aria-pressed')==='true');assert.equal(await page.locator('#fullscreen').getAttribute('aria-pressed'),'true');await page.keyboard.press('f');await page.waitForFunction(()=>!document.fullscreenElement);
|
||||||
await page.keyboard.press('f');await page.waitForFunction(()=>!!document.fullscreenElement);await page.locator('#fullscreen').click();await page.waitForFunction(()=>!document.fullscreenElement);
|
await page.keyboard.press('f');await page.waitForFunction(()=>!!document.fullscreenElement);await page.locator('#fullscreen').click();await page.waitForFunction(()=>!document.fullscreenElement);
|
||||||
await page.evaluate(()=>Reveal.slide(3));await page.waitForFunction(()=>location.hash.includes('sequence'));await page.reload();await page.waitForFunction(()=>window.deckReady);assert.equal(await page.evaluate(()=>Reveal.getIndices().h),3);
|
await page.evaluate(()=>Reveal.slide(3));await page.waitForFunction(()=>location.hash.includes('sequence'));await page.reload();await page.waitForFunction(()=>window.deckReady);assert.equal(await page.evaluate(()=>Reveal.getIndices().h),3);
|
||||||
if(reducedMotion==='reduce'){assert.equal(await page.evaluate(()=>presentationQA.active()),0);assert.equal(await page.locator('#motion').getAttribute('aria-pressed'),'false');}
|
if(reducedMotion==='reduce'){assert.equal(await page.evaluate(()=>presentationQA.active()),0);assert.equal(await page.locator('#motion').getAttribute('aria-pressed'),'false');}
|
||||||
|
|||||||
Reference in new issue
Block a user