# VFX-JS VFX-JS renders HTML elements (``, ``, text, any element) through WebGL shaders. The element keeps its place in the layout. VFX-JS draws the effect on one canvas over the page and hides the original element. Docs: https://amagi.dev/vfx-js/ · Examples: https://amagi.dev/vfx-js/examples · Transition demo: https://amagi.dev/vfx-js-transitions/ ## Packages | Package | Use it for | | --- | --- | | `@vfx-js/core` | The `VFX` class. Shader presets, custom shaders, the Effect API, `addTransition`. | | `@vfx-js/react` | React components (`VFXImg`, `VFXVideo`, `VFXSpan`, `VFXDiv`, `VFXP`, `VFXCanvas`) and ``. Subpaths `/next`, `/react-router`. | | `@vfx-js/vue` | Vue directive `v-vfx`, `VFXCanvas`, and `` from `/nuxt` or `/router`. | | `@vfx-js/svelte` | Svelte 5 attachment `vfx`, `VFXCanvas`, and `` from `/sveltekit`. | | `@vfx-js/astro` | Astro components (`VFXImg`, `VFXVideo`, `VFXSpan`, `VFXDiv`, `VFXP`), ``, ``. `registerVFX` from `/client`. | | `@vfx-js/effects` | Prebuilt `Effect` classes (`BloomEffect`, `PixelateEffect`, ...). | | `@vfx-js/transitions` | Transition presets (`Swipe`, `Glitch`, `Zoom`, ...). Experimental. | ## Pick the right API | Goal | Use | Read | | --- | --- | --- | | Quick effect on an element | `vfx.add(el, { shader: "glitch" })` | below | | Your own one-pass shader | `vfx.add(el, { shader: glslString })` | [references/shader.md](#custom-shaders-shader-option) | | Multi-pass, feedback, simulation | `shader: VFXPass[]` | [references/shader.md](#custom-shaders-shader-option) | | Prebuilt effects, chains, or reusable effect classes | `vfx.add(el, { effect })` | [references/effects.md](#effects-effect-option) | | Effect over the whole page | `new VFX({ postEffect })` | [references/shader.md](#custom-shaders-shader-option) | | Tabs, carousels, page transitions | `vfx.addTransition(el)` / `` | [references/transitions.md](#transitions-addtransition-vfx-jstransitions) | | Your own transition | `class X extends ShaderTransition` | [references/transitions.md](#transitions-addtransition-vfx-jstransitions) | ## Quick start ```bash npm i @vfx-js/core ``` ```ts import { VFX } from "@vfx-js/core"; const vfx = new VFX(); const img = document.querySelector("img")!; await vfx.add(img, { shader: "rgbShift" }); ``` Shader presets: `uvGradient`, `rainbow`, `glitch`, `rgbGlitch`, `rgbShift`, `shine`, `blink`, `spring`, `duotone`, `tritone`, `hueShift`, `sinewave`, `pixelate`, `halftone`, `invert`, `grayscale`, `vignette`, `chromatic`. Scroll reveal presets animate in when the element enters the viewport, and out when it leaves: `slitScanReveal`, `warpReveal`, `pixelateReveal`, `focusReveal`. The old names (`slitScanTransition`, `warpTransition`, `pixelateTransition`, `focusTransition`) still work. Without a bundler: ```html ``` ### Common `vfx.add` options ```ts vfx.add(el, { shader: "glitch", // preset name, GLSL string, or VFXPass[] uniforms: { // values for your shader; a function runs every frame amount: 0.5, scroll: () => window.scrollY, }, overflow: 100, // draw outside the element: px, [top, right, bottom, left], or true (fullscreen) overlay: true, // keep the original element visible (default hides it) zIndex: 0, // draw order inside the canvas wrap: "clamp", // texture wrap: "repeat" (default) | "clamp" | "mirror" intersection: { threshold: 0.5, duration: 1 }, // enter/leave timing; see references/shader.md }); ``` ### `VFX` methods - `add(el, props)` → `Promise`. Start rendering an element. - `addHTML(el, props)` → render any HTML subtree, with live updates (html-in-canvas, Chrome only, falls back to a slower capture). Check support with `supportsHtmlInCanvas()`. - `remove(el)`, `update(el)` (re-capture after the content changes), `updateEffects(el, effects)`. - `addTransition(el, opts)` → `TransitionHandle`. See [references/transitions.md](#transitions-addtransition-vfx-jstransitions). - `play()`, `stop()`, `render()`, `destroy()`, `setTime(t)`, `timeScale`. `new VFX(opts)` options: `pixelRatio`, `zIndex`, `autoplay`, `scrollPadding`, `wrapper`, `postEffect`, `preserveDrawingBuffer`, `timeScale`. ## React ```bash npm i @vfx-js/core @vfx-js/react ``` ```tsx import { VFXImg, VFXSpan, VFXVideo } from "@vfx-js/react"; Hello ``` - Every `vfx.add` option is a prop (`shader`, `effect`, `uniforms`, `overflow`, ...). - No provider is needed. Components share one canvas. Set its options with `useVFXConfig({ pixelRatio, zIndex, postEffect })`. - Use `` only for a separate canvas (e.g. a `wrapper`), and `VFXContext` to get the `VFX` instance. - The package is `"use client"`. In Next.js App Router, use the components from a client component or directly in a Server Component (they cross as client references). Pass only serializable props from a Server Component: preset names, GLSL strings, plain uniforms. - `...` renders any markup, form controls included. With SSR it renders a `` first and switches to `` after hydration, so its children mount again once. ## Vue and Nuxt ```bash npm i @vfx-js/core @vfx-js/vue ``` ```vue Hello ``` - The value is `VFXProps`. `null` / `undefined` means no effect. Register it globally with `app.directive("vfx", vVfx)`. - `VFXCanvas` renders any markup into a shader. `v-model:element` gives the element the effect is on. - `useVFXConfig({ pixelRatio, zIndex, postEffect, autoplay })` in `setup` sets the shared canvas options. `useVFXInstance()` gives the `VFX` as a ref. - Call `updateElement(el)` after changes the directive can't see, like an input's value. - Works with SSR (Nuxt). No `` needed. ## Svelte and SvelteKit Needs Svelte 5.29 or later. ```bash npm i @vfx-js/core @vfx-js/svelte ``` ```svelte ({ shader }))}>Hello ``` - Pass a getter (`() => ({ ... })`) for props that change. A plain object that reads state removes and re-adds the element on every change. - `VFXCanvas` renders any markup into a shader. `bind:element` gives the element the effect is on. - `vfxConfig({ ... })` sets the shared canvas options. `vfxInstance().current` is the `VFX`. - Keep a `postEffect` object out of `$state`; every new proxy replaces the canvas. - Call `updateElement(el)` after changes the attachment can't see. ## Astro Needs Astro 5 or later. No framework island. ```bash npm i @vfx-js/core @vfx-js/astro ``` ```astro --- import { VFXImg, VFXSpan } from "@vfx-js/astro"; --- Hello ``` - `VFXImg`, `VFXVideo`, `VFXSpan`, `VFXDiv`, `VFXP` take `VFXProps` that fit in HTML: `shader` is a preset, GLSL, or `"@name"` of a registered `VFXPass[]`; `uniforms` is plain numbers or the name of registered uniforms; no `effect`. - The props become a `data-vfx` JSON attribute. Any element with `data-vfx` works, e.g. `` from `astro:assets`. - Register functions by name in a client ` Home Gallery ``` ```vue ``` - Without Nuxt, import `VFXTransition` from `@vfx-js/vue/router` and wrap ``. It reads the router from `app.use(router)`. - Each path is one page. The first navigation does not run. - Which spec wins: the link's `v-transition`, then the page's `useTransitionIn`, then the `` props. Back does not reverse unless `autoReverse` is set. - `setNextTransition(spec)` before `navigateTo()` / `router.push()`. - In templates use `v-transition`, not `@click="transition(spec)"`; Vue wraps the handler, so it never runs. - Props are the `addTransition` options plus `fullscreen`. `:fullscreen="false"` lets you size the target with CSS. - A page with an async `setup` under your own `` may be captured early. Use `waitFor`. ## Svelte: SvelteKit ```bash npm i @vfx-js/core @vfx-js/svelte @vfx-js/transitions ``` ```svelte Home Gallery {@render children()} ``` ```svelte ``` - Needs Svelte 5.29 or later (`{@attach}`). - Which spec wins: the link's `transition(spec)`, then the page's `transitionIn`, then the `` props. Back does not reverse unless `autoReverse` is set. - `setNextTransition(spec)` before `goto()`. - `fullscreen={false}` lets you size the target with CSS. ## Astro: ClientRouter ```bash npm i @vfx-js/core @vfx-js/astro @vfx-js/transitions ``` ```astro --- // src/layouts/Layout.astro import { ClientRouter } from "astro:transitions"; import { VFXTransition } from "@vfx-js/astro"; --- Home Gallery ``` ```astro --- // src/pages/about.astro import { TransitionIn } from "@vfx-js/astro"; --- ``` - Needs ``. Without it links reload the page and nothing animates. - `transition:animate="none"` turns off the browser's view transition; the shader is the animation. - Transitions are names: lowercase class names from `@vfx-js/transitions` (`crossfade`, `swipe`, `glitch`, `pixelsort`, ...). Register others with `registerVFX({ transitions: { name: ClassOrInstance } })` from `@vfx-js/astro/client`. `ease` is `"smoothstep"`, `"linear"`, or a name from `registerVFX({ eases })`. - A link spec is a name or JSON: `data-vfx-transition='{"transition":"swipe","params":{"direction":"left"}}'`. - Which spec wins: the link's `data-vfx-transition`, then the page's ``, then the `` props. Back does not reverse unless `autoReverse` is set. - `fullscreen={false}` lets you size the target with CSS. - `installTransition({ swapFunctions })` from `@vfx-js/astro/client` is what `` calls. You rarely need it. ## Other routers Turbo, htmx and others have no binding. Use core `run(cb)`: hold the navigation until `run` has taken the snapshot, then let `cb` wait for the new page. ```ts router.beforeNavigate(() => new Promise((snapshotTaken) => { t.run(() => { snapshotTaken(); // the router may now swap the DOM return waitForNewPageInDom(); // resolve when the new page is in }).catch(console.error); }), ); ``` ## Rules and pitfalls - Give the target a fixed size (explicit height, or the viewport). Content of a different size does not animate between sizes; the box jumps after the run. - Width rules: the target must fill its parent's width or have an inline px width. Put `max-width` or centering on a wrapper, not on the target. - A `display` rule on a slide beats `hidden`. Add `[hidden] { display: none !important }`. - `` cannot be the target. Use the router outlet (``, `#app`). - VFX elements inside the target freeze during a run. - Next.js `loading.tsx` shows its fallback at once, so the transition lands on the skeleton. - Multi-page (cross-document) navigation is not supported. - Vue, Svelte and Astro `` render plain divs on the server. No client-only wrapper is needed. - Respect `prefers-reduced-motion`: pass `duration: 0` when it matches.