diff --git a/docs/contributing.mdx b/docs/contributing.mdx
index 51d544e74c..e4bab5dfed 100644
--- a/docs/contributing.mdx
+++ b/docs/contributing.mdx
@@ -89,16 +89,10 @@ bun run --filter '*' test
| Package | Path | Description |
|---------|------|-------------|
| [`@hyperframes/core`](/packages/core) | `packages/core` | Types, HTML generation, runtime, linter |
-| [`@hyperframes/sdk`](/packages/sdk) | `packages/sdk` | Headless composition editing engine |
| [`@hyperframes/engine`](/packages/engine) | `packages/engine` | Seekable page-to-video capture engine |
-| [`@hyperframes/player`](/packages/player) | `packages/player` | Embeddable composition player |
| [`@hyperframes/producer`](/packages/producer) | `packages/producer` | Full rendering pipeline (capture + encode) |
-| [`@hyperframes/shader-transitions`](/packages/shader-transitions) | `packages/shader-transitions` | WebGL shader transition engine |
-| [`@hyperframes/aws-lambda`](/packages/aws-lambda) | `packages/aws-lambda` | AWS Lambda distributed rendering adapter |
-| [`@hyperframes/gcp-cloud-run`](/packages/gcp-cloud-run) | `packages/gcp-cloud-run` | GCP Cloud Run distributed rendering adapter |
| [`@hyperframes/studio`](/packages/studio) | `packages/studio` | Composition editor UI |
| [`hyperframes`](/packages/cli) | `packages/cli` | CLI for creating, previewing, and rendering |
-| `@hyperframes/sdk-playground` (private) | `packages/sdk-playground` | Local SDK playground app |
## What to Work On
diff --git a/docs/docs.json b/docs/docs.json
index 7e8ac25996..e0fb7919dc 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -302,13 +302,9 @@
"group": "Packages",
"pages": [
"packages/core",
- "packages/sdk",
"packages/engine",
"packages/player",
"packages/producer",
- "packages/shader-transitions",
- "packages/aws-lambda",
- "packages/gcp-cloud-run",
"packages/studio",
"packages/cli"
]
diff --git a/docs/introduction.mdx b/docs/introduction.mdx
index 671310dd51..72bb514cde 100644
--- a/docs/introduction.mdx
+++ b/docs/introduction.mdx
@@ -79,27 +79,12 @@ Run `npx hyperframes render --output demo.mp4` and this produces an MP4 with det
Types, HTML parsing, runtime, and composition linter — the foundation everything else builds on.
-
- Headless composition editing engine for agents, custom editors, patch events, and persistence.
-
Seekable page-to-video capture engine. Loads HTML in headless Chrome and captures frame-by-frame.
-
- Embeddable web component for playing HyperFrames compositions in any web page.
-
Full rendering pipeline combining capture and FFmpeg encoding into a single API call.
-
- WebGL shader transitions for scene-to-scene motion and render-time compositing.
-
-
- AWS Lambda and Step Functions adapter for distributed rendering.
-
-
- Google Cloud Run and Workflows adapter for distributed rendering.
-
Visual composition editor UI for building and previewing timelines interactively.
diff --git a/packages/core/src/parsers/gsapParser.ts b/packages/core/src/parsers/gsapParser.ts
index a2f1047128..29f3c5c5cd 100644
--- a/packages/core/src/parsers/gsapParser.ts
+++ b/packages/core/src/parsers/gsapParser.ts
@@ -1243,13 +1243,17 @@ function applyEaseUpdate(varsArg: AstNode, ease: string): void {
}
}
-function applyUpdatesToCall(call: TweenCallInfo, updates: Partial): void {
+function applyUpdatesToCall(
+ call: TweenCallInfo,
+ updates: Partial & { easeEach?: string },
+): void {
if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties);
if (updates.fromProperties && call.method === "fromTo" && call.fromArg) {
reconcileEditableProperties(call.fromArg, updates.fromProperties);
}
if (updates.duration !== undefined) setVarsKey(call.varsArg, "duration", updates.duration);
- if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);
+ if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach);
+ else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);
if (updates.position !== undefined) {
const posIdx = call.method === "fromTo" ? 3 : 2;
call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));
@@ -1308,7 +1312,7 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit,
+ updates: Partial & { easeEach?: string },
): string {
let parsed: ParsedGsapAst;
try {
@@ -1437,6 +1441,7 @@ export function addAnimationWithKeyframesToScript(
auto?: boolean;
}>,
ease?: string,
+ easeEach?: string,
): { script: string; id: string } {
let parsed: ParsedGsapAst;
try {
@@ -1450,7 +1455,7 @@ export function addAnimationWithKeyframesToScript(
}
const selector = JSON.stringify(targetSelector);
- const kfCode = buildKeyframeObjectCode(keyframes);
+ const kfCode = buildKeyframeObjectCode(keyframes, easeEach ? { easeEach } : undefined);
const varEntries = [`keyframes: ${kfCode}`, `duration: ${valueToCode(duration)}`];
if (ease) varEntries.push(`ease: ${JSON.stringify(ease)}`);
const posCode = valueToCode(position);
diff --git a/packages/core/src/parsers/gsapWriterAcorn.ts b/packages/core/src/parsers/gsapWriterAcorn.ts
index 568bc255ab..c4ef2998aa 100644
--- a/packages/core/src/parsers/gsapWriterAcorn.ts
+++ b/packages/core/src/parsers/gsapWriterAcorn.ts
@@ -299,7 +299,7 @@ function findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {
export function updateAnimationInScript(
script: string,
animationId: string,
- updates: Partial,
+ updates: Partial & { easeEach?: string },
): string {
if (!Object.keys(updates).length) return script;
const parsed = parseGsapScriptAcornForWrite(script);
@@ -324,13 +324,11 @@ export function updateAnimationInScript(
if (updates.duration !== undefined) {
upsertProp(ms, call.varsArg, "duration", updates.duration);
}
- if (updates.ease !== undefined) {
- // For a keyframe tween, easing lives at keyframes.easeEach (per-keyframe),
- // not a top-level ease. Writing top-level ease would leave the per-keyframe
- // easing unchanged — the user's edit would silently do nothing.
+ const easeValue = updates.easeEach ?? updates.ease;
+ if (easeValue !== undefined) {
const kfNode = keyframesObjectNode(call.varsArg);
- if (kfNode) upsertProp(ms, kfNode, "easeEach", updates.ease);
- else upsertProp(ms, call.varsArg, "ease", updates.ease);
+ if (kfNode) upsertProp(ms, kfNode, "easeEach", easeValue);
+ else upsertProp(ms, call.varsArg, "ease", easeValue);
}
if (updates.extras) {
for (const [key, value] of Object.entries(updates.extras)) {
@@ -1338,6 +1336,7 @@ export function addAnimationWithKeyframesToScript(
auto?: boolean;
}>,
ease?: string,
+ easeEach?: string,
): { script: string; id: string } {
const parsed = parseGsapScriptAcornForWrite(script);
if (!parsed) return { script, id: "" };
@@ -1345,7 +1344,7 @@ export function addAnimationWithKeyframesToScript(
if (insertionPoint === null) return { script, id: "" };
const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
- const kfObjCode = buildKeyframeObjectCode(sorted);
+ const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);
const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];
if (ease) varParts.push(`ease: ${JSON.stringify(ease)}`);
const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(", ")} }, ${valueToCode(position)});`;
diff --git a/packages/core/src/runtime/media.ts b/packages/core/src/runtime/media.ts
index aa8e1b71da..bb67ab9d9b 100644
--- a/packages/core/src/runtime/media.ts
+++ b/packages/core/src/runtime/media.ts
@@ -290,33 +290,18 @@ export function syncRuntimeMedia(params: {
}
const forceSync = !isPlayingVideo && params.forceSync && drift > 0.02;
if (hardSync || strictSync || forceSync) {
- // Skip the per-tick seek (and the `el.load()` drift-recovery retry
- // below) for `