Add workout videos, editable HyperFrames projects and music tools

- diamond-pushup (with and without music) and bench-dips videos
- Editable project sources for both videos
- make_music.py: original copyright-free music generator

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXVsD1DgdzZLiPuwtKzMSY
This commit is contained in:
Claude committed 2026-10-21 23:40:56 +00:00
1 parent daac216bb9
commit 8b1eeff1ed
33 files changed
+1051

No files matched your search

@@ -0,0 +1,22 @@
<template>
<style>
#root { position: absolute; inset: 0; font-family: "Oswald", sans-serif; color: #eaf2ff; }
.rep { position: absolute; right: 192px; top: 108px; text-align: right; font-weight: 700; font-size: 64px; line-height: 1; letter-spacing: 0.04em; white-space: nowrap; }
.rep .lbl { font-family: "Inter", sans-serif; font-weight: 600; font-size: 28px; letter-spacing: 0.22em; margin-right: 18px; vertical-align: middle; }
.rep .n { color: var(--accent, #ff7a45); font-size: 96px; vertical-align: middle; }
.rep .of { opacity: 0.7; font-size: 48px; vertical-align: middle; }
</style>
<div id="root" data-composition-id="counter" data-width="1920" data-height="1080">
<div id="rep1" class="clip rep" data-start="2.4" data-duration="3.2" data-track-index="0"><span class="lbl">REP</span><span class="n">1</span> <span class="of">/ 3</span></div>
<div id="rep2" class="clip rep" data-start="5.6" data-duration="3.2" data-track-index="0"><span class="lbl">REP</span><span class="n">2</span> <span class="of">/ 3</span></div>
<div id="rep3" class="clip rep" data-start="8.8" data-duration="3.2" data-track-index="0"><span class="lbl">REP</span><span class="n">3</span> <span class="of">/ 3</span></div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
[["rep1", 2.4], ["rep2", 5.6], ["rep3", 8.8]].forEach(([id, s]) => {
tl.fromTo("#" + id, { opacity: 0, filter: "blur(6px)" }, { opacity: 1, filter: "blur(0px)", duration: 0.2, ease: "power4.out" }, s);
tl.to("#" + id, { opacity: 0, filter: "blur(6px)", duration: 0.12, ease: "power3.in" }, s + 3.2 - 0.17);
});
window.__timelines["counter"] = tl;
</script>
</template>
@@ -0,0 +1,126 @@
<template>
<style>
#root { position: absolute; inset: 0; background: #050b1a; }
#stage-svg { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; }
</style>
<div id="root" data-composition-id="stage" data-width="1920" data-height="1080">
<svg id="stage-svg" viewBox="0 0 1920 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="st-smoke"><stop offset="0" stop-color="#4a7bd6" stop-opacity="0.34"/><stop offset="1" stop-color="#4a7bd6" stop-opacity="0"/></radialGradient>
<linearGradient id="st-floor" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0d2147"/><stop offset="1" stop-color="#050b1a"/></linearGradient>
<radialGradient id="st-pool"><stop offset="0" stop-color="#3b82f6" stop-opacity="0.35"/><stop offset="1" stop-color="#3b82f6" stop-opacity="0"/></radialGradient>
<filter id="st-glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="9"/></filter>
</defs>
<rect width="1920" height="1080" fill="#050b1a"/>
<g id="st-smokes" data-layout-ignore></g>
<polygon points="0,780 1920,780 1920,1080 0,1080" fill="url(#st-floor)"/>
<g id="st-grid" data-layout-ignore stroke="#3b82f6" stroke-opacity="0.16" fill="none"></g>
<line x1="0" y1="780" x2="1920" y2="780" stroke="#60a5fa" stroke-opacity="0.5" stroke-width="2"/>
<g id="st-world" transform="translate(960 780) scale(1.2) translate(-960 -780)">
<ellipse cx="1000" cy="800" rx="720" ry="46" fill="url(#st-pool)"/>
<g id="st-bench">
<line x1="600" y1="625" x2="600" y2="780" stroke="#16306b" stroke-width="26" stroke-linecap="round"/>
<line x1="920" y1="625" x2="920" y2="780" stroke="#16306b" stroke-width="26" stroke-linecap="round"/>
<rect x="530" y="591" width="460" height="36" rx="12" fill="#1c3f8a"/>
<rect x="534" y="593" width="452" height="8" rx="4" fill="#7fb2ff" fill-opacity="0.55"/>
</g>
<g id="st-body"></g>
</g>
</svg>
</div>
<script>
const NS = "http://www.w3.org/2000/svg";
const svg = document.getElementById("stage-svg");
const mk = (tag, attrs, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); (parent || svg).appendChild(e); return e; };
// static atmosphere: floor grid + drifting smoke blobs (positions are a function of time below)
const grid = document.getElementById("st-grid");
for (let i = -12; i <= 12; i++) mk("line", { x1: 960 + i * 150, y1: 780, x2: 960 + i * 520, y2: 1080 }, grid);
for (let k = 1; k <= 7; k++) { const y = 780 + 300 * Math.pow(k / 7, 1.7); mk("line", { x1: 0, y1: y, x2: 1920, y2: y }, grid); }
const smokes = [[300, 300, 520, 0], [900, 220, 600, 1.7], [1500, 330, 560, 3.1], [1750, 560, 480, 4.6]].map(([x, y, r, ph]) =>
({ el: mk("circle", { cx: x, cy: y, r, fill: "url(#st-smoke)" }, document.getElementById("st-smokes")), x, y, ph }));
// body parts: each limb = dark outline + mid fill + thin highlight, so it reads as a glassy tube
const body = document.getElementById("st-body");
function limb(w, mid) {
const o = mk("line", { stroke: "#0b1d44", "stroke-width": w, "stroke-linecap": "round" }, body);
const m = mk("line", { stroke: mid || "#2a55b8", "stroke-width": w * 0.78, "stroke-linecap": "round" }, body);
const h = mk("line", { stroke: "#8fbcff", "stroke-opacity": 0.55, "stroke-width": w * 0.16, "stroke-linecap": "round" }, body);
return (a, b) => {
for (const e of [o, m]) { e.setAttribute("x1", a.x); e.setAttribute("y1", a.y); e.setAttribute("x2", b.x); e.setAttribute("y2", b.y); }
const dx = -w * 0.16, dy = -w * 0.2;
h.setAttribute("x1", a.x + dx); h.setAttribute("y1", a.y + dy); h.setAttribute("x2", b.x + dx); h.setAttribute("y2", b.y + dy);
};
}
const glowLine = (w) => mk("line", { stroke: "#ff7a45", "stroke-width": w, "stroke-linecap": "round" }, body);
const setShin = limb(54), setThigh = limb(76), setShorts = limb(102, "#10172f"), setTorso = limb(112, "#27499f"),
setUpper = limb(52), setFore = limb(42);
const shoe = mk("rect", { width: 92, height: 24, rx: 11, fill: "#0b1226", stroke: "#8fbcff", "stroke-opacity": 0.6, "stroke-width": 2 }, body);
const head = mk("circle", { r: 44, fill: "#2f5bbf", stroke: "#8fbcff", "stroke-opacity": 0.7, "stroke-width": 2 }, body);
const neck = limb(30, "#2a55b8");
const hand = mk("ellipse", { rx: 34, ry: 12, fill: "#7fa9f0", stroke: "#0b1d44", "stroke-width": 2 }, body);
// orange muscle glow: triceps on the upper arm, plus shoulder and chest
const tri = glowLine(26), triGlow = glowLine(40); triGlow.setAttribute("filter", "url(#st-glow)");
const delt = mk("circle", { r: 30, fill: "#ff7a45" }, body); delt.setAttribute("filter", "url(#st-glow)");
const chest = mk("circle", { r: 34, fill: "#ff7a45" }, body); chest.setAttribute("filter", "url(#st-glow)");
const deltCore = mk("circle", { r: 16, fill: "#ffb089" }, body);
// geometry (px). Real proportions: arm about 60 cm at 4.2 px/cm, bench 45 cm high.
const GY = 780, HAND = { x: 940, y: 581 };
const Lu = 126, Lf = 118, T = 210, Lleg = 378, lean = 6 * Math.PI / 180, DROP = 67;
const reach = 0.985 * (Lu + Lf);
const shTop = { x: HAND.x + 48, y: HAND.y - Math.sqrt(reach * reach - 48 * 48) };
const hip0 = { x: shTop.x + T * Math.sin(lean), y: shTop.y + T * Math.cos(lean) };
const ankle = { y: GY - 26 };
ankle.x = hip0.x + Math.sqrt(Lleg * Lleg - (ankle.y - hip0.y) ** 2); // heels stay planted
// reps: set-up hold to 2.4 s, then 3 reps of 3.2 s (1.7 s down, 1.5 s up), then a hold
const REP0 = 2.4, REP = 3.2, DOWN = 1.7, UP = 1.5;
const ease = (x) => 0.5 - 0.5 * Math.cos(Math.PI * Math.min(1, Math.max(0, x)));
function phase(t) {
for (let k = 0; k < 3; k++) {
const u = t - (REP0 + REP * k);
if (u >= 0 && u < DOWN) return { p: ease(u / DOWN), push: 0 };
if (u >= DOWN && u < REP) { const x = (u - DOWN) / UP; return { p: 1 - ease(x), push: Math.sin(Math.PI * x) }; }
}
return { p: 0, push: 0 };
}
function draw(t) {
const { p, push } = phase(t);
const hipY = hip0.y + DROP * p;
const hip = { x: ankle.x - Math.sqrt(Lleg * Lleg - (ankle.y - hipY) ** 2), y: hipY };
const sh = { x: hip.x - T * Math.sin(lean), y: hip.y - T * Math.cos(lean) };
// two-bone arm IK: hand is planted on the bench, elbow bends back (toward the bench)
const dx = HAND.x - sh.x, dy = HAND.y - sh.y, d = Math.hypot(dx, dy), dd = Math.min(d, (Lu + Lf) * 0.999);
const a = (Lu * Lu - Lf * Lf + dd * dd) / (2 * dd), h = Math.sqrt(Math.max(0, Lu * Lu - a * a));
const ux = dx / d, uy = dy / d;
const e1 = { x: sh.x + ux * a - uy * h, y: sh.y + uy * a + ux * h }, e2 = { x: sh.x + ux * a + uy * h, y: sh.y + uy * a - ux * h };
const el = e1.x < e2.x ? e1 : e2;
const knee = { x: hip.x + (ankle.x - hip.x) * 0.52, y: hip.y + (ankle.y - hip.y) * 0.52 - 7 };
setShin(knee, ankle); setThigh(hip, knee);
setShorts(hip, { x: hip.x + (knee.x - hip.x) * 0.45, y: hip.y + (knee.y - hip.y) * 0.45 });
shoe.setAttribute("x", ankle.x - 26); shoe.setAttribute("y", ankle.y + 4);
setTorso(hip, sh);
const hd = { x: sh.x + 18, y: sh.y - 74 };
head.setAttribute("cx", hd.x); head.setAttribute("cy", hd.y); neck(sh, { x: hd.x - 4, y: hd.y + 18 });
setUpper(sh, el); setFore(el, HAND);
hand.setAttribute("cx", HAND.x - 4); hand.setAttribute("cy", HAND.y + 4);
// glow follows the work: it swells while pressing up and stays warm at the bottom
const g = 0.22 + 0.78 * push + 0.25 * p * (1 - push);
const tA = { x: sh.x + (el.x - sh.x) * 0.3, y: sh.y + (el.y - sh.y) * 0.3 }, tB = { x: sh.x + (el.x - sh.x) * 0.88, y: sh.y + (el.y - sh.y) * 0.88 };
for (const e of [tri, triGlow]) { e.setAttribute("x1", tA.x); e.setAttribute("y1", tA.y); e.setAttribute("x2", tB.x); e.setAttribute("y2", tB.y); }
tri.setAttribute("stroke-opacity", Math.min(1, 0.25 + g * 0.8)); triGlow.setAttribute("stroke-opacity", Math.min(1, g));
delt.setAttribute("cx", sh.x + 4); delt.setAttribute("cy", sh.y + 8); delt.setAttribute("fill-opacity", 0.15 + 0.7 * g);
deltCore.setAttribute("cx", sh.x + 4); deltCore.setAttribute("cy", sh.y + 8); deltCore.setAttribute("fill-opacity", 0.1 + 0.6 * g);
chest.setAttribute("cx", sh.x + 30); chest.setAttribute("cy", sh.y + 62); chest.setAttribute("fill-opacity", 0.1 + 0.55 * g);
for (const s of smokes) { s.el.setAttribute("cx", s.x + 60 * Math.sin(t * 0.35 + s.ph)); s.el.setAttribute("cy", s.y + 24 * Math.sin(t * 0.27 + s.ph * 1.3)); }
}
draw(0);
const tl = gsap.timeline({ paused: true });
const clock = { t: 0 };
tl.to(clock, { t: 14.2, duration: 14.2, ease: "none", onUpdate: () => draw(clock.t) }, 0);
window.__timelines["stage"] = tl;
</script>
</template>
@@ -0,0 +1,42 @@
<template>
<style>
#root { position: absolute; inset: 0; font-family: "Inter", sans-serif; color: #eaf2ff; }
.tip { position: absolute; left: 192px; bottom: 108px; width: 1100px; }
.tip .num { display: inline-block; font-weight: 700; font-size: 28px; letter-spacing: 0.16em; color: var(--accent, #ff7a45); margin-right: 20px; }
.tip h2 { font-family: "Oswald", sans-serif; font-weight: 700; font-size: 76px; line-height: 1.02; letter-spacing: 0.01em; }
.tip p { margin-top: 10px; font-weight: 500; font-size: 32px; line-height: 1.25; }
</style>
<div id="root" data-composition-id="tips" data-width="1920" data-height="1080">
<div id="tip1" class="clip tip" data-start="0.3" data-duration="2.0" data-track-index="0">
<h2>SET UP</h2><p>Hands on the bench edge</p>
</div>
<div id="tip2" class="clip tip" data-start="2.4" data-duration="1.9" data-track-index="0">
<h2>LOWER SLOWLY</h2><p>Elbows back to 90°</p>
</div>
<div id="tip3" class="clip tip" data-start="4.4" data-duration="1.7" data-track-index="0">
<h2>PUSH UP</h2><p>Press through your palms</p>
</div>
<div id="tip4" class="clip tip" data-start="6.2" data-duration="2.5" data-track-index="0">
<h2>BACK CLOSE TO THE BENCH</h2><p>Hips stay near the edge</p>
</div>
<div id="tip5" class="clip tip" data-start="8.9" data-duration="3.0" data-track-index="0">
<h2>SHOULDERS DOWN</h2><p>Keep them away from your ears</p>
</div>
<div id="tip6" class="clip tip" data-start="12.1" data-duration="2.1" data-track-index="0">
<h2>NICE WORK</h2><p>3 reps done. Rest, then go again</p>
</div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
// Each tip blurs in, holds, and leaves faster than it arrived. Animate the children, never the .clip.
[["tip1", 0.3, 2.0], ["tip2", 2.4, 1.9], ["tip3", 4.4, 1.7], ["tip4", 6.2, 2.5], ["tip5", 8.9, 3.0], ["tip6", 12.1, 2.1]].forEach(([id, s, d]) => {
const kids = "#" + id + " > *";
tl.fromTo(kids, { opacity: 0, filter: "blur(8px)" },
{ opacity: 1, filter: "blur(0px)", duration: 0.22, ease: "power4.out", stagger: 0.06 }, s);
if (id !== "tip6") {
tl.to(kids, { opacity: 0, filter: "blur(8px)", duration: 0.14, ease: "power3.in" }, s + d - 0.2);
}
});
window.__timelines["tips"] = tl;
</script>
</template>
@@ -0,0 +1,33 @@
<template>
<style>
#root { position: absolute; inset: 0; font-family: "Inter", sans-serif; color: #eaf2ff; }
#title-block { position: absolute; left: 192px; top: 108px; }
#title-text { font-family: "Oswald", sans-serif; font-weight: 700; font-size: 96px; line-height: 1; letter-spacing: 0.01em; }
#title-text .w { display: inline-block; margin-right: 24px; }
#title-text .hot { color: var(--accent, #ff7a45); }
#title-muscles { margin-top: 18px; font-weight: 600; font-size: 30px; letter-spacing: 0.22em; }
#title-rule { width: 96px; height: 4px; margin-top: 22px; background: var(--accent, #ff7a45); transform-origin: left center; }
</style>
<div
id="root"
data-composition-id="title"
data-width="1920"
data-height="1080"
data-composition-variables='[{"id":"exercise","type":"string","label":"Exercise (first word)","default":"BENCH"},{"id":"exercise2","type":"string","label":"Exercise (second word)","default":"DIPS"},{"id":"muscles","type":"string","label":"Muscles worked","default":"TRICEPS · SHOULDERS · CHEST"},{"id":"accent","type":"color","label":"Accent colour","default":"#ff7a45"}]'
>
<div id="title-block">
<h1 id="title-text"><span class="w" data-var-text="exercise">BENCH</span><span class="w hot" data-var-text="exercise2">DIPS</span></h1>
<div id="title-muscles" data-var-text="muscles">TRICEPS · SHOULDERS · CHEST</div>
<div id="title-rule"></div>
</div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
tl.fromTo("#title-text .w", { opacity: 0, filter: "blur(8px)" },
{ opacity: 1, filter: "blur(0px)", duration: 0.2, ease: "power4.out", stagger: 0.09 }, 0.3);
tl.fromTo("#title-muscles", { opacity: 0, filter: "blur(6px)" },
{ opacity: 1, filter: "blur(0px)", duration: 0.3, ease: "power3.out" }, 0.65);
tl.fromTo("#title-rule", { scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: "expo.out" }, 0.7);
window.__timelines["title"] = tl;
</script>
</template>
@@ -0,0 +1,15 @@
<template>
<style>
#root { position: absolute; inset: 0; pointer-events: none; }
#wash-top { position: absolute; left: 0; top: 0; width: 100%; height: 420px; background: linear-gradient(to bottom, rgba(5,11,26,0.78), rgba(5,11,26,0)); }
#wash-bottom { position: absolute; left: 0; bottom: 0; width: 100%; height: 440px; background: linear-gradient(to top, rgba(5,11,26,0.82), rgba(5,11,26,0)); }
</style>
<div id="root" data-composition-id="wash" data-width="1920" data-height="1080">
<div id="wash-top" data-layout-ignore></div>
<div id="wash-bottom" data-layout-ignore></div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
window.__timelines["wash"] = tl;
</script>
</template>