img-fx
v0.2.2
Published
Animated WebGL image-generation / loader effect for React cards
Maintainers
Readme
img-fx
Animated WebGL "image generation / loader" effect for React. Wrap any card and it gets a real-time shader-driven loading mosaic that periodically reveals an image from a pool you provide. Ports the canonical image.html effect into a small, performant React library.
Live demo: image.jakubantalik.com
Install
npm install img-fxreact, react-dom, and three are peer dependencies.
Local development
npm install
npm run dev # local Vite playground
npm run build # produce dist/ (ESM + CJS + .d.ts)
npm run build:demo # produce dist-demo/ (static showcase site)Quick start
import { ImageGeneration } from 'img-fx';
export function Card() {
return (
<ImageGeneration
preset="pixels-organic"
images={['/img/a.jpg', '/img/b.jpg']}
autoReveal
>
<div className="card" style={{ width: 320, height: 320, borderRadius: 20 }} />
</ImageGeneration>
);
}Props
| Prop | Type | Default | Notes |
| ------------------- | --------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
| preset | 'dots-mechanic' \| 'pixels-organic' \| 'pixels-mechanic' | 'pixels-organic' | Selects the bundled effect preset (Type × Variant). |
| theme | 'auto' \| 'dark' \| 'light' | 'auto' | auto checks <html data-theme>, .dark/.light class, inline color-scheme, then prefers-color-scheme. Live-updates via MutationObserver. |
| strength | number (0..1) | 1 | Final opacity multiplier. Doesn't change shader animation. |
| cardBg | string (any CSS colour) | preset | Override the host card surface colour. Applied verbatim to the wrapper background (alpha preserved) AND parsed to opaque RGB for the shader's u_cardBg so colour-proximity logic stays in sync. Accepts hex, rgb()/rgba(), hsl(), named colours, etc. |
| images | string \| string[] | [] | Reveal pool. Random pick per cycle, never repeats last. |
| autoReveal | boolean | false | When true, runs the auto-loop scheduler. |
| revealDelayRange | [number, number] seconds | [2, 4] | Random shader-only gap between reveals. |
| revealHoldMs | number | 2000 | Image visible duration after reveal animation completes. |
| revealFadeOutMs | number | 300 | Cross-fade back to shader. |
| borderRadius | number | (auto) | Override the corner radius (px). Auto-detected from the wrapped child by default. |
| paused | boolean | false | Freezes shader and scheduler. |
| onCycle | (p) => void | - | Fires on each phase change: idle / reveal / visible / hide. |
Manual reveal (ref handle)
Pass a ref to trigger reveals from a button click or any other user action,
without enabling autoReveal:
import { useRef } from 'react';
import { ImageGeneration, type ImageGenerationHandle } from 'img-fx';
export function Card() {
const ref = useRef<ImageGenerationHandle>(null);
return (
<>
<ImageGeneration ref={ref} preset="pixels-organic" images={['/a.jpg', '/b.jpg']}>
<div className="card" style={{ width: 320, height: 320, borderRadius: 20 }} />
</ImageGeneration>
<button onClick={() => ref.current?.triggerReveal()}>
Reveal image
</button>
</>
);
}triggerReveal() runs one full reveal -> hold -> hide pass and is a no-op
while a reveal is already in progress. Works with or without autoReveal.
For a Reveal / Hide toggle button (like the playground on the demo
page), pair triggerReveal({ hold: 'manual' }) with triggerHide() and
use isImageActive() to drive the button label:
const onToggle = () => {
const h = ref.current;
if (!h) return;
if (h.isImageActive()) h.triggerHide();
else h.triggerReveal({ hold: 'manual' });
};Scale invariance
Cell size, vignette range, and edge-fade are computed in CSS pixels (the shader
gets u_dpr so it can convert from physical pixels back to CSS px). That means
a pixels-organic cell stays the same physical size whether the card is 200×200
or 600×600 — the cell count scales with the card, not the cell size. No
chunky pixels on big cards.
Performance
- Single shared
THREE.WebGLRendererfor the whole page; one WebGL context. - 30 fps cap via rAF accumulator (matches
image.html). IntersectionObserverpauses any card that scrolls offscreen.- Strength is implemented as
canvas.style.opacity— no shader recompile, no uniform reupload. - Reveal scratch canvases are reused across frames; only re-allocated when grid size changes.
- Image bitmaps cached by URL across all card instances.
License
MIT © Jakub Antalik. See LICENSE.
