mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(motion): preserve reference assets and exported animation frames
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Motion template sources and maintenance
|
||||
|
||||
These five examples extend the existing official HTML template mechanism:
|
||||
`SKILL.md`, `example.html`, `open-design.json`, curated Home chips, and Community
|
||||
cards. They use the existing preview baker, example binding, frozen reference
|
||||
package, Remix, and HyperFrames MP4 exporter.
|
||||
|
||||
## Content provenance
|
||||
|
||||
The original October 8–9, 2026 work used the Opus 5.5 video community to choose
|
||||
topics, including the collection at
|
||||
[zhuyansen/awesome-opus-5.5-video](https://github.com/zhuyansen/awesome-opus-5.5-video)
|
||||
and discovery through
|
||||
[yihui-dev/awesome-opus5-5-videos](https://github.com/yihui-dev/awesome-opus5-5-videos)
|
||||
and [Skillry's collection](https://skillry.dev/ai-videos/opus-5-5).
|
||||
View-count ranking was an informal topic-selection aid, not evidence that these
|
||||
are recreations of the five most popular individual videos.
|
||||
|
||||
Prompts were written for OD. HTML previews were generated in OD, then packaged
|
||||
with local script dependencies, poster-ready markers, and export-safe drawing.
|
||||
The source-project mapping comes from the original generator's `sources.json`;
|
||||
all five original `index.html` files were still present during the handoff audit.
|
||||
|
||||
| Template | Original OD project |
|
||||
| --- | --- |
|
||||
| Procedural 3D island | `5873852b-3e24-4734-a909-903ad038a8e3` |
|
||||
| Explainer | `baab39ba-f4ee-4261-a302-a98cd2c58dac` |
|
||||
| Product launch | `d38769ce-f3d6-4451-9977-72887e551fce` |
|
||||
| Showreel | `aebf983b-6223-46eb-bf63-4f7d03087695` |
|
||||
| Data story | `515296bd-a73b-475d-b343-ee1451b2b695` |
|
||||
|
||||
Driftnote and Studio Kinetik are fictional examples. Chart figures are sample
|
||||
content, not a verified business dataset. Preserve that distinction when
|
||||
adapting a template.
|
||||
|
||||
## Local dependencies and export
|
||||
|
||||
The bundled script versions are GSAP 3.12.5 and Three.js 0.160.0, originally
|
||||
retrieved from their version-pinned jsDelivr package URLs. No runtime script CDN
|
||||
is required. Each SKILL explicitly names the dependencies that the existing
|
||||
frozen-package scanner should carry.
|
||||
|
||||
Three.js exceeds the existing 256 KiB side-file limit. The gallery and Remix
|
||||
use the original `assets/three.min.js`; the frozen reference carries an identical
|
||||
gzip copy, with decompression instructions in its SKILL. When changing the
|
||||
vendored Three.js file, regenerate its compressed copy deterministically:
|
||||
|
||||
```sh
|
||||
gzip -n -9 -c plugins/_official/examples/motion-3d-scene/assets/three.min.js > plugins/_official/examples/motion-3d-scene/assets/three.min.js.gz
|
||||
```
|
||||
|
||||
The package regression checks that all referenced local scripts can be restored
|
||||
byte-for-byte. The browser regression checks callback-suppressed export seeking,
|
||||
including backward scrubbing of the Showreel's slider, graph, and signature.
|
||||
Keep procedural drawing in the shared per-frame renderer: nested tween callbacks
|
||||
alone do not execute during MP4 export.
|
||||
@@ -0,0 +1,44 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gunzipSync } from 'node:zlib';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
captureFrozenSkillPackageFromSources,
|
||||
materializeFrozenSkillPackage,
|
||||
} from '../../../apps/daemon/src/strategies/od-next/frozen-skill-package.js';
|
||||
|
||||
const examples = fileURLToPath(new URL('../../../plugins/_official/examples/', import.meta.url));
|
||||
|
||||
test.each(['3d-scene', 'explainer', 'product-launch', 'showreel', 'data-story'])(
|
||||
'[P1] motion-%s carries its local script dependencies through the frozen reference package',
|
||||
async (name) => {
|
||||
const source = path.join(examples, `motion-${name}`);
|
||||
const frozen = await captureFrozenSkillPackageFromSources({ sources: [{ dir: source, name: `Motion ${name}` }] });
|
||||
const project = await mkdtemp(path.join(tmpdir(), 'motion-reference-'));
|
||||
try {
|
||||
const [relativeRoot] = await materializeFrozenSkillPackage({ frozen, cwd: project });
|
||||
const reference = path.join(project, relativeRoot!);
|
||||
const html = await readFile(path.join(reference, 'example.html'), 'utf8');
|
||||
const skill = await readFile(path.join(reference, 'SKILL.md'), 'utf8');
|
||||
// Follow the template's documented offline preparation step for Three.js.
|
||||
if (name === '3d-scene') {
|
||||
expect(skill).toContain('gzip -dc');
|
||||
const bytes = gunzipSync(await readFile(path.join(reference, 'assets/three.min.js.gz')));
|
||||
await writeFile(path.join(reference, 'assets/three.min.js'), bytes);
|
||||
}
|
||||
const scripts = [...html.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["']/g)];
|
||||
expect(scripts.length).toBeGreaterThan(0);
|
||||
for (const [, src] of scripts) {
|
||||
expect(src).not.toMatch(/^https?:/);
|
||||
const relative = src!.replace(/^\.\//, '');
|
||||
expect(await readFile(path.join(reference, relative))).toEqual(await readFile(path.join(source, relative)));
|
||||
}
|
||||
} finally {
|
||||
await rm(project, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from '@/playwright/suite';
|
||||
|
||||
for (const name of ['3d-scene', 'explainer', 'product-launch', 'showreel', 'data-story']) {
|
||||
test(`[P1] motion-${name} exports the full composition without preview controls`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
// This is the marker installed by the daemon's existing frame bridge.
|
||||
// Actual MP4 encoding is verified separately through that renderer.
|
||||
await page.addInitScript(() => {
|
||||
Object.assign(window, { __odFrameRenderer: {} });
|
||||
});
|
||||
await page.goto(`/api/plugins/example-motion-${name}/preview`);
|
||||
await expect(page.locator('html')).toHaveClass(/od-export/);
|
||||
await expect(page.locator('#controls')).toBeHidden();
|
||||
const bounds = await page.locator('[data-composition-id]').boundingBox();
|
||||
expect(bounds).toEqual({ x: 0, y: 0, width: 1920, height: 1080 });
|
||||
});
|
||||
}
|
||||
|
||||
test('[P1] showreel procedural layers survive export seeks and backward scrubbing', async ({ page }) => {
|
||||
await page.goto('/api/plugins/example-motion-showreel/preview');
|
||||
await page.waitForFunction(() => Boolean((window as unknown as { __timelines?: unknown }).__timelines));
|
||||
const samples = await page.evaluate(async () => {
|
||||
const tl = (window as unknown as {
|
||||
__timelines: { main: { pause(): void; totalTime(t: number, suppressEvents: boolean): void } };
|
||||
}).__timelines.main;
|
||||
tl.pause();
|
||||
const result = [];
|
||||
for (const time of [8.7, 11.4, 14, 8.7, 11.4, 0, 14]) {
|
||||
tl.totalTime(time, true);
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
result.push({
|
||||
time,
|
||||
slider: document.querySelector('#sRead')?.textContent,
|
||||
number: document.querySelector('#bigNum')?.textContent,
|
||||
line: (document.querySelector('#linePath') as SVGElement).style.strokeDashoffset,
|
||||
dot: (document.querySelector('#morph') as HTMLElement).style.transform,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
expect(samples[0]?.slider).toBe('63%');
|
||||
expect(samples[1]?.number).toBe('+211%');
|
||||
expect(Number(samples[1]?.line)).toBeGreaterThan(0);
|
||||
expect(samples[3]).toEqual(samples[0]);
|
||||
expect(samples[4]).toEqual(samples[1]);
|
||||
expect(samples[5]?.slider).toBe('20%');
|
||||
expect(samples[5]?.number).toBe('+0%');
|
||||
expect(samples[6]).toEqual(samples[2]);
|
||||
});
|
||||
@@ -15,6 +15,18 @@ A stylized 3D motion piece rendered live in the browser with Three.js: a small
|
||||
procedural island at golden hour, one continuous camera move, a calm title
|
||||
close. `example.html` is the finished reference — start from it.
|
||||
|
||||
## Reference files
|
||||
- Copy `example.html` and `assets/gsap.min.js` into the new composition,
|
||||
preserving the relative `assets/` path. These files are carried in the frozen
|
||||
reference package; use these local bytes instead of a CDN or a new version.
|
||||
- The gallery uses `assets/three.min.js`. The frozen reference package carries
|
||||
the same Three.js bytes compressed as `assets/three.min.js.gz` to fit the
|
||||
existing per-file budget. Before rendering, create the composition's assets
|
||||
directory and decompress this file into `assets/three.min.js`:
|
||||
`gzip -dc "<reference-root>/assets/three.min.js.gz" > "<composition-root>/assets/three.min.js"`.
|
||||
Both placeholders are the actual reference and composition directories in
|
||||
this task. Keep the HTML script URL pointing to the decompressed `.js` file.
|
||||
|
||||
## Art direction
|
||||
- Toon-shaded low-poly world: faceted island (sand, grass mound, rocks), palm
|
||||
trees with drooping fronds, soft puffy clouds. 3-step toon light ramp, warm
|
||||
@@ -47,3 +59,6 @@ close. `example.html` is the finished reference — start from it.
|
||||
must not depend on `onUpdate`/`onComplete`; procedural layers redraw from the
|
||||
timeline time on every animation frame, as the per-frame loop in
|
||||
`example.html` does.
|
||||
- Keep playback controls outside the composition. The example detects the
|
||||
existing OD frame-export bridge and fills its capture viewport with only the
|
||||
composition; preserve this export layout when adapting the preview shell.
|
||||
|
||||
Binary file not shown.
@@ -36,6 +36,12 @@
|
||||
#controls button:focus-visible, #scrub:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
|
||||
#scrub { flex: 1; accent-color: var(--coral); min-height: 44px; }
|
||||
#time { font-variant-numeric: tabular-nums; min-width: 96px; text-align: right; font-size: 16px; }
|
||||
|
||||
/* The existing OD export bridge marks frame capture; keep preview controls out. */
|
||||
html.od-export body { margin:0!important; padding:0!important; gap:0!important; }
|
||||
html.od-export #controls { display:none!important; }
|
||||
html.od-export #viewport { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; display:block!important; }
|
||||
html.od-export #scaler, html.od-export #main { position:absolute!important; left:0!important; top:0!important; margin:0!important; transform:none!important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -391,6 +397,9 @@ syncUI();
|
||||
var drawnAt = null;
|
||||
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
|
||||
(function loop() {
|
||||
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
|
||||
document.documentElement.classList.add('od-export');
|
||||
}
|
||||
var t = tl.time();
|
||||
if (t !== drawnAt) { drawnAt = t; draw(); }
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
@@ -15,6 +15,11 @@ An animated data story where the shrinking gaps between milestones are the
|
||||
story: how long it took humanity to add each billion people (UN milestones
|
||||
1804 → 2022). `example.html` is the finished reference — start from it.
|
||||
|
||||
## Reference files
|
||||
- Copy `example.html` and `assets/gsap.min.js` into the new composition,
|
||||
preserving the relative `assets/` path. These files are carried in the frozen
|
||||
reference package; use these local bytes instead of a CDN or a new version.
|
||||
|
||||
## Art direction
|
||||
- Editorial print look: paper `#FAFAF7`, ink `#111111`, a single red highlight
|
||||
`#D7261E`. Serif headline, small sans/mono labels, thin black rules.
|
||||
@@ -42,3 +47,6 @@ story: how long it took humanity to add each billion people (UN milestones
|
||||
must not depend on `onUpdate`/`onComplete`; procedural layers redraw from the
|
||||
timeline time on every animation frame, as the per-frame loop in
|
||||
`example.html` does.
|
||||
- Keep playback controls outside the composition. The example detects the
|
||||
existing OD frame-export bridge and fills its capture viewport with only the
|
||||
composition; preserve this export layout when adapting the preview shell.
|
||||
|
||||
@@ -40,6 +40,12 @@ body{display:flex;flex-direction:column;align-items:center;justify-content:cente
|
||||
#controls button{font:600 16px var(--sans);padding:8px 16px;border:2px solid var(--ink);background:var(--paper);color:var(--ink);cursor:pointer}
|
||||
#controls button:focus-visible,#scrub:focus-visible{outline:3px solid var(--red);outline-offset:2px}
|
||||
#scrub{width:520px}
|
||||
|
||||
/* The existing OD export bridge marks frame capture; keep preview controls out. */
|
||||
html.od-export body { margin:0!important; padding:0!important; gap:0!important; }
|
||||
html.od-export #controls { display:none!important; }
|
||||
html.od-export #viewport { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; display:block!important; }
|
||||
html.od-export #scaler, html.od-export #main { position:absolute!important; left:0!important; top:0!important; margin:0!important; transform:none!important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -211,6 +217,9 @@ else { tl.play(); }
|
||||
var drawnAt = null;
|
||||
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
|
||||
(function loop() {
|
||||
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
|
||||
document.documentElement.classList.add('od-export');
|
||||
}
|
||||
var t = tl.time();
|
||||
if (t !== drawnAt) { drawnAt = t; draw(); }
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
@@ -15,6 +15,11 @@ A technical explainer that follows one recurring object — a waveform — throu
|
||||
cause and effect: how noise-cancelling headphones work. `example.html` is the
|
||||
finished reference — start from it.
|
||||
|
||||
## Reference files
|
||||
- Copy `example.html` and `assets/gsap.min.js` into the new composition,
|
||||
preserving the relative `assets/` path. These files are carried in the frozen
|
||||
reference package; use these local bytes instead of a CDN or a new version.
|
||||
|
||||
## Art direction
|
||||
- Clean technical diagram on dark graphite `#14171a`; cyan `#22d3ee` for the
|
||||
active signal, white `#f5f7f8` lines, dim `#8a9aa3` for secondary labels.
|
||||
@@ -45,3 +50,6 @@ finished reference — start from it.
|
||||
must not depend on `onUpdate`/`onComplete`; procedural layers redraw from the
|
||||
timeline time on every animation frame, as the per-frame loop in
|
||||
`example.html` does.
|
||||
- Keep playback controls outside the composition. The example detects the
|
||||
existing OD frame-export bridge and fills its capture viewport with only the
|
||||
composition; preserve this export layout when adapting the preview shell.
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
#controls button:focus-visible, #scrub:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
|
||||
#scrub { flex: 1; accent-color: var(--cyan); }
|
||||
#time { font-size: 14px; color: var(--dim); min-width: 90px; text-align: right; }
|
||||
|
||||
/* The existing OD export bridge marks frame capture; keep preview controls out. */
|
||||
html.od-export body { margin:0!important; padding:0!important; gap:0!important; }
|
||||
html.od-export #controls { display:none!important; }
|
||||
html.od-export #viewport { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; display:block!important; }
|
||||
html.od-export #scaler, html.od-export #main { position:absolute!important; left:0!important; top:0!important; margin:0!important; transform:none!important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -263,6 +269,9 @@ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
var drawnAt = null;
|
||||
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
|
||||
(function loop() {
|
||||
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
|
||||
document.documentElement.classList.add('od-export');
|
||||
}
|
||||
var t = tl.time();
|
||||
if (t !== drawnAt) { drawnAt = t; draw(); }
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
@@ -16,6 +16,11 @@ becomes a clean result, then the brand resolves. Shown with the fictional app
|
||||
Driftnote (meeting audio → one-page summary). `example.html` is the finished
|
||||
reference — start from it.
|
||||
|
||||
## Reference files
|
||||
- Copy `example.html` and `assets/gsap.min.js` into the new composition,
|
||||
preserving the relative `assets/` path. These files are carried in the frozen
|
||||
reference package; use these local bytes instead of a CDN or a new version.
|
||||
|
||||
## Art direction
|
||||
- Deep navy stage `#0B1530`, warm paper text `#F4EFE6`, one coral accent
|
||||
`#FF6B57`. Editorial serif for display, clean sans for UI, mono for transcript
|
||||
@@ -46,3 +51,6 @@ reference — start from it.
|
||||
must not depend on `onUpdate`/`onComplete`; procedural layers redraw from the
|
||||
timeline time on every animation frame, as the per-frame loop in
|
||||
`example.html` does.
|
||||
- Keep playback controls outside the composition. The example detects the
|
||||
existing OD frame-export bridge and fills its capture viewport with only the
|
||||
composition; preserve this export layout when adapting the preview shell.
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
#scrub { flex: 1; accent-color: var(--coral); min-height: 40px; }
|
||||
#time { min-width: 110px; text-align: right; }
|
||||
#rm-note { display: none; color: var(--coral); }
|
||||
|
||||
/* The existing OD export bridge marks frame capture; keep preview controls out. */
|
||||
html.od-export body { margin:0!important; padding:0!important; gap:0!important; }
|
||||
html.od-export #controls { display:none!important; }
|
||||
html.od-export #viewport { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; display:block!important; }
|
||||
html.od-export #scaler, html.od-export #main { position:absolute!important; left:0!important; top:0!important; margin:0!important; transform:none!important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -270,6 +276,9 @@ const start = () => {
|
||||
var drawnAt = null;
|
||||
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
|
||||
(function loop() {
|
||||
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
|
||||
document.documentElement.classList.add('od-export');
|
||||
}
|
||||
var t = tl.time();
|
||||
if (t !== drawnAt) { drawnAt = t; draw(); }
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
@@ -15,6 +15,11 @@ A 15-second motion designer showreel: several distinct techniques inside one
|
||||
visual identity, linked by a recurring accent dot. Signed by the fictional
|
||||
"Studio Kinetik". `example.html` is the finished reference — start from it.
|
||||
|
||||
## Reference files
|
||||
- Copy `example.html` and `assets/gsap.min.js` into the new composition,
|
||||
preserving the relative `assets/` path. These files are carried in the frozen
|
||||
reference package; use these local bytes instead of a CDN or a new version.
|
||||
|
||||
## Art direction
|
||||
- Off-white `#F4F4F1`, ink-black `#0E0E10`, a single vivid accent `#FF3D00`.
|
||||
- Heavy display grotesque (900 weight, very large) for kinetic type; uppercase
|
||||
@@ -42,3 +47,6 @@ visual identity, linked by a recurring accent dot. Signed by the fictional
|
||||
must not depend on `onUpdate`/`onComplete`; procedural layers redraw from the
|
||||
timeline time on every animation frame, as the per-frame loop in
|
||||
`example.html` does.
|
||||
- Keep playback controls outside the composition. The example detects the
|
||||
existing OD frame-export bridge and fills its capture viewport with only the
|
||||
composition; preserve this export layout when adapting the preview shell.
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
/* UI card */
|
||||
#card{left:360px;top:200px;width:1200px;height:720px;background:#FFFFFF;border:3px solid var(--ink);border-radius:24px;opacity:0;z-index:5}
|
||||
#card .abs, .uiItem{opacity:0}
|
||||
.uiItem{z-index:6}
|
||||
.uiLabel{font-family:var(--mono);font-weight:700;font-size:28px;letter-spacing:.06em;color:var(--ink);text-transform:uppercase}
|
||||
#tTrack{left:1240px;top:300px;width:160px;height:80px;border-radius:40px;border:3px solid var(--ink);background:var(--bg);z-index:6}
|
||||
#tKnob{left:1252px;top:312px;width:56px;height:56px;border-radius:28px;background:var(--ink);z-index:7}
|
||||
@@ -82,6 +83,12 @@
|
||||
#controls button:focus-visible,#controls input:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
|
||||
#scrub{flex:1;accent-color:#FF3D00;height:24px}
|
||||
#tc{min-width:120px;text-align:right}
|
||||
|
||||
/* The existing OD export bridge marks frame capture; keep preview controls out. */
|
||||
html.od-export body { margin:0!important; padding:0!important; gap:0!important; }
|
||||
html.od-export #controls { display:none!important; }
|
||||
html.od-export #viewport { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; display:block!important; }
|
||||
html.od-export #scaler, html.od-export #main { position:absolute!important; left:0!important; top:0!important; margin:0!important; transform:none!important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -276,12 +283,7 @@ const sx = p => SL.left + p*SL.w;
|
||||
tl.to('#cursor',{x:sx(0.2),y:SL.y,duration:0.35,ease:'power2.inOut'},7.8);
|
||||
tl.to('#cursor',{scale:0.82,duration:0.08},8.15);
|
||||
tl.to('#cursor',{x:sx(0.72),duration:0.7,ease:'power2.inOut'},8.25);
|
||||
tl.to(ui,{p:0.72,duration:0.7,ease:'power2.inOut',
|
||||
onUpdate(){
|
||||
gsap.set('#sKnob',{x:sx(ui.p)});
|
||||
gsap.set('#sFill',{scaleX:ui.p});
|
||||
$('#sRead').textContent = Math.round(ui.p*100)+'%';
|
||||
}},8.25);
|
||||
tl.to(ui,{p:0.72,duration:0.7,ease:'power2.inOut'},8.25);
|
||||
tl.to('#cursor',{scale:1,duration:0.12},8.95);
|
||||
tl.to('#cursor',{x:CFG.cardBtn.x+40,y:CFG.cardBtn.y,duration:0.4,ease:'power2.inOut'},9.0);
|
||||
tl.to('#cursor',{scale:0.82,duration:0.08},9.4);
|
||||
@@ -308,13 +310,7 @@ CFG.bars.forEach((_,i)=>{
|
||||
tl.to(['#bigNum','#bigCap'],{opacity:1,duration:0.3,ease:'power2.out'},10.4);
|
||||
tl.to('#bigNote',{opacity:1,duration:0.3},11.2);
|
||||
const dp = { p:0 };
|
||||
tl.to(dp,{p:1,duration:0.9,ease:'power2.inOut',
|
||||
onUpdate(){
|
||||
path.style.strokeDashoffset = pathLen*(1-dp.p);
|
||||
const pt = path.getPointAtLength(pathLen*dp.p);
|
||||
gsap.set('#morph',{x:pt.x,y:pt.y});
|
||||
$('#bigNum').textContent = '+'+Math.round(dp.p*248)+'%';
|
||||
}},10.8);
|
||||
tl.to(dp,{p:1,duration:0.9,ease:'power2.inOut'},10.8);
|
||||
tl.fromTo('#rip1',{x:lastX.x,y:lastX.y,scale:0.4,opacity:1},{scale:3,opacity:0,duration:0.5,ease:'power2.out'},11.7);
|
||||
tl.to('#morph',{scale:1.5,duration:0.15,ease:'power2.out',yoyo:true,repeat:1},11.7);
|
||||
|
||||
@@ -326,7 +322,7 @@ CFG.bars.forEach((_,i)=>{
|
||||
});
|
||||
|
||||
/* Beat 5 — signature (12.4 – 13.0, then hold to 15.0) */
|
||||
tl.to('#morph',{x:()=>offsetCenter($('#p2')).x,y:()=>offsetCenter($('#p2')).y,width:36,height:36,borderRadius:18,scale:1,duration:0.55,ease:'power3.inOut'},12.4);
|
||||
tl.fromTo('#morph',{x:lastX.x,y:lastX.y},{x:()=>offsetCenter($('#p2')).x,y:()=>offsetCenter($('#p2')).y,width:36,height:36,borderRadius:18,scale:1,duration:0.55,ease:'power3.inOut',immediateRender:false},12.4);
|
||||
tl.set('#sigWrap',{opacity:1},12.4);
|
||||
tl.to('#sigText',{yPercent:0,duration:0.5,ease:'power4.out'},12.45);
|
||||
tl.set('#frame',{opacity:1},12.4);
|
||||
@@ -340,6 +336,17 @@ const fmt = t => {
|
||||
};
|
||||
tl.eventCallback('onUpdate',()=>{
|
||||
const t = clock.t;
|
||||
// Rebuild every procedural layer here so suppressed export seeks can use
|
||||
// the same renderer via the per-frame loop below, including backward seeks.
|
||||
gsap.set('#sKnob',{x:sx(ui.p)});
|
||||
gsap.set('#sFill',{scaleX:ui.p});
|
||||
$('#sRead').textContent = Math.round(ui.p*100)+'%';
|
||||
path.style.strokeDashoffset = pathLen*(1-dp.p);
|
||||
$('#bigNum').textContent = '+'+Math.round(dp.p*248)+'%';
|
||||
if(t>=10.8 && t<12.4){
|
||||
const pt = path.getPointAtLength(pathLen*dp.p);
|
||||
gsap.set('#morph',{x:pt.x,y:pt.y});
|
||||
}
|
||||
$('#tcStage').textContent = fmt(t);
|
||||
gsap.set('#progressFill',{scaleX:t/CFG.duration});
|
||||
});
|
||||
@@ -391,6 +398,9 @@ sync();
|
||||
var drawnAt = null;
|
||||
tl.eventCallback('onUpdate', function () { drawnAt = tl.time(); return draw.apply(this, arguments); });
|
||||
(function loop() {
|
||||
if (window.__odFrameRenderer && !document.documentElement.classList.contains('od-export')) {
|
||||
document.documentElement.classList.add('od-export');
|
||||
}
|
||||
var t = tl.time();
|
||||
if (t !== drawnAt) { drawnAt = t; draw(); }
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
Reference in New Issue
Block a user