Files
hyperframes/packages/shader-transitions
James RussoandClaude Opus 4.6 b23b0751da fix(player): parent-frame media playback for mobile (#266)
* fix(player): parent-frame media playback for mobile

Mobile browsers block media.play() inside iframes when the user
gesture happened in the parent frame — postMessage doesn't transfer
user activation (per the User Activation v2 spec).

## Problem

The player renders compositions in a sandboxed iframe. When a user
taps play in the parent frame, the player sends a postMessage to the
iframe's runtime, which calls audio.play(). On mobile, this fails
silently because the iframe has no user activation context.

## Solution

The player now extracts ALL timed media elements (audio/video with
data-start) from the iframe's DOM (same-origin access), creates
parent-frame copies, and disables the iframe originals. On play(),
parentMedia.play() runs synchronously in the gesture call stack,
satisfying mobile autoplay policy.

### Generic media handling

- Finds all `audio[data-start], video[data-start]` in the iframe
- Creates a parent-frame copy for each (Audio or Video element)
- Preserves data-start offsets for correct seek positioning
- Strips data-start from iframe elements so the runtime ignores them
- Falls back to iframe media for cross-origin iframes

### `audio-src` attribute

Convenience for the common single-narration case. When set, the
player starts preloading audio immediately — before the iframe loads.
This eliminates the loading delay that caused jittery playback.

### No active sync

Both parent media and the GSAP timeline are real-time systems. When
started simultaneously, they naturally stay within ~10ms — no drift
correction needed. Active sync with coarse granularity (50ms polling)
caused MORE jitter than it prevented via repeated audio seeks.

## CI

- Added unified `test` job replacing separate per-package test jobs
- Added root `test` script: `bun run --filter '*' test`
- New packages with test scripts are automatically included
- Added happy-dom for player DOM tests

## Tests

- 10 new tests for parent-frame media: preloading, play, pause,
  seek, muted/rate sync, cleanup, attribute changes
- All 21 player tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shader-transitions): pass CI when no test files exist

Add --passWithNoTests to vitest run so the unified test job
doesn't fail on packages that have a test script but no test
files yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): update tests for new id field and GSAP lint rule

- normalize.test.ts: loadTranscript now assigns id fields (w0, w1, etc.)
  to SRT/VTT results and empty string for words-json passthrough
- lintProject.test.ts: add GSAP CDN script to validHtml() fixture to
  satisfy the missing_gsap_script lint rule added in core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): add missing data-start/data-duration to validHtml fixture

The validHtml() test fixture was missing data-start and data-duration
attributes, triggering the root_composition_missing_data_start and
root_composition_missing_data_duration lint warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): fetch LFS objects for producer test job

Producer regression tests compare rendered output against reference MP4
files stored in git LFS. Without lfs: true, checkout fetches pointer
files instead of actual videos, causing "moov atom not found" errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: remove redundant test-producer job

The regression workflow already runs the same 28 producer fixtures
in a Docker container with prod-matching Chrome/fonts/ffmpeg, sharded
across 8 parallel matrix jobs with 40-min timeouts. The CI test-producer
job was a duplicate that ran on bare runners with worse determinism
and a 15-min timeout too short for all fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:20:07 -07:00
..

@hyperframes/shader-transitions

WebGL shader transitions for HyperFrames compositions. Renders GPU-accelerated scene-to-scene transitions using fragment shaders, driven by GSAP timelines.

Install

npm install @hyperframes/shader-transitions

Or load directly via CDN:

<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>

Usage

import { init } from "@hyperframes/shader-transitions";

const tl = init({
  bgColor: "#0a0a0a",
  accentColor: "#ff6b2b",
  scenes: ["scene-1", "scene-2", "scene-3"],
  transitions: [
    { time: 3, shader: "domain-warp", duration: 0.8 },
    { time: 8, shader: "light-leak", duration: 0.7 },
  ],
});

The init() function captures each scene to a WebGL texture at transition time, crossfades between them using the selected shader, and returns a GSAP timeline. If WebGL is unavailable, it falls back to hard cuts.

With an existing timeline

Pass your own GSAP timeline to layer transitions onto it:

const tl = gsap.timeline({ paused: true });
// ... add your scene animations ...

init({
  bgColor: "#000",
  scenes: ["intro", "demo", "outro"],
  transitions: [
    { time: 5, shader: "cinematic-zoom" },
    { time: 12, shader: "glitch", duration: 0.5 },
  ],
  timeline: tl,
});

Available shaders

Shader Description
domain-warp Organic noise-based warp with glowing edge
ridged-burn Ridged noise burn with sparks and heat glow
whip-pan Horizontal motion blur simulating a fast camera pan
sdf-iris Circular iris wipe with glowing ring edge
ripple-waves Concentric ripple distortion radiating from center
gravitational-lens Warping gravity well with chromatic aberration
cinematic-zoom Radial zoom blur with chromatic fringing
chromatic-split RGB channel separation expanding from center
glitch Digital glitch with block displacement and scanlines
swirl-vortex Spiral rotation with noise-based warping
thermal-distortion Heat shimmer rising from the bottom
flash-through-white Flash to white then reveal the next scene
cross-warp-morph Noise-driven morph blending both scenes
light-leak Warm cinematic light leak with lens flare

API

init(config): GsapTimeline

Option Type Required Description
bgColor string yes Background color (hex) for scene capture
accentColor string no Accent color (hex) for shader glow effects
scenes string[] yes Element IDs of each scene, in order
transitions TransitionConfig[] yes Transition definitions (see below)
timeline GsapTimeline no Existing timeline to attach transitions to
compositionId string no Override the data-composition-id for timeline registration

TransitionConfig

Option Type Default Description
time number Start time in seconds
shader ShaderName Shader name from the table above
duration number 0.7 Transition duration in seconds
ease string "power2.inOut" GSAP easing function

SHADER_NAMES

Array of all available shader name strings, useful for validation or building UIs.

import { SHADER_NAMES } from "@hyperframes/shader-transitions";
// ["domain-warp", "ridged-burn", "whip-pan", ...]

Distribution

Format File Use case
ESM dist/index.js Bundlers (Vite, webpack, etc.)
CJS dist/index.cjs Node.js / require()
IIFE dist/index.global.js <script> tag, CDN

All formats include source maps. TypeScript definitions included.

License

MIT