diff --git a/SKILL.md b/SKILL.md
index ae8ed6c..ef8eaae 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,7 +1,7 @@
---
name: presentation-html
description: Build offline HTML decks with deliberate motion.
-version: 0.2.0
+version: 0.3.0
author: RootAtSkic, Hermes Agent
license: MIT
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.
- Optional design/review skills may supplement this workflow; channel instructions remain channel-owned.
+## Read on demand: choreography and deterministic motion
+
+Before adapting animation recipes, read [choreography](references/choreography.md): motion briefs, named timeline beats, paused ownership, deterministic seeking, SVG drawing/motion paths and spatial stagger. The starter exposes seek/replay/settle for authoring and QA without adding presentation controls. [Catalog patterns](references/catalog-patterns.md) records the three inspected `animejs` skills, source pins, license limits and rejected v3 examples. Tests exercise our original v4 equivalents; upstream snippets are not automatically compatible.
+
## 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.
@@ -47,7 +51,7 @@ npm test
The builder works entirely offline using vendored assets. Dependency/browser installation requires network once; deck playback and build do not. If a compatible Chromium is already installed, set `CHROMIUM_EXECUTABLE` to its actual executable when invoking `npm test`. Do not silently download a new runtime during an offline test.
-Open `dist/presentation-html-demo-0.1.0.html` directly. The six-slide demo is a teaching example, not a mandated length. Source: `assets/starter.html`, `assets/app.js`, `scripts/build.py`. Tests: `scripts/test.mjs`.
+Open `dist/presentation-html-demo-0.3.0.html` directly. The six-slide demo is a teaching example, not a mandated length. Source: `assets/starter.html`, `assets/app.js`, `scripts/build.py`. Tests: `scripts/test.mjs`.
## Procedure
diff --git a/assets/app.js b/assets/app.js
index ef21511..0ad2843 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -1,22 +1,42 @@
(() => {
- const {animate, createTimeline, stagger} = anime;
+ const {createTimeline, stagger} = anime;
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
const printMode = location.search.includes('print-pdf');
- const seen = new Set(); let active = []; let motion = !reduced.matches;
+ const seen = new Set(), timelines = new Map(); let motion = !reduced.matches;
const motionButton = document.querySelector('#motion');
+ // Explicit paused instances: Reveal owns slide navigation; one clock owns motion.
+ // Targets are dedicated motion wrappers, not positioned/rotated diagram parts.
+ document.querySelectorAll('.slides>section').forEach(slide=>{
+ const targets=[...slide.querySelectorAll('[data-motion]')];
+ if(!targets.length)return;
+ const tl=createTimeline({autoplay:false,defaults:{duration:300,ease:'outCubic'}});
+ if(slide.id==='sequence') {
+ ['observe','interpret','act'].forEach((label,i)=>{
+ tl.label(label,i*180).add(targets[i],{opacity:[0,1],translateY:[12,0]},label);
+ });
+ } else tl.add(targets,{opacity:[0,1],translateY:[12,0],delay:stagger(70),duration:400},0);
+ tl.seek(tl.duration);tl.pause();timelines.set(slide,tl);
+ });
+ const allowed=()=>motion&&!reduced.matches&&!printMode&&!Reveal.isOverview();
function settle() {
- active.forEach(a => { a.complete(); }); active = [];
- document.querySelectorAll('[data-motion]').forEach(e => {e.style.opacity='1';e.style.transform='none';});
+ timelines.forEach(tl=>{tl.pause();tl.seek(tl.duration);});
+ }
+ function replay() {
+ settle();const tl=timelines.get(Reveal.getCurrentSlide());
+ if(!tl||!allowed())return false;
+ tl.seek(0);tl.play();return true;
+ }
+ function seek(timeMs) {
+ if(!Number.isFinite(timeMs))throw new TypeError('seek requires finite milliseconds');
+ const tl=timelines.get(Reveal.getCurrentSlide());
+ if(!tl)return false;
+ tl.pause();tl.seek(allowed()?Math.max(0,Math.min(timeMs,tl.duration)):tl.duration);
+ return true;
}
function run() {
- settle(); const slide=Reveal.getCurrentSlide();
- if (!slide || seen.has(slide.id) || !motion || reduced.matches || printMode) return;
- seen.add(slide.id); const targets=slide.querySelectorAll('[data-motion]');
- if (!targets.length) return;
- if(slide.id==='sequence') {
- const tl=createTimeline({defaults:{duration:300,ease:'outCubic'}});
- targets.forEach((e,i)=>tl.add(e,{opacity:[0,1],translateY:[12,0]},i*180));active.push(tl);
- } else active.push(animate(targets,{opacity:[0,1],translateY:[12,0],delay:stagger(70),duration:400,ease:'outCubic'}));
+ settle();const slide=Reveal.getCurrentSlide();
+ if(!slide||seen.has(slide.id)||!allowed())return;
+ seen.add(slide.id);replay();
}
function motionLabel(){motionButton.textContent=motion&&!reduced.matches?'Motion on':'Motion off';motionButton.setAttribute('aria-pressed',String(motion&&!reduced.matches));}
motionButton.onclick=()=>{motion=!motion;settle();motionLabel();};
@@ -37,5 +57,7 @@
run(); window.deckReady=true;
});
Reveal.on('slidechanged',()=>{try{localStorage.setItem('presentation-html-demo-slide',String(Reveal.getIndices().h));}catch{}run();});
- window.presentationQA={settle,reduced:()=>reduced.matches,active:()=>active.filter(a=>!a.completed).length};
+ Reveal.on('overviewshown',settle);
+ document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();});
+ window.presentationQA={settle,seek,replay,reduced:()=>reduced.matches,active:()=>[...timelines.values()].filter(tl=>!tl.paused&&!tl.completed).length};
})();
diff --git a/dist/manifest.json b/dist/manifest.json
index de77b05..89f0caa 100644
--- a/dist/manifest.json
+++ b/dist/manifest.json
@@ -1,7 +1,7 @@
{
- "file": "presentation-html-demo-0.1.0.html",
- "sha256": "c81f4f77250ee50973c97d12d22172d9e2cd6f97a1e699e960b491a0aa1581b0",
- "bytes": 362988,
+ "file": "presentation-html-demo-0.3.0.html",
+ "sha256": "f9e7ea2a327ea85c34db5c90332633a77bca647d256223918c6d79f1a0723015",
+ "bytes": 363913,
"slides": 6,
"interactive_nodes": 6,
"reveal": "6.0.2",
diff --git a/dist/presentation-html-demo-0.3.0.html b/dist/presentation-html-demo-0.3.0.html
new file mode 100644
index 0000000..4127f0b
--- /dev/null
+++ b/dist/presentation-html-demo-0.3.0.html
@@ -0,0 +1,262 @@
+
+
From signal to decision — offline HTML starter
+
+
+
+
Original example · presentation-html
From signal
to decision.
Use structure to explain the system.
Use motion only to explain change.
Reveal.js 6.0.2 + Anime.js 4.5.0 · All assets embedded
+
01 / Narrative
One question. One visual answer.
ContextWhat changed, and why does it matter?
MechanismWhat connects the evidence to the choice?
DecisionWhat should the audience do next?
Choose the slide count from the story—not a template quota.
+
02 / Editable SVG + real interaction
Keep the decision at the center.
+Select a source to reveal its role. Tab + Enter or Space also works.
+
03 / Anime.js timeline
Reveal dependency—not decoration.
ObserveMake the signal visible.
InterpretConnect evidence and context.
ActName the next reversible step.
A short, bounded sequence. No loops. No hidden final content.
+
04 / Restrained stagger
Make order perceptible.
The pattern is original. The technique is portable.
Reduced motion, print and revisit states show the complete result.
+
05 / Delivery gate
Test the exact file.
Inspect every slide.
Offline. Readable. Keyboard-operable.
Then publish the immutable artifact.
← → Navigate · Esc Overview · F Fullscreen · Tab Focus
+
+
+
diff --git a/evidence/motion-test-report.json b/evidence/motion-test-report.json
new file mode 100644
index 0000000..648d446
--- /dev/null
+++ b/evidence/motion-test-report.json
@@ -0,0 +1,18 @@
+{
+ "passed": true,
+ "checks": [
+ "deterministic seek",
+ "finite input",
+ "replay",
+ "departure",
+ "revisit",
+ "overview",
+ "reduced motion",
+ "print",
+ "SVG draw",
+ "SVG motion path",
+ "grid stagger"
+ ],
+ "errors": [],
+ "requests": []
+}
diff --git a/evidence/slide-01.png b/evidence/slide-01.png
index beeb39f..ceb6d29 100644
Binary files a/evidence/slide-01.png and b/evidence/slide-01.png differ
diff --git a/evidence/slide-02.png b/evidence/slide-02.png
index e4319f1..ea1e576 100644
Binary files a/evidence/slide-02.png and b/evidence/slide-02.png differ
diff --git a/evidence/slide-03.png b/evidence/slide-03.png
index e269b16..946cf1f 100644
Binary files a/evidence/slide-03.png and b/evidence/slide-03.png differ
diff --git a/evidence/slide-04.png b/evidence/slide-04.png
index fc9fd8e..2b4bdd4 100644
Binary files a/evidence/slide-04.png and b/evidence/slide-04.png differ
diff --git a/evidence/slide-05.png b/evidence/slide-05.png
index 67b849b..41a8ed6 100644
Binary files a/evidence/slide-05.png and b/evidence/slide-05.png differ
diff --git a/evidence/slide-06.png b/evidence/slide-06.png
index 18e2856..1b2c80a 100644
Binary files a/evidence/slide-06.png and b/evidence/slide-06.png differ
diff --git a/evidence/test-report.json b/evidence/test-report.json
index 8eb775c..4c56185 100644
--- a/evidence/test-report.json
+++ b/evidence/test-report.json
@@ -1,6 +1,6 @@
{
- "file": "/opt/data/work/presentation-html-skill/dist/presentation-html-demo-0.1.0.html",
- "sha256": "c81f4f77250ee50973c97d12d22172d9e2cd6f97a1e699e960b491a0aa1581b0",
+ "file": "/opt/data/channels/skills-public/repositories/presentation-html/dist/presentation-html-demo-0.3.0.html",
+ "sha256": "f9e7ea2a327ea85c34db5c90332633a77bca647d256223918c6d79f1a0723015",
"runs": [
{
"reducedMotion": "no-preference",
diff --git a/package-lock.json b/package-lock.json
index 50c2276..9f93ac8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "presentation-html-skill",
- "version": "0.1.0",
+ "version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "presentation-html-skill",
- "version": "0.1.0",
+ "version": "0.3.0",
"dependencies": {
"@fontsource/work-sans": "5.2.8",
"animejs": "4.5.0",
diff --git a/package.json b/package.json
index fdefe1e..98005ab 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,17 @@
{
"name": "presentation-html-skill",
- "version": "0.1.0",
+ "version": "0.3.0",
"private": true,
- "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"},
- "dependencies": {"reveal.js":"6.0.2","animejs":"4.5.0","@fontsource/work-sans":"5.2.8"}
+ "scripts": {
+ "build": "python3 scripts/build.py",
+ "test": "npm run build && node scripts/test.mjs && node scripts/test-motion.mjs && python3 scripts/build-mechanism.py && node scripts/test-mechanism.mjs"
+ },
+ "devDependencies": {
+ "@playwright/test": "1.64.1"
+ },
+ "dependencies": {
+ "reveal.js": "6.0.2",
+ "animejs": "4.5.0",
+ "@fontsource/work-sans": "5.2.8"
+ }
}
diff --git a/references/catalog-patterns.md b/references/catalog-patterns.md
new file mode 100644
index 0000000..858115c
--- /dev/null
+++ b/references/catalog-patterns.md
@@ -0,0 +1,29 @@
+# Catalog patterns: adopt behavior, not incompatible code
+
+The Hermes Skills Hub search field returned **three** results for `animejs` on 2026-10-23. All three source packages were downloaded and inspected locally. These are source-inspection verdicts, not claims that the peer applications were executed.
+
+| Source | Relative verdict | Useful advantage | Limitation vs presentation-html |
+|---|---|---|---|
+| presentation-html baseline 563aaf4 | Reference | Offline Reveal + pinned Anime v4, accessible nodes, mechanism demo and browser QA | Entry timelines were disposable; no deterministic seek/replay regression suite or compact SVG/grid recipes |
+| [ClawHub animejs 1.0.0](https://clawhub.ai/skills/animejs) | Specialist: stronger clock contract | Explicit paused-instance registry and externally driven seeking | HyperFrames-specific; basic/timeline snippets use v3 `anime()` / `anime.timeline()` despite a v4 CDN URL; no portable Reveal deck |
+| [freshtechbro/claudedesignskills](https://github.com/freshtechbro/claudedesignskills/tree/1da73febff0c3e1dfefc07f8a5ef8f7d1dfdb6cd/plugins/individual/animejs/skills/animejs) | Specialist: broader choreography recipes | Timeline presets and spatial stagger guidance | Skill and Python timeline generator emit legacy v3 syntax; web hero/modal/loader presets are not a presentation narrative |
+| [bowtiedswan/animejs-skills](https://github.com/bowtiedswan/animejs-skills/tree/cf2bed8d6654a6cfd02d3ff11b74ff3a7a7631e2) | Specialist: broader v4 API reference | SVG drawing/motion paths, grid stagger, scope lifecycle and seek API coverage | Not an offline deck builder; unpinned CDN examples and some namespace claims must be checked against the installed UMD runtime |
+
+## Adopted capabilities
+
+| Capability | Best evidence | Native destination | Excluded | Acceptance gate |
+|---|---|---|---|---|
+| Explicit animation ownership and deterministic seeking | ClawHub SKILL.md contract | assets/app.js: paused timeline Map, seek/replay/settle | HyperFrames registry/runtime, GSAP replacement and wall-clock callbacks | Forward → backward → same-time DOM equality; active clock paused |
+| Meaningful named beats | Freshtechbro timeline guidance and timeline_builder.py | assets/app.js sequence labels; references/choreography.md | v3 generated code, modal/loader choreography as deck default | Real v4 playback plus replay/departure/revisit tests |
+| Vector flow and spatial order recipes | Bowtiedswan SKILL.md and references/examples.md | references/choreography.md + scripts/test-motion.mjs | Unpinned CDN, scroll-driven slide navigation, perpetual waves | SVG draw/path and six-cell grid execute offline against vendored 4.5.0 |
+| Cleanup under state changes | Explicit-clock contract plus reference lifecycle gaps | assets/app.js overview/visibility settling | Global engine pause affecting unrelated scenes; blanket transform removal | Overview/reduced-motion/print regression checks; final state retained |
+
+Reveal.js 6.0.2, Anime.js 4.5.0, the dependency-free Python builder, native DOM/SVG and single-file offline delivery remain unchanged. No new runtime dependencies. Existing assembly/inspection behavior is retained rather than replaced. Accessible motion remains an enhancement; source availability does not establish a missing capability or justify removing existing QA.
+
+## Provenance and reuse
+
+- ClawHub source archive: `https://clawhub.ai/api/v1/download?slug=animejs&version=1.0.0`; package includes SKILL.md, skill-card.md and _meta.json. Registry metadata declares MIT-0; no license file was included. SKILL.md SHA-256: `1ca46135880d2475c7149d5e54fc41ce3d85d63809fddfdb4f4ded649120f8de`.
+- Freshtechbro commit: `1da73febff0c3e1dfefc07f8a5ef8f7d1dfdb6cd`; repository LICENSE is MIT (2025 Claude Skills Project). Inspected package path: `plugins/individual/animejs/skills/animejs`; bundled copies are not counted as additional independent skills.
+- Bowtiedswan commit: `cf2bed8d6654a6cfd02d3ff11b74ff3a7a7631e2`; no root LICENSE file was present. Treat as inspection-only, not permission to redistribute its package.
+
+No peer source files, generators, artwork or installation scripts are redistributed or run. New examples are original implementations using the already-licensed Anime API. Runtime tests establish our recipes' compatibility, not correctness of every upstream example. HyperFrames itself was not installed or validated. Morphing, drag/scroll interaction and split text remain optional techniques, not newly claimed working features.
diff --git a/references/choreography.md b/references/choreography.md
new file mode 100644
index 0000000..9238426
--- /dev/null
+++ b/references/choreography.md
@@ -0,0 +1,60 @@
+# Anime v4 choreography recipes
+
+Use these patterns to explain causality, spatial order or state change. Keep Reveal for navigation and the pinned Anime UMD bundle for motion. These are original examples, not copied peer implementations. Read this before adapting catalog examples with `anime(...)`, `anime.timeline(...)`, `easing`, `anime.path` or `anime.setDashoffset`: those are v3 APIs, not our v4 contract.
+
+## Motion brief before code
+
+For each slide record: stable objects, meaningful change, named beats, final readable state, previous/next behavior, and reduced-motion equivalent. Motion must carry information, not merely fade in a panel. A dependency can draw from cause to effect; a comparison can preserve object identity; an assembly can separate and rejoin parts. Keep a plain title legible while its explanatory diagram changes. Do not impose the sample's three beats or six slides on a real deck.
+
+## One clock, explicit instances
+
+Create timelines synchronously with `autoplay:false`. Register them by slide; initialize inactive slides to their final state. Start only the active slide's timeline. Avoid randomness, asynchronous DOM mutations and side-effect callbacks in seekable scenes. Wait for fonts/assets before constructing geometry-sensitive timelines. Layout changes require rebuilding that geometry, not assuming cached path measurements remain correct.
+
+The starter provides `presentationQA.seek(milliseconds)`, `replay()`, `settle()` and `active()`. Seeking pauses, clamps to the finite timeline and must reproduce the same DOM state after forward/backward seeks. Replay is explicit: ordinary revisits remain complete. Reduced motion and overview reject replay and force the final state. Departure, overview, page hiding and print settle timelines. The API is a QA/authoring surface, not a request to bolt extra controls onto the delivered presentation.
+
+Use dedicated motion wrappers. Do not overwrite a diagram part's structural `transform` or let Reveal Auto-Animate own the same properties. For an SPA/embedded deck, additionally detach event listeners and call `revert()` on its owned timelines when unmounting; the single-document starter does not implement component mounting.
+
+## Semantic timeline positions
+
+```js
+const tl = anime.createTimeline({autoplay:false,
+ defaults:{duration:300, ease:'outCubic'}});
+tl.label('cause', 0)
+ .add(cause, {opacity:[0,1]}, 'cause')
+ .label('effect', 240)
+ .add(effect, {opacity:[0,1]}, 'effect');
+```
+
+Labels express narrative beats; explicit overlap is a decision, not arbitrary delay accumulation. Keep descriptions in normal HTML; never make an essential conclusion available only during a transition.
+
+## SVG connections and motion paths
+
+Use the UMD `anime.svg` namespace. Keep the path in an SVG coordinate space shared with the animated marker. A marker starting at the path origin avoids accidental double offsets.
+
+```js
+const edge = document.querySelector('#flow-path');
+const trace = anime.animate(anime.svg.createDrawable(edge), {
+ draw:['0 0','0 1'], duration:600, ease:'linear', autoplay:false
+});
+const flow = anime.animate(document.querySelector('#packet'), {
+ ...anime.svg.createMotionPath(edge), duration:600,
+ ease:'linear', autoplay:false
+});
+```
+
+Register **both** instances with the owner; settle them to full duration on departure/reduced motion/print. Animate a flow marker only when travel encodes direction or latency. Do not animate essential labels away. Avoid arbitrary path morphs: establish corresponding shapes, direction and final labels first. No automatic conversion of incompatible v3 `d` interpolation snippets.
+
+## Spatial stagger
+
+```js
+const pulse = anime.animate(cells, {
+ opacity:[0.25,1], delay:anime.stagger(60,{grid:[3,2],from:'center'}),
+ duration:240, ease:'linear', autoplay:false
+});
+```
+
+The grid is `[columns, rows]`; supply targets in row-major visual order and match `columns * rows` to the actual node count. Spatial order must mean something (e.g. propagation from an origin), not decorate a list. Choose opacity/transform rather than animating layout width/height across many nodes. Restore full readability when motion is disabled.
+
+## Acceptance
+
+Run `npm test` through `terminal` in the copied package. `scripts/test-motion.mjs` covers deterministic seeking, replay, departure/revisit, overview, live reduced-motion changes, print, no network and SVG/grid primitives against the actual vendored v4 bundle. Inspect meaningful intermediate frames as well as final screenshots. Library existence is not compatibility proof; keep these checks when upgrading pins. Browser-based headless checks do not establish screen-reader, other-browser or performance acceptance.
diff --git a/scripts/build.py b/scripts/build.py
index d03e513..28f045d 100644
--- a/scripts/build.py
+++ b/scripts/build.py
@@ -24,7 +24,7 @@ def build():
s=s.replace('/* FONT_CSS */',fonts).replace('/* APP_JS */',(R/'assets/app.js').read_text())
licenses='\n'.join((vendor/f).read_text() for f in ['reveal-LICENSE','anime-LICENSE.md','work-sans-LICENSE'])
s=s.replace('','')
- out=R/'dist';out.mkdir(exist_ok=True);p=out/'presentation-html-demo-0.1.0.html';p.write_text(s)
+ out=R/'dist';out.mkdir(exist_ok=True);p=out/'presentation-html-demo-0.3.0.html';p.write_text(s)
report={'file':p.name,'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'bytes':p.stat().st_size,'slides':6,'interactive_nodes':6,'reveal':'6.0.2','anime':'4.5.0'}
(out/'manifest.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report))
if __name__=='__main__':build()
diff --git a/scripts/test-motion.mjs b/scripts/test-motion.mjs
new file mode 100644
index 0000000..d8dbb42
--- /dev/null
+++ b/scripts/test-motion.mjs
@@ -0,0 +1,61 @@
+import { chromium } from '@playwright/test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+const root=path.resolve(import.meta.dirname,'..');
+const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
+try {
+ const context=await browser.newContext({offline:true});
+ const page=await context.newPage();const errors=[],requests=[];
+ page.on('pageerror',e=>errors.push(String(e)));
+ page.on('request',r=>{if(/^https?:/.test(r.url()))requests.push(r.url());});
+ await page.goto(pathToFileURL(path.join(root,'dist/presentation-html-demo-0.3.0.html')).href);
+ await page.waitForFunction(()=>window.deckReady);
+ assert.equal(await page.evaluate(()=>typeof presentationQA.seek),'function');
+ await page.evaluate(()=>Reveal.slide(3));
+ const sample=()=>page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].map(e=>({opacity:e.style.opacity,transform:e.style.transform})));
+ await page.evaluate(()=>presentationQA.seek(240)); const first=await sample();
+ await page.evaluate(()=>presentationQA.seek(600));await page.evaluate(()=>presentationQA.seek(240));
+ assert.deepEqual(await sample(),first,'backward seek is deterministic');
+ assert.equal(await page.evaluate(()=>presentationQA.active()),0,'seek pauses the clock');
+ await page.evaluate(()=>presentationQA.seek(Infinity)).then(()=>assert.fail('nonfinite seek accepted'),()=>{});
+ await page.evaluate(()=>presentationQA.replay());
+ assert.equal(await page.evaluate(()=>presentationQA.active()),1);
+ await page.evaluate(()=>Reveal.slide(4));
+ assert.equal(await page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].every(e=>+getComputedStyle(e).opacity===1)),true,'departure settles previous slide');
+ await page.evaluate(()=>Reveal.slide(3));assert.equal(await page.evaluate(()=>presentationQA.active()),0,'revisit is complete');
+ await page.evaluate(()=>presentationQA.replay());await page.keyboard.press('Escape');
+ assert.equal(await page.evaluate(()=>presentationQA.active()),0,'overview settles');await page.keyboard.press('Escape');
+ await page.evaluate(()=>presentationQA.replay());await page.emulateMedia({reducedMotion:'reduce'});
+ await page.waitForFunction(()=>presentationQA.active()===0 && document.querySelector('#motion').getAttribute('aria-pressed')==='false');
+ assert.equal(await page.evaluate(()=>presentationQA.active()),0);
+ assert.equal(await page.evaluate(()=>presentationQA.replay()),false,'reduced motion forbids replay');
+ await page.evaluate(()=>presentationQA.seek(0));
+ assert.equal(await page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].every(e=>+getComputedStyle(e).opacity===1)),true,'seek respects reduced motion');
+ await page.emulateMedia({reducedMotion:'no-preference'});
+ await page.evaluate(()=>presentationQA.replay());await page.evaluate(()=>dispatchEvent(new Event('beforeprint')));
+ assert.equal(await page.evaluate(()=>presentationQA.active()),0);
+ await page.setContent(''+ '•'.repeat(6)+'
');
+ await page.addScriptTag({path:path.join(root,'assets/vendor/anime.umd.min.js')});
+ const primitives=await page.evaluate(()=>{
+ const edge=document.querySelector('#flow-path');
+ const trace=anime.animate(anime.svg.createDrawable(edge),{draw:['0 0','0 1'],duration:600,ease:'linear',autoplay:false});
+ const flow=anime.animate(document.querySelector('#packet'),{...anime.svg.createMotionPath(edge),duration:600,ease:'linear',autoplay:false});
+ const pulse=anime.animate(document.querySelectorAll('#cells i'),{opacity:[.25,1],delay:anime.stagger(60,{grid:[3,2],from:'center'}),duration:240,ease:'linear',autoplay:false});
+ const snapshot=()=>[edge.getAttribute('stroke-dasharray'),edge.getAttribute('stroke-dashoffset'),document.querySelector('#packet').getAttribute('style'),...Array.from(document.querySelectorAll('#cells i'),e=>e.style.opacity)];
+ [trace,flow,pulse].forEach(a=>a.seek(200));const first=snapshot();
+ [trace,flow,pulse].forEach(a=>a.seek(a.duration));const end=snapshot();
+ [trace,flow,pulse].forEach(a=>a.seek(200));const second=snapshot();
+ const x=new DOMMatrix(getComputedStyle(document.querySelector('#packet')).transform).m41;
+ return {first,end,second,x,paused:[trace,flow,pulse].every(a=>a.paused)};
+ });
+ assert.deepEqual(primitives.first,primitives.second);assert.notDeepEqual(primitives.first,primitives.end);
+ assert.notDeepEqual(primitives.first.slice(0,2),primitives.end.slice(0,2),'SVG draw changes dash attributes');
+ assert.ok(primitives.first.slice(3).some(v=>+v<1),'grid has intermediate opacity');
+ assert.ok(primitives.end.slice(3).every(v=>+v===1),'grid final state is visible');
+ assert.equal(primitives.paused,true);assert.ok(primitives.x>50&&primitives.x<80,'motion path reaches one-third of 200px path: '+JSON.stringify(primitives));
+ assert.deepEqual(errors,[]);assert.deepEqual(requests,[]);
+ fs.writeFileSync(path.join(root,'evidence/motion-test-report.json'),JSON.stringify({passed:true,checks:['deterministic seek','finite input','replay','departure','revisit','overview','reduced motion','print','SVG draw','SVG motion path','grid stagger'],errors,requests},null,2)+'\n');
+ console.log('Motion lifecycle and v4 SVG/grid recipes: PASS');
+} finally { await browser.close(); }
diff --git a/scripts/test.mjs b/scripts/test.mjs
index 91465db..66bf3ee 100644
--- a/scripts/test.mjs
+++ b/scripts/test.mjs
@@ -1,6 +1,6 @@
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,'..');const file=process.env.DECK_PATH||path.join(root,'dist/presentation-html-demo-0.1.0.html');
+const root=path.resolve(import.meta.dirname,'..');const file=process.env.DECK_PATH||path.join(root,'dist/presentation-html-demo-0.3.0.html');
const executablePath=process.env.CHROMIUM_EXECUTABLE;
const browser=await chromium.launch({headless:true,...(executablePath?{executablePath}:{})});
const report={file,sha256:crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),runs:[]};