← Back to catalog

shader-transition / gravitational-lens

gravitational-lens

Warps scene pixels around a lens-like transition. Ported from the pinned Hyperframes shader.

0 / 119

Parameters

Use this effect

VideoML XML

Save this example as example.videoml.xml and place the downloaded asset in an assets/ directory beside it. Run vml pipeline example.videoml.xml with a VideoML installation that supports frame effects.

Download Geist-Latin.woff2 to assets/.

<vml id="catalog-example" width="1280" height="720" fps="30" duration="4s">
  <voiceover seed="0"/>
  <assets><asset id="font" src="assets/Geist-Latin.woff2" media-type="font/woff2"/></assets>
  <scene id="from" start="0s" duration="3s">
    <video-background props='{"color":"#224466"}'/>
    <frame-effect id="echo" effect-id="videoml/echo-smear" effect-version="1" props='{&quot;parameters&quot;:{&quot;length&quot;:100,&quot;threshold&quot;:40,&quot;color&quot;:&quot;#71f5a7&quot;}}' start="0s" duration="3s"/>
  </scene>
  <transition id="transition" effect="gravitational-lens" start="2s" duration="1s" ease="linear" props='{&quot;accent&quot;:[1,0.6,0.2],&quot;dark&quot;:[0.4,0.15,0],&quot;bright&quot;:[1,0.85,0.5]}'/>
  <scene id="to" start="2s" duration="2s">
    <video-background props='{"color":"#88aa66"}'/>
    <frame-effect id="particles" effect-id="videoml/particle-text-dissolve" effect-version="1" props='{&quot;parameters&quot;:{&quot;text&quot;:&quot;VideoML&quot;,&quot;density&quot;:&quot;med&quot;,&quot;accent&quot;:&quot;green&quot;,&quot;direction&quot;:&quot;in&quot;}}' start="0s" duration="2s"/>
  </scene>
</vml>

TypeScript

Requires the frozen stdlib package, GSAP, a page element with id="effect-root", and the referenced local assets. Revoke any generated object URLs when the host disposes the effect.

import { createEffectRegistry, mountEffect } from '@videoml/stdlib/effects';
import { registerEchoSmear } from '@videoml/stdlib/effects/echo-smear';
import { registerParticleEffects } from '@videoml/stdlib/effects/particles';
import { mountSceneCompositor } from '@videoml/stdlib/effects/transitions';
import { gsap } from 'gsap';

const root = document.querySelector<HTMLElement>('#effect-root');
if (!root) throw new Error('Missing #effect-root');
const registry = createEffectRegistry(); registerEchoSmear(registry); registerParticleEffects(registry);
const response = await fetch('./assets/Geist-Latin.woff2');
if (!response.ok) throw new Error('Font unavailable');
const font = URL.createObjectURL(await response.blob());
const context = { width: 1280, height: 720, fps: 30, seed: 0, assets: { font } };
function scene(id: string, parameters: Record<string, unknown>, startFrame: number, endFrame: number, background: string) {
  const layer = document.createElement('div'); Object.assign(layer.style, { position: 'absolute', left: '-10000px', top: '0', width: '1280px', height: '720px' }); document.body.append(layer);
  const effect = mountEffect(layer, registry, { id, version: '1', parameters, startFrame, endFrame }, context);
  const canvas = document.createElement('canvas'); canvas.width = 1280; canvas.height = 720;
  const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas unavailable');
  return { seek: (frame: number) => effect.seek(frame), async capture() { const content = layer.querySelector('canvas'); if (!content) throw new Error('Scene unavailable'); ctx.fillStyle = background; ctx.fillRect(0, 0, 1280, 720); ctx.drawImage(content, 0, 0); return canvas; }, dispose() { effect.dispose(); layer.remove(); } };
}
const from = scene('videoml/echo-smear', {"length":100,"threshold":40,"color":"#71f5a7"}, 0, 90, '#224466');
const to = scene('videoml/particle-text-dissolve', {"text":"VideoML","density":"med","accent":"green","direction":"in"}, 60, 120, '#88aa66');
const easeName = "linear";
if (!/^[A-Za-z][A-Za-z0-9]*(?:\.(?:in|out|inOut))?$/.test(easeName)) throw new Error('TRANSITION_INVALID: unknown easing');
const ease = gsap.parseEase(easeName); if (typeof ease !== 'function') throw new Error('TRANSITION_INVALID: unknown easing');
const transition = mountSceneCompositor(root, { from, to, width: 1280, height: 720, startFrame: 60, endFrame: 90, shader: "gravitational-lens", parameters: {"accent":[1,0.6,0.2],"dark":[0.4,0.15,0],"bright":[1,0.85,0.5]}, ease });
await transition.seek(75);
// On teardown: transition.dispose(); URL.revokeObjectURL(font);

Upstream Apache-2.0 license · GSAP license · Geist font license

implemented · interactive preview

This entry is implemented but not yet verified.

VideoML is a draft standard for declarative video composition. This site is a work in progress.