open-design/skills/html-ppt/assets/animations/fx/counter-explosion.js
Tom Huang f4ab52d9dd
feat(skills): integrate lewislulu/html-ppt-skill + 15 per-template Examples cards (#193)
* feat(skills): integrate lewislulu/html-ppt-skill as html-ppt + 15 per-template Examples cards

Bring the MIT-licensed lewislulu/html-ppt-skill upstream into skills/html-ppt/
with its full asset tree (36 themes, 31 single-page layouts, 27 CSS + 20
canvas-FX animations, runtime + presenter mode, all 15 full-deck templates,
and the upstream LICENSE preserved verbatim).

Surface each full-deck template as its own Examples gallery card via thin
wrapper skills under skills/html-ppt-<template>/. Each wrapper ships:

- SKILL.md with `od.mode=deck`, scenario, `featured: 20-34` (slotting after
  the existing curated cards), an `od.example_prompt` tuned to the template,
  and `od.upstream` pointing at the upstream repo. Clicking "Use this prompt"
  on a card now wires up `kind=deck` + `speakerNotes=true` and seeds the
  composer with the upstream's authoring flow so the prompt -> output path
  matches the upstream demo.
- example.html baked self-contained (fonts/base/animations/style/theme CSS
  inlined, runtime <script> stripped) so the gallery srcdoc iframe renders
  the upstream look without external paths.

scripts/scaffold-html-ppt-skills.mjs and scripts/bake-html-ppt-examples.mjs
are idempotent generators — re-run after editing skills/html-ppt/ to re-sync
all per-template wrappers and their baked examples.

Add a Credits section + extend the License section in README.md /
README.zh-CN.md / README.ko.md to credit the upstream alongside the
already-cited op7418/guizang-ppt-skill.

* fix(scripts): allowlist html-ppt skill JS for residual-js check

Add scripts/bake-html-ppt-examples.mjs and scripts/scaffold-html-ppt-skills.mjs
to allowedExactPaths, and skills/html-ppt/assets/ to allowedPathPrefixes so
pnpm check:residual-js no longer flags the vendored upstream runtime JS or the
new maintainer-only .mjs scripts.

* fix(skills): keep all slides in baked html-ppt examples + correct asset guidance

The bake script's `STATIC_FALLBACK_CSS` set `.slide+.slide{display:none}`,
which silently truncated every baked `example.html` to slide 1. That artifact
is also served by `/api/skills/:id/example` and reused by the Examples
preview modal's share/export and print-to-PDF, so the rule dropped the rest
of the deck from those flows. Drop the rule — slides now stack in the
print-style flow the surrounding comment already described, the gallery
thumbnail iframe still naturally lands on slide 1 (each `.slide` is `100vh`),
and modal/share/export contains the full deck.

The wrapper SKILL.md authoring instructions told agents to copy
`index.html` + `style.css` into a project while keeping the upstream
`../../../assets/...` links, but those parent-relative URLs only resolve
in-tree (the template sits three folders deep). Once the file lives in a
project artifact, `base.css`, `animations.css`, and `runtime.js` 404 and
the deck never activates. Replace step 3 with two recipes — copy the
shared assets into a project-local `assets/` and rewrite the four tags,
or inline the CSS/JS directly — and re-emit all 15 wrapper SKILL.md
files via the scaffold generator.
2026-05-02 11:00:44 +08:00

58 lines
2.3 KiB
JavaScript

(function(){
window.HPX = window.HPX || {};
window.HPX['counter-explosion'] = function(el){
const U = window.HPX._u;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const target = parseInt(el.getAttribute('data-fx-to') || '2400', 10);
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
// number overlay
const num = document.createElement('div');
num.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:900 120px system-ui,sans-serif;color:var(--text-1,#fff);pointer-events:none;text-shadow:0 4px 40px rgba(124,92,255,0.5);';
num.textContent = '0';
el.appendChild(num);
let parts = [];
let state = 'count'; // count | burst | hold
let stateT = 0;
let value = 0;
let cycle = 0;
const burst = () => {
const cx = k.w/2, cy = k.h/2;
for (let i=0;i<120;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(120, 400);
parts.push({x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,r:U.rand(2,5),c:pal[(Math.random()*pal.length)|0]});
}
};
const stop = U.loop(() => {
ctx.clearRect(0,0,k.w,k.h);
const dt = 1/60;
stateT += dt;
if (state === 'count'){
const dur = 2.2;
const p = Math.min(1, stateT/dur);
const eased = 1 - Math.pow(1-p,3);
value = Math.round(target*eased);
num.textContent = value.toLocaleString();
if (p >= 1){ state='burst'; stateT=0; burst(); }
} else if (state === 'burst'){
if (stateT > 0.05 && stateT < 0.3 && parts.length < 200) {}
if (stateT > 2.5){ state='hold'; stateT=0; }
} else if (state === 'hold'){
if (stateT > 1.5){
state='count'; stateT=0; value=0; num.textContent='0'; cycle++;
}
}
parts = parts.filter(p => p.life > 0);
for (const p of parts){
p.vy += 260*dt; p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx*dt; p.y += p.vy*dt; p.life -= 0.01;
ctx.globalAlpha = Math.max(0,p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); if (num.parentNode) num.parentNode.removeChild(num); } };
};
})();