feat: add verified deterministic Anime v4 choreography

This commit is contained in:
jarvis-at-skic committed 2026-10-23 09:40:36 +00:00
1 parent 563aaf4757
commit 6854138486
19 files changed
+493 -28

No files matched your search

+6 -2
View File
@@ -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
+35 -13
View File
@@ -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};
})();
+3 -3
View File
@@ -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",
+262
View File
@@ -0,0 +1,262 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>From signal to decision — offline HTML starter</title>
<style>.reveal .stretch,.reveal .r-stretch{max-width:none;max-height:none}.reveal pre.stretch code,.reveal pre.r-stretch code{box-sizing:border-box;height:100%;max-height:100%}.reveal .r-fit-text{white-space:nowrap;display:inline-block}.reveal .r-stack{grid-template-rows:100%;display:grid}.reveal .r-stack>*{grid-area:1/1;margin:auto}.reveal .r-vstack,.reveal .r-hstack{display:flex}.reveal .r-vstack img,.reveal .r-vstack video,.reveal .r-hstack img,.reveal .r-hstack video{object-fit:contain;min-width:0;min-height:0}.reveal .r-vstack{flex-direction:column;justify-content:center;align-items:center}.reveal .r-hstack{flex-direction:row;justify-content:center;align-items:center}.reveal .items-stretch{align-items:stretch}.reveal .items-start{align-items:flex-start}.reveal .items-center{align-items:center}.reveal .items-end{align-items:flex-end}.reveal .justify-between{justify-content:space-between}.reveal .justify-around{justify-content:space-around}.reveal .justify-start{justify-content:flex-start}.reveal .justify-center{justify-content:center}.reveal .justify-end{justify-content:flex-end}html.reveal-full-page{width:100%;height:100vh;height:calc(var(--vh,1vh) * 100);height:100dvh;overflow:hidden}.reveal-viewport{color:#000;--r-controls-spacing:12px;--r-overlay-header-height:40px;--r-overlay-margin:0px;--r-overlay-padding:6px;--r-overlay-gap:5px;background-color:#fff;height:100%;margin:0;line-height:1;position:relative;overflow:hidden}@media screen and (width<=1024px),(height<=768px){.reveal-viewport{--r-overlay-header-height:26px}}.reveal-viewport:fullscreen{width:100%!important;height:100%!important;top:0!important;left:0!important;transform:none!important}.reveal .fragment{transition:all .2s}.reveal .fragment:not(.custom){opacity:0;visibility:hidden;will-change:opacity}.reveal .fragment.visible{opacity:1;visibility:inherit}.reveal .fragment.disabled{transition:none}.reveal .fragment.grow{opacity:1;visibility:inherit}.reveal .fragment.grow.visible{transform:scale(1.3)}.reveal .fragment.shrink{opacity:1;visibility:inherit}.reveal .fragment.shrink.visible{transform:scale(.7)}.reveal .fragment.zoom-in{transform:scale(.1)}.reveal .fragment.zoom-in.visible{transform:none}.reveal .fragment.fade-out{opacity:1;visibility:inherit}.reveal .fragment.fade-out.visible{opacity:0;visibility:hidden}.reveal .fragment.semi-fade-out{opacity:1;visibility:inherit}.reveal .fragment.semi-fade-out.visible{opacity:.5;visibility:inherit}.reveal .fragment.strike{opacity:1;visibility:inherit}.reveal .fragment.strike.visible{text-decoration:line-through}.reveal .fragment.fade-up{transform:translateY(40px)}.reveal .fragment.fade-up.visible{transform:translate(0)}.reveal .fragment.fade-down{transform:translateY(-40px)}.reveal .fragment.fade-down.visible{transform:translate(0)}.reveal .fragment.fade-right{transform:translate(-40px)}.reveal .fragment.fade-right.visible{transform:translate(0)}.reveal .fragment.fade-left{transform:translate(40px)}.reveal .fragment.fade-left.visible{transform:translate(0)}.reveal .fragment.fade-in-then-out,.reveal .fragment.current-visible{opacity:0;visibility:hidden}.reveal .fragment.fade-in-then-out.current-fragment,.reveal .fragment.current-visible.current-fragment{opacity:1;visibility:inherit}.reveal .fragment.fade-in-then-semi-out{opacity:0;visibility:hidden}.reveal .fragment.fade-in-then-semi-out.visible{opacity:.5;visibility:inherit}.reveal .fragment.fade-in-then-semi-out.current-fragment,.reveal .fragment.highlight-red,.reveal .fragment.highlight-current-red,.reveal .fragment.highlight-green,.reveal .fragment.highlight-current-green,.reveal .fragment.highlight-blue,.reveal .fragment.highlight-current-blue{opacity:1;visibility:inherit}.reveal .fragment.highlight-red.visible{color:#ff2c2d}.reveal .fragment.highlight-green.visible{color:#17ff2e}.reveal .fragment.highlight-blue.visible{color:#1b91ff}.reveal .fragment.highlight-current-red.current-fragment{color:#ff2c2d}.reveal .fragment.highlight-current-green.current-fragment{color:#17ff2e}.reveal .fragment.highlight-current-blue.current-fragment{color:#1b91ff}.reveal:after{content:"";font-style:italic}.reveal iframe{z-index:1}.reveal a{position:relative}@keyframes bounce-right{0%,10%,25%,40%,50%{transform:translate(0)}20%{transform:translate(10px)}30%{transform:translate(-5px)}}@keyframes bounce-left{0%,10%,25%,40%,50%{transform:translate(0)}20%{transform:translate(-10px)}30%{transform:translate(5px)}}@keyframes bounce-down{0%,10%,25%,40%,50%{transform:translateY(0)}20%{transform:translateY(10px)}30%{transform:translateY(-5px)}}.reveal .controls{top:auto;bottom:var(--r-controls-spacing);right:var(--r-controls-spacing);z-index:11;color:#000;pointer-events:none;font-size:10px;display:none;position:absolute;left:auto}.reveal .controls button{cursor:pointer;color:currentColor;z-index:2;pointer-events:auto;font-size:inherit;visibility:hidden;opacity:0;-webkit-appearance:none;-webkit-tap-highlight-color:#0000;background-color:#0000;border:0;outline:0;padding:0;trLine truncated
</style><style>
@font-face{font-family:'Work Sans';font-weight:400;font-style:normal;font-display:block;src:url(data:font/woff2;base64,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 truncated
:root{--r-background-color:#101e25;--r-main-color:#f4f3ed;--ink:#f4f3ed;--muted:#b8c4c7;--bg:#101e25;--accent:#c5f27a;--line:#57717c}*{box-sizing:border-box}body.reveal-viewport{background:var(--bg);color:var(--ink)}.reveal{font-family:'Work Sans',sans-serif;font-size:28px}.reveal .slides{text-align:left}.reveal .slides>section{height:700px;padding:45px 48px}.eyebrow{color:var(--accent);font-size:20px;letter-spacing:.15em;text-transform:uppercase;margin:0 0 24px}h1{font-size:86px;line-height:1.05;letter-spacing:-.045em;margin:24px 0}h2{font-size:55px;line-height:1.1;margin:0 0 30px;letter-spacing:-.025em}p{line-height:1.5;max-width:1000px}.muted{color:var(--muted)}.big{font-size:38px;max-width:900px}.accent{color:var(--accent)}.rule{border-top:2px solid var(--line);padding-top:20px;margin-top:42px}button{font:inherit;color:var(--ink);background:#20343e;border:2px solid var(--line);padding:10px 16px;cursor:pointer;min-height:44px}button:hover,button[aria-pressed=true]{border-color:var(--accent);color:var(--accent)}button:focus-visible,[role=button]:focus-visible{outline:4px solid var(--accent);outline-offset:5px}.toolbar{position:fixed;right:22px;top:14px;z-index:30;display:flex;gap:12px;font:16px 'Work Sans',sans-serif}.toolbar button{font-size:16px}.reveal .slide-number{font:18px 'Work Sans';background:transparent;color:var(--muted);left:25px;right:auto;bottom:22px}.reveal .controls{color:var(--accent)}svg{width:100%;height:370px;overflow:visible}svg text{font-family:inherit;fill:var(--ink);font-size:24px;text-anchor:middle;dominant-baseline:middle}svg .edge{stroke:var(--line);stroke-width:3;fill:none}svg rect{fill:#20343e;stroke:var(--line);stroke-width:2}svg [role=button]{cursor:pointer}svg [aria-pressed=true] rect{stroke:var(--accent)}svg .hub{fill:var(--accent)}svg .hub-text{fill:var(--bg);font-weight:700}.detail{margin:5px 0;font-size:24px;min-height:40px}.stages{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin-top:60px}.stage{border-top:3px solid var(--accent);padding-top:20px}.stage strong{font-size:36px}.stage p{font-size:25px}.dots{display:flex;gap:20px;margin:65px 0}.dot{width:70px;height:70px;background:var(--accent)}.ending{font-size:70px;line-height:1.12}.reveal .progress{color:var(--accent)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
@media print{.toolbar,.controls{display:none!important}.reveal .slides section [data-motion]{opacity:1!important;transform:none!important}}
</style></head><body>
<div class="toolbar"><button id="motion" aria-pressed="true">Motion on</button><button id="fullscreen" aria-pressed="false">Enter fullscreen</button></div>
<div class="reveal"><div class="slides">
<section id="signal"><p class="eyebrow">Original example · presentation-html</p><h1>From signal<br>to <span class="accent">decision.</span></h1><p class="big">Use structure to explain the system.<br>Use motion only to explain change.</p><p class="rule muted">Reveal.js 6.0.2 + Anime.js 4.5.0 · All assets embedded</p></section>
<section id="story"><p class="eyebrow">01 / Narrative</p><h2>One question. One visual answer.</h2><div class="stages"><div class="stage" data-motion><strong>Context</strong><p>What changed, and why does it matter?</p></div><div class="stage" data-motion><strong>Mechanism</strong><p>What connects the evidence to the choice?</p></div><div class="stage" data-motion><strong>Decision</strong><p>What should the audience do next?</p></div></div><p class="rule muted">Choose the slide count from the story—not a template quota.</p></section>
<section id="system"><p class="eyebrow">02 / Editable SVG + real interaction</p><h2>Keep the decision at the center.</h2>
<svg viewBox="0 0 1100 370" role="group" aria-label="Six evidence sources feed one decision"><g aria-hidden="true"><path class="edge" d="M250 65L550 185M550 65V185M850 65L550 185M250 305L550 185M550 305V185M850 305L550 185"/></g>
<rect class="hub" x="435" y="145" width="230" height="80"/><text class="hub-text" x="550" y="185">Decision</text>
<g role="button" tabindex="0" aria-label="People" aria-pressed="false" data-node="0" data-detail="People: who owns the decision?"><rect x="140" y="33" width="220" height="64"/><text x="250" y="65">People</text></g>
<g role="button" tabindex="0" aria-label="Process" aria-pressed="false" data-node="1" data-detail="Process: how does work move?"><rect x="440" y="33" width="220" height="64"/><text x="550" y="65">Process</text></g>
<g role="button" tabindex="0" aria-label="Platform" aria-pressed="false" data-node="2" data-detail="Platform: what enables delivery?"><rect x="740" y="33" width="220" height="64"/><text x="850" y="65">Platform</text></g>
<g role="button" tabindex="0" aria-label="Quality" aria-pressed="false" data-node="3" data-detail="Quality: what evidence proves fitness?"><rect x="140" y="273" width="220" height="64"/><text x="250" y="305">Quality</text></g>
<g role="button" tabindex="0" aria-label="Cost" aria-pressed="false" data-node="4" data-detail="Cost: what constraint shapes the choice?"><rect x="440" y="273" width="220" height="64"/><text x="550" y="305">Cost</text></g>
<g role="button" tabindex="0" aria-label="Risk" aria-pressed="false" data-node="5" data-detail="Risk: what could invalidate the plan?"><rect x="740" y="273" width="220" height="64"/><text x="850" y="305">Risk</text></g>
</svg><p class="detail" id="detail" aria-live="polite">Select a source to reveal its role. Tab + Enter or Space also works.</p></section>
<section id="sequence"><p class="eyebrow">03 / Anime.js timeline</p><h2>Reveal dependency—not decoration.</h2><div class="stages"><div class="stage" data-motion><strong>Observe</strong><p>Make the signal visible.</p></div><div class="stage" data-motion><strong>Interpret</strong><p>Connect evidence and context.</p></div><div class="stage" data-motion><strong>Act</strong><p>Name the next reversible step.</p></div></div><p class="rule muted">A short, bounded sequence. No loops. No hidden final content.</p></section>
<section id="stagger"><p class="eyebrow">04 / Restrained stagger</p><h2>Make order perceptible.</h2><div class="dots" aria-hidden="true"><i class="dot" data-motion></i><i class="dot" data-motion></i><i class="dot" data-motion></i><i class="dot" data-motion></i><i class="dot" data-motion></i><i class="dot" data-motion></i></div><p class="big">The pattern is original. The technique is portable.</p><p class="muted">Reduced motion, print and revisit states show the complete result.</p></section>
<section id="decision"><p class="eyebrow">05 / Delivery gate</p><h2 class="ending">Test the exact file.<br>Inspect <span class="accent">every slide.</span></h2><p class="big">Offline. Readable. Keyboard-operable.<br>Then publish the immutable artifact.</p><p class="rule muted">← → Navigate · Esc Overview · F Fullscreen · Tab Focus</p></section>
</div></div><script>/*!
* reveal.js
* https://revealjs.com
* MIT licensed
*
* Copyright (C) 2011-2026 Hakim El Hattab, https://hakim.se
*/
(function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t():typeof define==`function`&&define.amd?define([],t):(e=typeof globalThis<`u`?globalThis:e||self,e.Reveal=t())})(this,function(){var e=(e,t)=>{for(let n in t)e[n]=t[n];return e},t=(e,t)=>Array.from(e.querySelectorAll(t)),n=(e,t,n)=>{n?e.classList.add(t):e.classList.remove(t)},r=e=>{if(typeof e==`string`){if(e===`null`)return null;if(e===`true`)return!0;if(e===`false`)return!1;if(e.match(/^-?[\d\.]+$/))return parseFloat(e)}return e},i=(e,t)=>{e.style.transform=t},a=(e,t)=>{let n=e.matches||e.matchesSelector||e.msMatchesSelector;return!!(n&&n.call(e,t))},o=(e,t)=>{if(e&&typeof e.closest==`function`)return e.closest(t);for(;e;){if(a(e,t))return e;e=e.parentElement}return null},s=e=>{e=e||document.documentElement;let t=e.requestFullscreen||e.webkitRequestFullscreen||e.webkitRequestFullScreen||e.mozRequestFullScreen||e.msRequestFullscreen;t&&t.apply(e)},c=(e,t,n,r=``)=>{let i=e.querySelectorAll(`.`+n);for(let t=0;t<i.length;t++){let n=i[t];if(n.parentNode===e)return n}let a=document.createElement(t);return a.className=n,a.innerHTML=r,e.appendChild(a),a},l=e=>{let t=document.createElement(`style`);return e&&e.length>0&&t.appendChild(document.createTextNode(e)),document.head.appendChild(t),t},u=()=>{let e={};location.search.replace(/[A-Z0-9]+?=([\w\.%-]*)/gi,t=>{let n=t.split(`=`).shift(),r=t.split(`=`).pop();return n&&r!==void 0&&(e[n]=r),t});for(let t in e){let n=e[t];e[t]=r(unescape(n))}return e.dependencies!==void 0&&delete e.dependencies,e},d=(e,t=0)=>{if(e){var n;let r,i=e.style.height;return e.style.height=`0px`,e.parentElement&&(e.parentElement.style.height=`auto`),r=t-(((n=e.parentElement)==null?void 0:n.offsetHeight)||0),e.style.height=i+`px`,e.parentElement&&e.parentElement.style.removeProperty(`height`),r}return t},f={mp4:`video/mp4`,m4a:`video/mp4`,ogv:`video/ogg`,mpeg:`video/mpeg`,webm:`video/webm`},p=(e=``)=>{let t=e.split(`.`).pop();return t?f[t]:void 0},m=(e=``)=>encodeURI(e).replace(/%5B/g,`[`).replace(/%5D/g,`]`).replace(/[!'()*]/g,e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`),h=navigator.userAgent,g=/(iphone|ipod|ipad|android)/gi.test(h)||navigator.platform===`MacIntel`&&navigator.maxTouchPoints>1;/chrome/i.test(h)&&/edge/i.test(h);var _=/android/gi.test(h),v=function(e){if(e){var t=function(e){return[].slice.call(e)},n=0,r=1,i=2,a=3,o=[],s=null,c=`requestAnimationFrame`in e?function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{sync:!1};e.cancelAnimationFrame(s);var n=function(){return u(o.filter((function(e){return e.dirty&&e.active})))};if(t.sync)return n();s=e.requestAnimationFrame(n)}:function(){},l=function(e){return function(t){o.forEach((function(t){return t.dirty=e})),c(t)}},u=function(e){e.filter((function(e){return!e.styleComputed})).forEach((function(e){e.styleComputed=m(e)})),e.filter(h).forEach(g);var t=e.filter(p);t.forEach(f),t.forEach((function(e){g(e),d(e)})),t.forEach(_)},d=function(e){return e.dirty=n},f=function(e){e.availableWidth=e.element.parentNode.clientWidth,e.currentWidth=e.element.scrollWidth,e.previousFontSize=e.currentFontSize,e.currentFontSize=Math.min(Math.max(e.minSize,e.availableWidth/e.currentWidth*e.previousFontSize),e.maxSize),e.whiteSpace=e.multiLine&&e.currentFontSize===e.minSize?`normal`:`nowrap`},p=function(e){return e.dirty!==i||e.dirty===i&&e.element.parentNode.clientWidth!==e.availableWidth},m=function(t){var n=e.getComputedStyle(t.element,null);return t.currentFontSize=parseFloat(n.getPropertyValue(`font-size`)),t.display=n.getPropertyValue(`display`),t.whiteSpace=n.getPropertyValue(`white-space`),!0},h=function(e){var t=!1;return!e.preStyleTestCompleted&&(/inline-/.test(e.display)||(t=!0,e.display=`inline-block`),e.whiteSpace!==`nowrap`&&(t=!0,e.whiteSpace=`nowrap`),e.preStyleTestCompleted=!0,t)},g=function(e){e.element.style.whiteSpace=e.whiteSpace,e.element.style.display=e.display,e.element.style.fontSize=e.currentFontSize+`px`},_=function(e){e.element.dispatchEvent(new CustomEvent(`fit`,{detail:{oldValue:e.previousFontSize,newValue:e.currentFontSize,scaleFactor:e.currentFontSize/e.previousFontSize}}))},v=function(e,t){return function(n){e.dirty=t,e.active&&c(n)}},y=function(e){return function(){o=o.filter((function(t){return t.element!==e.element})),e.observeMutations&&e.observer.disconnect(),e.element.style.whiteSpace=e.originalStyle.whiteSpace,e.element.style.display=e.originalStyle.display,e.element.style.fontSize=e.originalStyle.fontSize}},b=function(e){return function(){e.active||(e.active=!0,c())}},x=function(e){return function(){return e.active=!1}},S=function(e){e.observeMutations&&(e.observer=new MutationObserver(v(e,r)),e.observer.observe(e.element,e.observeMutations))},ee={minSize:16,maxSize:512,multiLine:!0,observeMutations:`MutationObserver`in e&&{subtree:!0,childList:!0,characterData:!0}},C=null,w=function(){e.clearTimeout(C),C=e.setTimeout(l(i),E.observeWindowDelay)},te=[`resize`,`orientationchange`];return Object.defineProperty(ELine truncated
<span class="slide-number-a">${e}</span>
<span class="slide-number-delimiter">${t}</span>
<span class="slide-number-b">${n}</span>
</a>`:`<a href="${r}">
<span class="slide-number-a">${e}</span>
</a>`}destroy(){this.element.remove()}},re=class{constructor(e){this.Reveal=e,this.onInput=this.onInput.bind(this),this.onBlur=this.onBlur.bind(this),this.onKeyDown=this.onKeyDown.bind(this)}render(){this.element=document.createElement(`div`),this.element.className=`jump-to-slide`,this.jumpInput=document.createElement(`input`),this.jumpInput.type=`text`,this.jumpInput.className=`jump-to-slide-input`,this.jumpInput.placeholder=`Jump to slide`,this.jumpInput.addEventListener(`input`,this.onInput),this.jumpInput.addEventListener(`keydown`,this.onKeyDown),this.jumpInput.addEventListener(`blur`,this.onBlur),this.element.appendChild(this.jumpInput)}show(){this.indicesOnShow=this.Reveal.getIndices(),this.Reveal.getRevealElement().appendChild(this.element),this.jumpInput.focus()}hide(){this.isVisible()&&(this.element.remove(),this.jumpInput.value=``,clearTimeout(this.jumpTimeout),delete this.jumpTimeout)}isVisible(){return!!this.element.parentNode}jump(){clearTimeout(this.jumpTimeout),delete this.jumpTimeout;let e=this.jumpInput.value.trim(``),t;if(/^\d+$/.test(e)){let n=this.Reveal.getConfig().slideNumber;if(n===`c`||n===`c/t`){let n=this.Reveal.getSlides()[parseInt(e,10)-1];n&&(t=this.Reveal.getIndices(n))}}return t||(/^\d+\.\d+$/.test(e)&&(e=e.replace(`.`,`/`)),t=this.Reveal.location.getIndicesFromHash(e,{oneBasedIndex:!0})),!t&&/\S+/i.test(e)&&e.length>1&&(t=this.search(e)),t&&e!==``?(this.Reveal.slide(t.h,t.v,t.f),!0):(this.Reveal.slide(this.indicesOnShow.h,this.indicesOnShow.v,this.indicesOnShow.f),!1)}jumpAfter(e){clearTimeout(this.jumpTimeout),this.jumpTimeout=setTimeout(()=>this.jump(),e)}search(e){let t=RegExp(`\\b`+e.trim()+`\\b`,`i`),n=this.Reveal.getSlides().find(e=>t.test(e.innerText));return n?this.Reveal.getIndices(n):null}cancel(){this.Reveal.slide(this.indicesOnShow.h,this.indicesOnShow.v,this.indicesOnShow.f),this.hide()}confirm(){this.jump(),this.hide()}destroy(){this.jumpInput.removeEventListener(`input`,this.onInput),this.jumpInput.removeEventListener(`keydown`,this.onKeyDown),this.jumpInput.removeEventListener(`blur`,this.onBlur),this.element.remove()}onKeyDown(e){e.keyCode===13?this.confirm():e.keyCode===27&&(this.cancel(),e.stopImmediatePropagation())}onInput(e){this.jumpAfter(200)}onBlur(){setTimeout(()=>this.hide(),1)}},D=e=>{let t=e.match(/^#([0-9a-f]{3})$/i);if(t&&t[1]){let e=t[1];return{r:parseInt(e.charAt(0),16)*17,g:parseInt(e.charAt(1),16)*17,b:parseInt(e.charAt(2),16)*17}}let n=e.match(/^#([0-9a-f]{6})$/i);if(n&&n[1]){let e=n[1];return{r:parseInt(e.slice(0,2),16),g:parseInt(e.slice(2,4),16),b:parseInt(e.slice(4,6),16)}}let r=e.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);if(r)return{r:parseInt(r[1],10),g:parseInt(r[2],10),b:parseInt(r[3],10)};let i=e.match(/^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i);return i?{r:parseInt(i[1],10),g:parseInt(i[2],10),b:parseInt(i[3],10),a:parseFloat(i[4])}:null},ie=e=>(typeof e==`string`&&(e=D(e)),e?(e.r*299+e.g*587+e.b*114)/1e3:null),ae=class{constructor(e){this.Reveal=e}render(){this.element=document.createElement(`div`),this.element.className=`backgrounds`,this.Reveal.getRevealElement().appendChild(this.element)}create(){this.element.innerHTML=``,this.element.classList.add(`no-transition`),this.Reveal.getHorizontalSlides().forEach(e=>{let n=this.createBackground(e,this.element);t(e,`section`).forEach(e=>{this.createBackground(e,n),n.classList.add(`stack`)})}),this.Reveal.getConfig().parallaxBackgroundImage?(this.element.style.backgroundImage=`url("`+this.Reveal.getConfig().parallaxBackgroundImage+`")`,this.element.style.backgroundSize=this.Reveal.getConfig().parallaxBackgroundSize,this.element.style.backgroundRepeat=this.Reveal.getConfig().parallaxBackgroundRepeat,this.element.style.backgroundPosition=this.Reveal.getConfig().parallaxBackgroundPosition,setTimeout(()=>{this.Reveal.getRevealElement().classList.add(`has-parallax-background`)},1)):(this.element.style.backgroundImage=``,this.Reveal.getRevealElement().classList.remove(`has-parallax-background`))}createBackground(e,t){let n=document.createElement(`div`);n.className=`slide-background `+e.className.replace(/present|past|future/,``);let r=document.createElement(`div`);return r.className=`slide-background-content`,n.appendChild(r),t.appendChild(n),e.slideBackgroundElement=n,e.slideBackgroundContentElement=r,this.sync(e),n}sync(e){let t=e.slideBackgroundElement,n=e.slideBackgroundContentElement,r={background:e.getAttribute(`data-background`),backgroundSize:e.getAttribute(`data-background-size`),backgroundImage:e.getAttribute(`data-background-image`),backgroundVideo:e.getAttribute(`data-background-video`),backgroundIframe:e.getAttribute(`data-background-iframe`),backgroundColor:e.getAttribute(`data-background-color`),backgroundGradient:e.getAttribute(`data-background-gradient`),backgroundRepeat:e.getAttribute(`data-background-repeat`),backgroundPosition:e.getAttribute(`data-background-position`),backgroundTransition:e.getAttribute(`data-backLine truncated
<button class="navigate-right" aria-label="${e?`previous slide`:`next slide`}"><div class="controls-arrow"></div></button>
<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`,this.Reveal.getRevealElement().appendChild(this.element),this.controlsLeft=t(n,`.navigate-left`),this.controlsRight=t(n,`.navigate-right`),this.controlsUp=t(n,`.navigate-up`),this.controlsDown=t(n,`.navigate-down`),this.controlsPrev=t(n,`.navigate-prev`),this.controlsNext=t(n,`.navigate-next`),this.controlsFullscreen=t(n,`.enter-fullscreen`),this.controlsRightArrow=this.element.querySelector(`.navigate-right`),this.controlsLeftArrow=this.element.querySelector(`.navigate-left`),this.controlsDownArrow=this.element.querySelector(`.navigate-down`)}configure(e,t){let n=e.controls===`speaker`||e.controls===`speaker-only`;this.element.style.display=e.controls&&(!n||this.Reveal.isSpeakerNotes())?`block`:`none`,this.element.setAttribute(`data-controls-layout`,e.controlsLayout),this.element.setAttribute(`data-controls-back-arrows`,e.controlsBackArrows)}bind(){let e=[`touchstart`,`click`];_&&(e=[`touchend`]),e.forEach(e=>{this.controlsLeft.forEach(t=>t.addEventListener(e,this.onNavigateLeftClicked,!1)),this.controlsRight.forEach(t=>t.addEventListener(e,this.onNavigateRightClicked,!1)),this.controlsUp.forEach(t=>t.addEventListener(e,this.onNavigateUpClicked,!1)),this.controlsDown.forEach(t=>t.addEventListener(e,this.onNavigateDownClicked,!1)),this.controlsPrev.forEach(t=>t.addEventListener(e,this.onNavigatePrevClicked,!1)),this.controlsNext.forEach(t=>t.addEventListener(e,this.onNavigateNextClicked,!1)),this.controlsFullscreen.forEach(t=>t.addEventListener(e,this.onEnterFullscreen,!1))})}unbind(){[`touchstart`,`touchend`,`click`].forEach(e=>{this.controlsLeft.forEach(t=>t.removeEventListener(e,this.onNavigateLeftClicked,!1)),this.controlsRight.forEach(t=>t.removeEventListener(e,this.onNavigateRightClicked,!1)),this.controlsUp.forEach(t=>t.removeEventListener(e,this.onNavigateUpClicked,!1)),this.controlsDown.forEach(t=>t.removeEventListener(e,this.onNavigateDownClicked,!1)),this.controlsPrev.forEach(t=>t.removeEventListener(e,this.onNavigatePrevClicked,!1)),this.controlsNext.forEach(t=>t.removeEventListener(e,this.onNavigateNextClicked,!1)),this.controlsFullscreen.forEach(t=>t.removeEventListener(e,this.onEnterFullscreen,!1))})}update(){let e=this.Reveal.availableRoutes();[...this.controlsLeft,...this.controlsRight,...this.controlsUp,...this.controlsDown,...this.controlsPrev,...this.controlsNext].forEach(e=>{e.classList.remove(`enabled`,`fragmented`),e.setAttribute(`disabled`,`disabled`)}),e.left&&this.controlsLeft.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)}),e.right&&this.controlsRight.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)}),e.up&&this.controlsUp.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)}),e.down&&this.controlsDown.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)}),(e.left||e.up)&&this.controlsPrev.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)}),(e.right||e.down)&&this.controlsNext.forEach(e=>{e.classList.add(`enabled`),e.removeAttribute(`disabled`)});let t=this.Reveal.getCurrentSlide();if(t){let e=this.Reveal.fragments.availableRoutes();e.prev&&this.controlsPrev.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)}),e.next&&this.controlsNext.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)});let n=this.Reveal.isVerticalSlide(t),r=n&&t.parentElement&&t.parentElement.querySelectorAll(`:scope > section`).length>1;n&&r?(e.prev&&this.controlsUp.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)}),e.next&&this.controlsDown.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)})):(e.prev&&this.controlsLeft.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)}),e.next&&this.controlsRight.forEach(e=>{e.classList.add(`fragmented`,`enabled`),e.removeAttribute(`disabled`)}))}if(this.Reveal.getConfig().controlsTutorial){let t=this.Reveal.getIndices();!this.Reveal.hasNavigatedVertically()&&e.down?this.controlsDownArrow.classList.add(`highlight`):(this.controlsDownArrow.classList.remove(`highlight`),this.Reveal.getConfig().rtl?!this.Reveal.hasNavigatedHorizontally()&&e.left&&t.v===0?this.controlsLeftArrow.classList.add(`highlight`):this.controlsLeftArrow.classList.remove(`highlight`):!this.Reveal.hasNavigatedHorizontally()&&e.right&&t.v===0?this.controlsRightArrow.classList.add(`highlight`):this.controlsRightArrow.classList.remove(`highlight`))}}destroy(){this.unbind(),this.element.remove()}onNavigateLeftClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.getConfig().navigationMode===`linear`?this.Reveal.prev():this.Reveal.left()}onNavigateRightClicked(e){e.preventDefault(),this.Reveal.onUserInput(),this.Reveal.getConfig().navigationMode===`linear`?this.Reveal.next():this.Reveal.right()}onNavigateUpClicked(e){e.preventDefault(),this.Reveal.onULine truncated
`+e))});let r=document.querySelector(`head`);r&&r.insertBefore(n,r.lastChild)},ye=class{constructor(e){this.Reveal=e,this.state=`idle`,this.registeredPlugins={},this.asyncDependencies=[]}load(e,t){return this.state=`loading`,e.forEach(this.registerPlugin.bind(this)),new Promise(e=>{let n=[],r=0;if(t.forEach(e=>{(!e.condition||e.condition())&&(e.async?this.asyncDependencies.push(e):n.push(e))}),n.length){r=n.length;let t=t=>{t&&typeof t.callback==`function`&&t.callback(),--r===0&&this.initPlugins().then(e)};n.forEach(e=>{typeof e.id==`string`?(this.registerPlugin(e),t(e)):typeof e.src==`string`?ve(e.src,()=>t(e)):(console.warn(`Unrecognized plugin format`,e),t())})}else this.initPlugins().then(e)})}initPlugins(){return new Promise(e=>{let t=Object.values(this.registeredPlugins),n=t.length;if(n===0)this.loadAsync().then(e);else{let r,i=()=>{--n===0?this.loadAsync().then(e):r()},a=0;r=()=>{let e=t[a++];if(typeof e.init==`function`){let t=e.init(this.Reveal);t&&typeof t.then==`function`?t.then(i):i()}else i()},r()}})}loadAsync(){return this.state=`loaded`,this.asyncDependencies.length&&this.asyncDependencies.forEach(e=>{ve(e.src,e.callback)}),Promise.resolve()}registerPlugin(e){arguments.length===2&&typeof arguments[0]==`string`?(e=arguments[1],e.id=arguments[0]):typeof e==`function`&&(e=e());let t=e.id;typeof t==`string`?this.registeredPlugins[t]===void 0?(this.registeredPlugins[t]=e,this.state===`loaded`&&typeof e.init==`function`&&e.init(this.Reveal)):console.warn(`reveal.js: "`+t+`" plugin has already been registered`):console.warn(`Unrecognized plugin format; can't find plugin.id`,e)}hasPlugin(e){return!!this.registeredPlugins[e]}getPlugin(e){return this.registeredPlugins[e]}getRegisteredPlugins(){return this.registeredPlugins}destroy(){Object.values(this.registeredPlugins).forEach(e=>{typeof e.destroy==`function`&&e.destroy()}),this.registeredPlugins={},this.asyncDependencies=[]}},be=class{constructor(e){this.Reveal=e,this.onSlidesClicked=this.onSlidesClicked.bind(this),this.iframeTriggerSelector=null,this.mediaTriggerSelector=`[data-preview-image], [data-preview-video]`,this.stateProps=[`previewIframe`,`previewImage`,`previewVideo`,`previewFit`],this.state={}}update(){this.iframeTriggerSelector=this.Reveal.getConfig().previewLinks?`a[href]:not([data-preview-link=false]), [data-preview-link]:not(a):not([data-preview-link=false])`:`[data-preview-link]:not([data-preview-link=false])`;let e=this.Reveal.getSlidesElement().querySelectorAll(this.iframeTriggerSelector).length>0,t=this.Reveal.getSlidesElement().querySelectorAll(this.mediaTriggerSelector).length>0;e||t?this.Reveal.getSlidesElement().addEventListener(`click`,this.onSlidesClicked,!1):this.Reveal.getSlidesElement().removeEventListener(`click`,this.onSlidesClicked,!1)}createOverlay(e){this.dom=document.createElement(`div`),this.dom.classList.add(`r-overlay`),this.dom.classList.add(e),this.viewport=document.createElement(`div`),this.viewport.classList.add(`r-overlay-viewport`),this.dom.appendChild(this.viewport),this.Reveal.getRevealElement().appendChild(this.dom)}previewIframe(e){if(typeof e!=`string`)return;let t;try{t=new URL(e,document.baseURI)}catch(e){return}if(t.protocol===`file:`&&window.location.protocol!==`file:`||![`http:`,`https:`,`file:`].includes(t.protocol)&&t.href!==`about:blank`)return;this.close(),this.state={previewIframe:e},this.createOverlay(`r-overlay-preview`),this.dom.dataset.state=`loading`,this.viewport.innerHTML=`<header class="r-overlay-header">
<a class="r-overlay-header-button r-overlay-external" target="_blank" rel="noopener"><span class="icon"></span></a>
<button class="r-overlay-header-button r-overlay-close"><span class="icon"></span></button>
</header>
<div class="r-overlay-spinner"></div>
<div class="r-overlay-content">
<iframe></iframe>
<small class="r-overlay-content-inner">
<span class="r-overlay-error x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
</small>
</div>`;let n=this.dom.querySelector(`iframe`);n.addEventListener(`load`,e=>{this.dom.dataset.state=`loaded`},!1),n.src=t.href,this.dom.querySelector(`.r-overlay-external`).href=t.href,this.dom.querySelector(`.r-overlay-close`).addEventListener(`click`,e=>{this.close(),e.preventDefault()},!1),this.dom.querySelector(`.r-overlay-external`).addEventListener(`click`,e=>{this.close()},!1),this.Reveal.dispatchEvent({type:`previewiframe`,data:{url:e}})}previewMedia(e,t,n){if(t!==`image`&&t!==`video`){console.warn(`Please specify a valid media type to preview (image|video)`);return}this.close(),n=n||`scale-down`,this.createOverlay(`r-overlay-preview`),this.dom.dataset.state=`loading`,this.dom.dataset.previewFit=n,this.viewport.innerHTML=`<header class="r-overlay-header">
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
</header>
<div class="r-overlay-spinner"></div>
<div class="r-overlay-content"></div>`;let r=this.dom.querySelector(`.r-overlay-content`);if(t===`image`){this.state={previewImage:e,previewFit:n};let t=document.createElement(`img`,{});t.src=e,r.appendChild(t),t.addEventListener(`load`,()=>{this.dom.dataset.state=`loaded`},!1),t.addEventListener(`error`,()=>{this.dom.dataset.state=`error`,r.innerHTML=`<span class="r-overlay-error">Unable to load image.</span>`},!1),this.dom.style.cursor=`zoom-out`,this.dom.addEventListener(`click`,e=>{this.close()},!1),this.Reveal.dispatchEvent({type:`previewimage`,data:{url:e}})}else if(t===`video`){this.state={previewVideo:e,previewFit:n};let t=document.createElement(`video`);t.autoplay=this.dom.dataset.previewAutoplay!==`false`,t.controls=this.dom.dataset.previewControls!==`false`,t.loop=this.dom.dataset.previewLoop===`true`,t.muted=this.dom.dataset.previewMuted===`true`,t.playsInline=!0,t.src=e,r.appendChild(t),t.addEventListener(`loadeddata`,()=>{this.dom.dataset.state=`loaded`},!1),t.addEventListener(`error`,()=>{this.dom.dataset.state=`error`,r.innerHTML=`<span class="r-overlay-error">Unable to load video.</span>`},!1),this.Reveal.dispatchEvent({type:`previewvideo`,data:{url:e}})}else throw Error(`Please specify a valid media type to preview`);this.dom.querySelector(`.r-overlay-close`).addEventListener(`click`,e=>{this.close(),e.preventDefault()},!1)}previewImage(e,t){this.previewMedia(e,`image`,t)}previewVideo(e,t){this.previewMedia(e,`video`,t)}toggleHelp(e){typeof e==`boolean`?e?this.showHelp():this.close():this.dom?this.close():this.showHelp()}showHelp(){if(this.Reveal.getConfig().help){this.close(),this.createOverlay(`r-overlay-help`);let e=`<p class="title">Keyboard Shortcuts</p>`,t=this.Reveal.keyboard.getShortcuts(),n=this.Reveal.keyboard.getBindings();e+=`<table><th>KEY</th><th>ACTION</th>`;for(let n in t)e+=`<tr><td>${n}</td><td>${t[n]}</td></tr>`;for(let t in n)n[t].key&&n[t].description&&(e+=`<tr><td>${n[t].key}</td><td>${n[t].description}</td></tr>`);e+=`</table>`,this.viewport.innerHTML=`
<header class="r-overlay-header">
<button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
</header>
<div class="r-overlay-content">
<div class="r-overlay-help-content">${e}</div>
</div>
`,this.dom.querySelector(`.r-overlay-close`).addEventListener(`click`,e=>{this.close(),e.preventDefault()},!1),this.Reveal.dispatchEvent({type:`showhelp`})}}isOpen(){return!!this.dom}close(){return this.dom?(this.dom.remove(),this.dom=null,this.state={},this.Reveal.dispatchEvent({type:`closeoverlay`}),!0):!1}getState(){return this.state}setState(e){this.stateProps.every(t=>this.state[t]===e[t])||(e.previewIframe?this.previewIframe(e.previewIframe):e.previewImage?this.previewImage(e.previewImage,e.previewFit):e.previewVideo?this.previewVideo(e.previewVideo,e.previewFit):this.close())}onSlidesClicked(e){let t=e.target,n=t.closest(this.iframeTriggerSelector),r=t.closest(this.mediaTriggerSelector);if(n){if(e.metaKey||e.shiftKey||e.altKey)return;let t=n.getAttribute(`data-preview-link`),r=typeof t==`string`&&t.startsWith(`http`)?t:n.getAttribute(`href`);r&&(this.previewIframe(r),e.preventDefault())}else if(r){if(r.hasAttribute(`data-preview-image`)){let t=r.dataset.previewImage||r.getAttribute(`src`);t&&(this.previewImage(t,r.dataset.previewFit),e.preventDefault())}else if(r.hasAttribute(`data-preview-video`)){let t=r.dataset.previewVideo||r.getAttribute(`src`);if(!t){let e=r.querySelector(`source`);e&&(t=e.getAttribute(`src`))}t&&(this.previewVideo(t,r.dataset.previewFit),e.preventDefault())}}}destroy(){this.close()}},F=40,I=1.03,xe=class{constructor(e){this.Reveal=e,this.touchStartX=0,this.touchStartY=0,this.touchStartCount=0,this.touchCaptured=!1,this.activePointers=new Map,this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this),this.onPointerUp=this.onPointerUp.bind(this),this.onPointerCancel=this.onPointerCancel.bind(this),this.onTouchStart=this.onTouchStart.bind(this),this.onTouchMove=this.onTouchMove.bind(this),this.onTouchEnd=this.onTouchEnd.bind(this)}bind(){let e=this.Reveal.getRevealElement();`onpointerdown`in window?(e.addEventListener(`pointerdown`,this.onPointerDown,!1),e.addEventListener(`pointermove`,this.onPointerMove,!1),e.addEventListener(`pointerup`,this.onPointerUp,!1),e.addEventListener(`pointercancel`,this.onPointerCancel,!1)):(e.addEventListener(`touchstart`,this.onTouchStart,!1),e.addEventListener(`touchmove`,this.onTouchMove,!1),e.addEventListener(`touchend`,this.onTouchEnd,!1))}unbind(){let e=this.Reveal.getRevealElement();e.removeEventListener(`pointerdown`,this.onPointerDown,!1),e.removeEventListener(`pointermove`,this.onPointerMove,!1),e.removeEventListener(`pointerup`,this.onPointerUp,!1),e.removeEventListener(`pointercancel`,this.onPointerCancel,!1),e.removeEventListener(`touchstart`,this.onTouchStart,!1),e.removeEventListener(`touchmove`,this.onTouchMove,!1),e.removeEventListener(`touchend`,this.onTouchEnd,!1)}isSwipePrevented(e){if(a(e,`video[controls], audio[controls]`))return!0;for(;e&&typeof e.hasAttribute==`function`;){if(e.hasAttribute(`data-prevent-swipe`))return!0;e=e.parentNode}return!1}isViewportZoomed(){if(!window.visualViewport||typeof window.visualViewport.scale!=`number`)return!1;let e=window.visualViewport.scale;return this.visualViewportBaseScale=this.visualViewportBaseScale===void 0?e:Math.min(this.visualViewportBaseScale,e),e/this.visualViewportBaseScale>I}onTouchStart(e){if(this.touchCaptured=!1,this.isSwipePrevented(e.target))return!0;this.touchStartX=e.touches[0].clientX,this.touchStartY=e.touches[0].clientY,this.touchStartCount=e.touches.length}onTouchMove(e){if(this.isSwipePrevented(e.target))return!0;let t=this.Reveal.getConfig();if(this.touchCaptured)_&&e.preventDefault();else{this.Reveal.onUserInput(e);let n=e.touches[0].clientX,r=e.touches[0].clientY;if(e.touches.length===1&&this.touchStartCount!==2){let i=this.Reveal.availableRoutes({includeFragments:!0}),a=n-this.touchStartX,o=r-this.touchStartY;a>F&&Math.abs(a)>Math.abs(o)?(this.touchCaptured=!0,t.navigationMode===`linear`?t.rtl?this.Reveal.next():this.Reveal.prev():this.Reveal.left()):a<-40&&Math.abs(a)>Math.abs(o)?(this.touchCaptured=!0,t.navigationMode===`linear`?t.rtl?this.Reveal.prev():this.Reveal.next():this.Reveal.right()):o>F&&i.up?(this.touchCaptured=!0,t.navigationMode===`linear`?this.Reveal.prev():this.Reveal.up()):o<-40&&i.down&&(this.touchCaptured=!0,t.navigationMode===`linear`?this.Reveal.next():this.Reveal.down()),t.embedded?(this.touchCaptured||this.Reveal.isVerticalSlide())&&e.preventDefault():e.preventDefault()}}}onTouchEnd(e){this.touchCaptured&&!this.Reveal.slideContent.isAllowedToPlayAudio()&&this.Reveal.startEmbeddedContent(this.Reveal.getCurrentSlide()),this.touchCaptured=!1}getActiveTouches(){return Array.from(this.activePointers.values(),e=>({clientX:e.clientX,clientY:e.clientY}))}onPointerDown(e){e.pointerType===`touch`&&(this.activePointers.set(e.pointerId,e),e.touches=this.getActiveTouches(),this.onTouchStart(e))}onPointerMove(e){e.pointerType===`touch`&&(this.activePointers.set(e.pointerId,e),e.touches=this.getActiveTouches(),this.onTouchMove(e))}onPointerUp(e){e.pointerType===`touch`&&(this.activePointers.delete(e.pointerId),Line truncated
`):null}destroy(){this.element.remove()}},we=class{constructor(e,t){this.diameter=100,this.diameter2=this.diameter/2,this.thickness=6,this.playing=!1,this.progress=0,this.progressOffset=1,this.container=e,this.progressCheck=t,this.canvas=document.createElement(`canvas`),this.canvas.className=`playback`,this.canvas.width=this.diameter,this.canvas.height=this.diameter,this.canvas.style.width=this.diameter2+`px`,this.canvas.style.height=this.diameter2+`px`,this.context=this.canvas.getContext(`2d`),this.container.appendChild(this.canvas),this.render()}setPlaying(e){let t=this.playing;this.playing=e,!t&&this.playing?this.animate():this.render()}animate(){let e=this.progress;this.progress=this.progressCheck(),e>.8&&this.progress<.2&&(this.progressOffset=this.progress),this.render(),this.playing&&requestAnimationFrame(this.animate.bind(this))}render(){let e=this.playing?this.progress:0,t=this.diameter2-this.thickness,n=this.diameter2,r=this.diameter2;this.progressOffset+=(1-this.progressOffset)*.1;let i=-Math.PI/2+Math.PI*2*e,a=-Math.PI/2+this.progressOffset*(Math.PI*2);this.context.save(),this.context.clearRect(0,0,this.diameter,this.diameter),this.context.beginPath(),this.context.arc(n,r,t+4,0,Math.PI*2,!1),this.context.fillStyle=`rgba( 0, 0, 0, 0.4 )`,this.context.fill(),this.context.beginPath(),this.context.arc(n,r,t,0,Math.PI*2,!1),this.context.lineWidth=this.thickness,this.context.strokeStyle=`rgba( 255, 255, 255, 0.2 )`,this.context.stroke(),this.playing&&(this.context.beginPath(),this.context.arc(n,r,t,a,i,!1),this.context.lineWidth=this.thickness,this.context.strokeStyle=`#fff`,this.context.stroke()),this.context.translate(n-14,r-14),this.playing?(this.context.fillStyle=`#fff`,this.context.fillRect(0,0,10,28),this.context.fillRect(18,0,10,28)):(this.context.beginPath(),this.context.translate(4,0),this.context.moveTo(0,0),this.context.lineTo(24,14),this.context.lineTo(0,28),this.context.fillStyle=`#fff`,this.context.fill()),this.context.restore()}on(e,t){this.canvas.addEventListener(e,t,!1)}off(e,t){this.canvas.removeEventListener(e,t,!1)}destroy(){this.playing=!1,this.canvas.parentNode&&this.container.removeChild(this.canvas)}},Te={width:960,height:700,margin:.04,minScale:.2,maxScale:2,controls:!0,controlsTutorial:!0,controlsLayout:`bottom-right`,controlsBackArrows:`faded`,progress:!0,slideNumber:!1,showSlideNumber:`all`,hashOneBasedIndex:!1,hash:!1,respondToHashChanges:!0,jumpToSlide:!0,history:!1,keyboard:!0,keyboardCondition:null,disableLayout:!1,overview:!0,center:!0,touch:!0,loop:!1,rtl:!1,navigationMode:`default`,shuffle:!1,fragments:!0,fragmentInURL:!0,embedded:!1,help:!0,pause:!0,showNotes:!1,showHiddenSlides:!1,autoPlayMedia:null,preloadIframes:null,mouseWheel:!1,previewLinks:!1,viewDistance:3,mobileViewDistance:2,display:`block`,hideInactiveCursor:!0,hideCursorTime:5e3,sortFragmentsOnSync:!0,autoAnimate:!0,autoAnimateMatcher:null,autoAnimateEasing:`ease`,autoAnimateDuration:1,autoAnimateUnmatched:!0,autoAnimateStyles:[`opacity`,`color`,`background-color`,`padding`,`font-size`,`line-height`,`letter-spacing`,`border-width`,`border-color`,`border-radius`,`outline`,`outline-offset`],autoSlide:0,autoSlideStoppable:!0,autoSlideMethod:null,defaultTiming:null,postMessage:!0,postMessageEvents:!1,focusBodyOnPageVisibilityChange:!0,transition:`slide`,transitionSpeed:`default`,backgroundTransition:`fade`,parallaxBackgroundImage:``,parallaxBackgroundSize:``,parallaxBackgroundRepeat:``,parallaxBackgroundPosition:``,parallaxBackgroundHorizontal:null,parallaxBackgroundVertical:null,view:null,scrollLayout:`full`,scrollSnap:`mandatory`,scrollProgress:`auto`,scrollActivationWidth:435,pdfMaxPagesPerSlide:1/0,pdfSeparateFragments:!0,pdfPageHeightOffset:-1,dependencies:[],plugins:[]},Ee=`6.0.2`;function z(a,s){arguments.length<2&&(s=arguments[0],a=document.querySelector(`.reveal`));let l={},f={},p=!1,m=!1,h,_,v,y,b={hasNavigatedHorizontally:!1,hasNavigatedVertically:!1},x=[],S=1,T={layout:``,overview:``},D={},ie=`idle`,O=0,k,se=0,ce=-1,A=!1,j=new ee(l),M=new ne(l),N=new re(l),ve=new oe(l),F=new ae(l),I=new le(l),L=new ue(l),R=new de(l),z=new fe(l),B=new pe(l),V=new me(l),H=new he(l),U=new ge(l),De=new _e(l),W=new ye(l),G=new be(l),K=new Se(l),Oe=new xe(l),q=new Ce(l);function ke(e){if(!a)throw`Unable to find presentation root (<div class="reveal">).`;if(p)throw`Reveal.js has already been initialized.`;if(p=!0,D.wrapper=a,D.slides=a.querySelector(`.slides`),!D.slides)throw`Unable to find slides container (<div class="slides">).`;return f=P(P(P(P(P({},Te),f),s),e),u()),/print-pdf/gi.test(window.location.search)&&(f.view=`print`),Ae(),window.addEventListener(`load`,Y,!1),W.load(f.plugins,f.dependencies).then(je),new Promise(e=>l.on(`ready`,e))}function Ae(){f.embedded===!0?D.viewport=o(a,`.reveal-viewport`)||a:(D.viewport=document.body,document.documentElement.classList.add(`reveal-full-page`)),D.viewport.classList.add(`reveal-viewport`)}function je(){p!==!1&&(m=!0,Ne(),Pe(),Ve(),ze(),Be(),vt(),He(),F.update(!0),Me(),VLine truncated
* 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>(() => {
const {createTimeline, stagger} = anime;
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
const printMode = location.search.includes('print-pdf');
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() {
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)||!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();};
reduced.addEventListener('change',()=>{if(reduced.matches)settle();motionLabel();});motionLabel();
addEventListener('beforeprint',settle);
const fs=document.querySelector('#fullscreen');
async function fullscreen(){try{if(document.fullscreenElement)await document.exitFullscreen();else await document.documentElement.requestFullscreen();}catch{fs.textContent='Fullscreen unavailable';}}
fs.onclick=fullscreen;
document.addEventListener('fullscreenchange',()=>{fs.textContent=document.fullscreenElement?'Exit fullscreen':'Enter fullscreen';fs.setAttribute('aria-pressed',String(!!document.fullscreenElement));});
document.addEventListener('keydown',e=>{if(e.key.toLowerCase()==='f'&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&!e.target.matches('input,textarea,[contenteditable]')){e.preventDefault();e.stopPropagation();fullscreen();}},true);
document.querySelectorAll('[data-node]').forEach(node=>{
function select(){const was=node.getAttribute('aria-pressed')==='true';document.querySelectorAll('[data-node]').forEach(n=>n.setAttribute('aria-pressed','false'));node.setAttribute('aria-pressed',String(!was));document.querySelector('#detail').textContent=was?'Select a source to reveal its role. Tab + Enter or Space also works.':node.dataset.detail;}
node.addEventListener('click',select);
node.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();e.stopPropagation();select();}});
});
Reveal.initialize({width:1240,height:700,margin:.08,center:false,hash:true,transition:'none',backgroundTransition:'none',slideNumber:'c/t',view:'slide',keyboard:{70:null},autoAnimate:false}).then(()=>{
if(!location.hash){try{const i=Number(localStorage.getItem('presentation-html-demo-slide'));if(Number.isInteger(i)&&i>=0&&i<Reveal.getTotalSlides())Reveal.slide(i);}catch{}}
run(); window.deckReady=true;
});
Reveal.on('slidechanged',()=>{try{localStorage.setItem('presentation-html-demo-slide',String(Reveal.getIndices().h));}catch{}run();});
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};
})();
</script>
<!-- THIRD-PARTY NOTICES
Copyright (C) 2011-2026 Hakim El Hattab, http://hakim.se, and reveal.js contributors
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.
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>
+18
View File
@@ -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": []
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 68 KiB

+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
+13 -4
View File
@@ -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"
}
}
+29
View File
@@ -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.
+60
View File
@@ -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.
+1 -1
View File
@@ -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('<!-- LICENSES -->','<!-- THIRD-PARTY NOTICES\n'+licenses.replace('--','—')+'\n-->')
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()
+61
View File
@@ -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('<svg width="240" height="80" viewBox="0 0 240 80"><path id="flow-path" d="M0 30 L200 30" fill="none" stroke="black"/><circle id="packet" r="5"/></svg><div id="cells">'+ '<i style="display:inline-block;width:20px;height:20px">•</i>'.repeat(6)+'</div>');
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(); }
+1 -1
View File
@@ -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:[]};