From 8548a1777138e369cb340626f5c068cb740d625b Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 19 Apr 2026 17:03:29 -0700 Subject: [PATCH] feat(hdr): GSAP transforms and border-radius masks on HDR video (#290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary HDR video elements with GSAP animations (position, scale, rotation, opacity) and CSS border-radius rendered without any transforms applied — the video just sat at (0,0) full-size. This PR adds affine transform support and rounded-corner masking for natively-composited HDR video. ## What it does **Affine blit with bilinear interpolation:** - `blitRgb48leAffine()` — Takes a 4x4 DOMMatrix and maps each destination pixel back to source coordinates via the inverse transform. Bilinear interpolation between the 4 nearest source pixels produces smooth edges under rotation and non-integer scaling. Optional opacity and border-radius parameters. - `parseTransformMatrix()` — Parses CSS `matrix(a,b,c,d,e,f)` strings into `[a,b,c,d,e,f]` tuples. **Accumulated viewport matrix:** - `getViewportMatrix()` — Walks the `offsetParent` chain from element to viewport, accumulating position offsets and CSS transforms at each level. Correctly handles `transform-origin` using the CSS sandwich: `translate(origin) × M × translate(-origin)`. This is critical because GSAP animates transforms on wrapper divs, not directly on the video element. **Effective opacity:** - `getEffectiveOpacity()` — Multiplies opacity values walking up the ancestor chain. Uses `Number.isNaN()` (not `|| 1`) so opacity:0 isn't incorrectly treated as 1. **Border-radius masks:** - `roundedRectAlpha()` — Per-pixel anti-aliased rounded-rectangle mask with support for independent corner radii. - `getEffectiveBorderRadius()` — Walks ancestors for `overflow:hidden` + border-radius. Resolves percentage values (e.g., `50%` for circles) via `offsetWidth`/`offsetHeight`. **Layout dimensions for extraction:** - Uses `offsetWidth`/`offsetHeight` (unaffected by CSS transforms) instead of `getBoundingClientRect()` (which returns the transformed bounding box and wobbles under rotation). ## Files changed | File | What changed | |------|-------------| | `packages/engine/src/utils/alphaBlit.ts` | `blitRgb48leAffine()`, `parseTransformMatrix()`, `roundedRectAlpha()`, `cornerAlpha()` | | `packages/engine/src/services/videoFrameInjector.ts` | `getViewportMatrix()`, `getEffectiveOpacity()`, `getEffectiveBorderRadius()`, `layoutWidth`/`layoutHeight` on `ElementStackingInfo` | | `packages/producer/src/services/renderOrchestrator.ts` | Affine blit path, extraction at layout dimensions, border-radius parameter passing | ## How to test Render a composition with an HDR video that has GSAP scale + rotation animation and a `border-radius: 50%` wrapper (circle mask). The video should rotate smoothly with round edges — no wobble, no sharp corners. ## Stack position **5 of 6** — Stacked on #289 (z-ordered layers). Adds transform and masking support to the HDR blit that the layer compositor uses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- packages/engine/src/index.ts | 2 + .../engine/src/services/videoFrameInjector.ts | 144 +++++++++++++++++- .../src/services/renderOrchestrator.ts | 105 ++++++++++--- .../shader-transitions/src/hyper-shader.ts | 59 +++++++ 4 files changed, 284 insertions(+), 26 deletions(-) diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 6050ac5cc..741c42efd 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -161,6 +161,8 @@ export { decodePngToRgb48le, blitRgba8OverRgb48le, blitRgb48leRegion, + blitRgb48leAffine, + parseTransformMatrix, getSrgbToHdrLut, } from "./utils/alphaBlit.js"; diff --git a/packages/engine/src/services/videoFrameInjector.ts b/packages/engine/src/services/videoFrameInjector.ts index 3eb62a159..288b8903a 100644 --- a/packages/engine/src/services/videoFrameInjector.ts +++ b/packages/engine/src/services/videoFrameInjector.ts @@ -236,9 +236,14 @@ export interface ElementStackingInfo { y: number; width: number; height: number; + /** Layout dimensions before CSS transforms (offsetWidth/offsetHeight). */ + layoutWidth: number; + layoutHeight: number; opacity: number; visible: boolean; isHdr: boolean; + transform: string; // CSS transform matrix string, e.g. "matrix(1,0,0,1,0,0)" or "none" + borderRadius: [number, number, number, number]; // [tl, tr, br, bl] in CSS px from nearest clipping ancestor } /** @@ -290,18 +295,148 @@ export async function queryElementStacking( return 0; } + // Find border-radius that clips the element. Replaced elements like