Add bodyweight squat video and editable project

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:46:55 +00:00
1 parent 8b1eeff1ed
commit b3600ec8f3
16 files changed
+506 -1

No files matched your search

@@ -0,0 +1,23 @@
<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="2.8" data-track-index="0"><span class="lbl">REP</span><span class="n">1</span> <span class="of">/ 4</span></div>
<div id="rep2" class="clip rep" data-start="5.2" data-duration="2.8" data-track-index="0"><span class="lbl">REP</span><span class="n">2</span> <span class="of">/ 4</span></div>
<div id="rep3" class="clip rep" data-start="8.0" data-duration="2.8" data-track-index="0"><span class="lbl">REP</span><span class="n">3</span> <span class="of">/ 4</span></div>
<div id="rep4" class="clip rep" data-start="10.8" data-duration="2.8" data-track-index="0"><span class="lbl">REP</span><span class="n">4</span> <span class="of">/ 4</span></div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
[["rep1", 2.4], ["rep2", 5.2], ["rep3", 8.0], ["rep4", 10.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 + 2.8 - 0.17);
});
window.__timelines["counter"] = tl;
</script>
</template>
@@ -0,0 +1,112 @@
<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(0.85) translate(-960 -780)">
<ellipse cx="1250" cy="800" rx="520" ry="42" fill="url(#st-pool)"/>
<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; };
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 }));
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 glowDot = (r) => { const c = mk("circle", { r, fill: "#ff7a45" }, body); c.setAttribute("filter", "url(#st-glow)"); return c; };
const setShin = limb(58), setThigh = limb(84), setShorts = limb(108, "#10172f"), setTorso = limb(112, "#27499f"),
setUpper = limb(46), setFore = limb(38), neck = limb(30);
const shoe = mk("rect", { width: 104, height: 26, rx: 12, 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 hand = mk("circle", { r: 20, fill: "#7fa9f0", stroke: "#0b1d44", "stroke-width": 2 }, body);
const quad = glowLine(34), quadGlow = glowLine(48); quadGlow.setAttribute("filter", "url(#st-glow)");
const glute = glowDot(46), core = glowDot(34);
// geometry (px at scale 1): feet planted, joints worked out from the heels up
const AX = 1240, ANK = { x: AX, y: 754 };
const Ls = 185, Lt = 193, T = 218, Lu = 126, Lf = 118;
const A1 = 30 * Math.PI / 180, A2 = 90 * Math.PI / 180, A3 = 40 * Math.PI / 180; // shin lean, thigh drop, torso lean at the bottom
// 4 reps of 2.8 s (1.5 s down, 1.3 s up) after a 2.4 s set-up
const REP0 = 2.4, REP = 2.8, DOWN = 1.5, UP = 1.3, REPS = 4;
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 < REPS; 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 a1 = A1 * p, a2 = A2 * p, a3 = A3 * Math.pow(p, 0.85);
const knee = { x: ANK.x + Ls * Math.sin(a1), y: ANK.y - Ls * Math.cos(a1) };
const hip = { x: knee.x - Lt * Math.sin(a2), y: knee.y - Lt * Math.cos(a2) };
const sh = { x: hip.x + T * Math.sin(a3), y: hip.y - T * Math.cos(a3) };
// arms reach straight forward for balance; elbow bends slightly downward
const tgt = { x: sh.x + 0.97 * (Lu + Lf), y: sh.y + 10 };
const dx = tgt.x - sh.x, dy = tgt.y - sh.y, d = Math.hypot(dx, dy);
const a = (Lu * Lu - Lf * Lf + d * d) / (2 * d), h = Math.sqrt(Math.max(0, Lu * Lu - a * a)), 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.y > e2.y ? e1 : e2;
setShin(ANK, knee); setThigh(knee, hip);
setShorts(hip, { x: hip.x + (knee.x - hip.x) * 0.5, y: hip.y + (knee.y - hip.y) * 0.5 });
shoe.setAttribute("x", ANK.x - 36); shoe.setAttribute("y", ANK.y + 2);
setTorso(hip, sh);
const hd = { x: sh.x + Math.sin(a3) * 74 + 16, y: sh.y - Math.cos(a3) * 74 };
head.setAttribute("cx", hd.x); head.setAttribute("cy", hd.y); neck(sh, { x: hd.x - 6, y: hd.y + 20 });
setUpper(sh, el); setFore(el, tgt);
hand.setAttribute("cx", tgt.x + 6); hand.setAttribute("cy", tgt.y);
// glow follows the work: strongest while driving up, warm at the bottom
const g = 0.22 + 0.78 * push + 0.3 * p * (1 - push);
const qa = { x: knee.x + (hip.x - knee.x) * 0.15, y: knee.y + (hip.y - knee.y) * 0.15 }, qb = { x: knee.x + (hip.x - knee.x) * 0.8, y: knee.y + (hip.y - knee.y) * 0.8 };
// sit the glow on the front of the thigh (rotate the hip-to-knee direction by -90 degrees)
const tx = knee.x - hip.x, ty = knee.y - hip.y, tn = Math.hypot(tx, ty), fx = (ty / tn) * 13, fy = (-tx / tn) * 13;
for (const e of [quad, quadGlow]) { e.setAttribute("x1", qa.x + fx); e.setAttribute("y1", qa.y + fy); e.setAttribute("x2", qb.x + fx); e.setAttribute("y2", qb.y + fy); }
quad.setAttribute("stroke-opacity", Math.min(1, 0.25 + g * 0.8)); quadGlow.setAttribute("stroke-opacity", Math.min(1, g));
glute.setAttribute("cx", hip.x - 34); glute.setAttribute("cy", hip.y + 6); glute.setAttribute("fill-opacity", 0.12 + 0.7 * g);
core.setAttribute("cx", (hip.x + sh.x) / 2 + 26); core.setAttribute("cy", (hip.y + sh.y) / 2); core.setAttribute("fill-opacity", 0.08 + 0.45 * 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: 16.0, duration: 16.0, ease: "none", onUpdate: () => draw(clock.t) }, 0);
window.__timelines["stage"] = tl;
</script>
</template>
@@ -0,0 +1,45 @@
<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>Feet shoulder-width apart</p>
</div>
<div id="tip2" class="clip tip" data-start="2.4" data-duration="2.0" data-track-index="0">
<h2>SIT BACK AND DOWN</h2><p>Knees follow your toes</p>
</div>
<div id="tip3" class="clip tip" data-start="4.5" data-duration="1.7" data-track-index="0">
<h2>DRIVE UP</h2><p>Press through heels</p>
</div>
<div id="tip4" class="clip tip" data-start="6.3" data-duration="2.6" data-track-index="0">
<h2>CHEST UP</h2><p>Keep your back straight</p>
</div>
<div id="tip5" class="clip tip" data-start="9.0" data-duration="2.9" data-track-index="0">
<h2>CORE TIGHT</h2><p>Brace your core</p>
</div>
<div id="tip6" class="clip tip" data-start="12.0" data-duration="1.6" data-track-index="0">
<h2>FINISH STRONG</h2><p>Last rep</p>
</div>
<div id="tip7" class="clip tip" data-start="13.7" data-duration="2.3" data-track-index="0">
<h2>GREAT SET</h2><p>4 reps done. 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, 2.0], ["tip3", 4.5, 1.7], ["tip4", 6.3, 2.6], ["tip5", 9.0, 2.9], ["tip6", 12.0, 1.6], ["tip7", 13.7, 2.3]].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 !== "tip7") {
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":"BODYWEIGHT"},{"id":"exercise2","type":"string","label":"Exercise (second word)","default":"SQUAT"},{"id":"muscles","type":"string","label":"Muscles worked","default":"QUADS · GLUTES · CORE"},{"id":"accent","type":"color","label":"Accent colour","default":"#ff7a45"}]'
>
<div id="title-block">
<h1 id="title-text"><span class="w" data-var-text="exercise">BODYWEIGHT</span><span class="w hot" data-var-text="exercise2">SQUAT</span></h1>
<div id="title-muscles" data-var-text="muscles">QUADS · GLUTES · CORE</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>