Img Fx
Install: npm install img-fx@0.5.1 three@0.182.0
When to use
Show pixel/sweep generation and reveal effects over already supplied images.
Key native props
| Prop | Meaning |
|---|---|
preset | pixels-organic, pixels-mechanic or sweep-gradient. These are the three public bundled presets. |
images | Nonempty source pool is required for triggerReveal; without it the method is a silent no-op. |
autoReveal | Optional scheduler; false in the manual demo. revealInitialDelay/revealHoldMs govern timing. |
paused | Freezes shader and reveal scheduling. strength changes appearance, not whether work is running. |
onCycle | Reports idle/reveal/visible/hide with the selected src. Ref methods triggerReveal/triggerHide/triggerRegenerate operate on existing images. |
theme | Synchronize the app theme. Install the pinned three peer package and provide Canvas/WebGL fallback. |
Interaction and state checks
Select all presets, pause/resume, reveal then hide, and regenerate only while an image is visible. The example disables commands outside their valid phase: reveal from idle, hide/regenerate from visible. Test image errors, CORS and unsupported graphics; never treat a phase event as server generation success.
Integration boundaries
No AI image generation service is included. Regenerate is a local visual transition between supplied images. fragmentShader is page-shared native state; avoid unreviewed shaders or mixed libraries in the same preview runtime.
Example files and host setup
Copy the component file and shared.tsx from examples in the source panel, keeping their relative locations; do not copy only the upstream implementation. Install the pinned npm package and use its own export in a browser component. Examples need no Tailwind or competing theme provider, read host html language/theme and provide reduced-motion/graphics fallbacks. Native implementation references are for reading; import via npm rather than copying an incomplete engine. Showcase labels are not new native props. Upstream detailed prose remains English; no full-translation claim is made.
Runnable usage
"use client";
import { useRef, useState } from "react";
import { ImageGeneration, type ImageGenerationPreset, type ImageGenerationHandle } from "img-fx";
import { DemoFrame, GraphicsGuard, control, demoImages, demoTheme, panel, t } from "./shared";
export default function Example({ variant = "Default" }: { variant?: string }) {
const ref = useRef<ImageGenerationHandle>(null);
const [preset, setPreset] = useState<ImageGenerationPreset>(variant === "Mechanical" ? "pixels-mechanic" : variant === "Sweep" ? "sweep-gradient" : "pixels-organic"), [paused, setPaused] = useState(variant === "Paused"), [phase, setPhase] = useState("idle");
return <DemoFrame status={`preset=${preset}, paused=${paused}, phase=${phase}`} controls={<>
<label>{t("Preset", "预设")} <select style={control} aria-label={t("Image preset", "图像预设")} value={preset} onChange={e => setPreset(e.target.value as ImageGenerationPreset)}>{["pixels-organic", "pixels-mechanic", "sweep-gradient"].map(s => <option key={s}>{s}</option>)}</select></label>
<button type="button" style={control} aria-pressed={paused} onClick={() => setPaused(!paused)}>{paused ? t("Resume", "继续动画") : t("Pause", "暂停动画")}</button>
{variant === "Manual reveal" && <><button type="button" disabled={paused || phase !== "idle"} style={control} onClick={() => ref.current?.triggerReveal({ hold: "manual" })}>{t("Reveal image", "显示图片")}</button><button type="button" disabled={paused || phase !== "visible"} style={control} onClick={() => ref.current?.triggerHide()}>{t("Hide image", "隐藏图片")}</button><button type="button" disabled={paused || phase !== "visible"} style={control} onClick={() => ref.current?.triggerRegenerate({ durationMs: 1000 })}>{t("Regenerate visually", "重新生成视觉效果")}</button><span style={{ fontSize: 12 }}>{t("Reveal an image before hiding or regenerating it.", "先显示图片,再隐藏或重新生成视觉效果。")}</span></>}
</>}><GraphicsGuard kind="webgl2"><ImageGeneration ref={ref} preset={preset} theme={demoTheme()} paused={paused} images={demoImages} autoReveal={false} revealInitialDelay={0} onCycle={event => setPhase(event.phase)} style={{ width: "100%", maxWidth: 320 }}><div style={{ ...panel, height: 200 }}>{t("Visual generation effect — not an AI request", "视觉生成特效,不调用 AI 接口")}</div></ImageGeneration></GraphicsGuard></DemoFrame>;
}
Preview states
Default, Mechanical, Sweep, Paused, Manual reveal
Demo state and mock data do not provide a backend service. Inspect the component props before wiring real data.
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-fx
react, 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>
);
}
Three presets ship with the library: pixels-organic (Chromium Flow),
pixels-mechanic (Nebula), and sweep-gradient (a fast top-left → bottom-right
gradient sweep with per-cell random flicker that reads as "generating"; its
reveal materializes the image cell-by-cell along the traveling band).
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
preset | 'pixels-organic' | 'pixels-mechanic' | 'sweep-gradient' | 'pixels-organic' | Selects the bundled effect preset. pixels-organic = Chromium Flow mosaic, pixels-mechanic = Nebula mosaic, sweep-gradient = diagonal "generating" gradient sweep with per-cell flicker. |
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. |
pixelScale | number | 1 | Pixel-cell size multiplier for the mosaic. 0.5 = finer grid, 2 = chunkier. The reveal dissolve stays in lockstep with the shader grid. |
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. |
colors | (string | null)[] | preset | Per-slot palette override (up to 7 CSS colours, one per shader slot). Slots with a value replace the preset colour; null/missing slots keep it. Re-tints the running effect without authoring a preset. |
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' });
};
Regenerate
triggerRegenerate() re-runs the "generating" effect from the currently
revealed image: the photo breaks into the effect's pixel-cell grid and
churns (cells popping in/out with the preset's flicker clock) while the
shader plays through the gaps — then the next image from the pool dissolves
in over the churn and stays visible.
By default the effect is recolored from the outgoing image: its palette is sampled from the visible pixels and mapped onto the preset's palette slots by luminance rank, so the churn reads as pixelation born from the image rather than the preset's stock colors. The palette is restored automatically once the new image is fully visible.
The churn always uses a pixel-mosaic preset (pixels-mechanic /
pixels-organic): a sweep-gradient card temporarily switches to a
randomly-picked pixel preset for the churn and restores its authored preset
once the new image is fully visible.
<button onClick={() => ref.current?.triggerRegenerate({ durationMs: 3000 })}>
Regenerate
</button>
Options:
| Option | Type | Default | Notes |
|---|---|---|---|
durationMs | number | 4000 | Churn length before the next image auto-reveals (held manually). |
tintFromImage | boolean | true | Recolor the effect from the outgoing image. |
autoReveal | boolean | true | false churns indefinitely — end it with triggerReveal() or triggerHide(). |
No-op unless an image is currently revealed. Requires same-origin (or CORS-enabled) images for the palette sampling; cross-origin images without CORS fall back to the preset palette.
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. - 10 fps default cap via rAF accumulator — ideal for the slow-drift presets. Raise it with
setFrameRate(15)(up to 60) for faster custom presets. 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.
Source-backed parameter reference
Implementation signatures and types are generated from the pinned local source. Optional props, native inherited types, union branches and never exclusions are preserved. A forwardRef implementation's ref is a React reference, not an additional business prop.
ImageGeneration({
children,
preset = 'pixels-organic',
theme = 'auto',
strength = 1,
speed = 1,
pixelScale = 1,
cardBg: cardBgProp,
colors,
images,
autoReveal = false,
revealDelayRange = [2, 4],
revealInitialDelay,
revealHoldMs = 2000,
revealFadeOutMs = 300,
borderRadius,
paused = false,
fragmentShader,
onCycle,
excludeSrcs,
className,
style,
...rest
}, forwardedRef)
export type ImageGenerationTheme = 'auto' | 'dark' | 'light';
export type ImageGenerationPreset = PresetName;
export type ImageGenerationCycleEvent = CycleEvent;
export interface ImageGenerationHandle {
/** The wrapper `<div>` element. */
element: HTMLDivElement | null;
/**
* Run a reveal pass now.
*
* - No-op if a reveal is already in progress (currently revealing, holding
* the image visible, or fading back to the shader).
* - `hold: 'auto'` (default) — runs reveal -> hold (`revealHoldMs`) -> hide
* automatically, mirroring the auto-loop's pass.
* - `hold: 'manual'` — runs reveal then stays in the visible phase until
* `triggerHide()` is called. Use this for "Reveal / Hide" toggle buttons.
*
* Requires a non-empty `images` prop; otherwise it's a silent no-op.
*/
triggerReveal: (opts?: { hold?: 'auto' | 'manual' }) => void;
/**
* Manually trigger the hide fade if an image is currently revealed (in
* `reveal` or `visible` phase). No-op otherwise.
*/
triggerHide: () => void;
/**
* Regenerate the currently revealed image in place: the image breaks into
* the effect's pixel-cell grid, the cells churn (pop in/out with the
* preset's flicker clock) while the shader plays through the gaps, and
* after `durationMs` the next image from the pool dissolves in over the
* churn (held like `triggerReveal({ hold: 'manual' })`).
*
* By default the effect is also recolored from the outgoing image — its
* palette is sampled from the visible pixels and mapped onto the preset's
* palette slots by luminance rank, so the churn reads as pixelation born
* from that image rather than the preset's stock colors. The preset
* palette (or the `colors` prop, when set) is restored automatically once
* the new image is fully visible.
*
* The churn always runs on a pixel-mosaic preset (`pixels-mechanic` /
* `pixels-organic`). When the active preset is `sweep-gradient`, the effect
* temporarily switches to a randomly-picked pixel preset for the churn and
* restores the authored preset once the new image is fully visible.
*
* Options:
* - `durationMs` — churn length before the next image auto-reveals.
* @default 4000
* - `tintFromImage` — recolor the effect from the outgoing image.
* @default true
* - `autoReveal` — set to `false` to churn indefinitely instead; end it
* manually with `triggerReveal()` (next image dissolves in over the
* churn) or `triggerHide()`.
* @default true
*
* No-op unless an image is currently revealed (`reveal` or `visible`
* phase). Emits an `idle` cycle event when the churn starts.
*/
triggerRegenerate: (opts?: {
durationMs?: number;
tintFromImage?: boolean;
autoReveal?: boolean;
}) => void;
/**
* Returns true while an image is showing on top of the shader (any of the
* `reveal`, `visible`, or `hide` phases). Useful for driving the label /
* icon of a Reveal/Hide toggle button.
*/
isImageActive: () => boolean;
}
export interface ImageGenerationProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
/**
* Single host element to wrap. The wrapper sizes itself to the child; the
* shader paints inside the wrapper at full size, clipped to the child's
* border-radius.
*/
children: ReactNode;
/**
* Selects which bundled preset to render.
*
* - `pixels-organic` - Chromium Flow rendered as a pixel mosaic.
* - `pixels-mechanic`- Nebula rendered as a pixel mosaic.
* - `sweep-gradient` - Gradient Sweep rendered as a pixel mosaic.
*
* @default 'pixels-organic'
*/
preset?: ImageGenerationPreset;
/**
* Theme mode. `'auto'` resolves via `matchMedia('(prefers-color-scheme: dark)')`
* and switches live when the OS theme changes.
* @default 'auto'
*/
theme?: ImageGenerationTheme;
/**
* Effect strength (0..2). In 0..1 it multiplies the shader canvas opacity
* (the shader keeps animating at full intensity; only the rendered alpha
* is scaled down) — exactly the historical behavior. Above 1 the canvas
* stays fully opaque and the shader's palette intensity is boosted
* instead, making the effect markedly more prominent.
* @default 1
*/
strength?: number;
/**
* Animation speed multiplier on top of the preset's baked tempo. Scales
* the effect's whole clock — drift, churn and flicker together — so the
* character of the motion is preserved. `1` (default) is the preset
* tempo; `0.5` half speed, `2` double.
*/
speed?: number;
/**
* Pixel-cell size multiplier for the mosaic effect. Scales the on-screen
* size of each pixel cell (and the matching reveal dissolve) without
* touching the preset:
*
* - `1` (default) — the preset's authored cell size.
* - `0.5` — cells at half size (finer grid, effect reads as "zoomed out").
* - `2` — cells at double size (chunkier grid, "zoomed in").
*
* Implemented by dividing the shader grid's base cell count by this value,
* so the shader mosaic and the reveal pixel dissolve stay in lockstep. The
* grid always keeps a floor of 2 cells, so extreme values are safe.
* @default 1
*/
pixelScale?: number;
/**
* Card background colour. Accepts any CSS colour string the browser can
* parse — hex (`#rgb` / `#rrggbb` / `#rrggbbaa`), `rgb()` / `rgba()`,
* `hsl()` / `hsla()`, named colours, modern `color(...)` syntax — and is
* applied two ways:
*
* 1. Verbatim as the wrapper element's CSS `background`, so the
* original alpha is preserved visually.
* 2. Parsed to an opaque RGB triple for the shader's `u_cardBg`
* uniform, which drives colour-proximity / contrast logic so any
* shader colours that match the card background get faded out and
* the effect stays visually balanced.
*
* Alpha is intentionally dropped when feeding the shader — the renderer
* has no source for what's behind a translucent card and can't composite
* correctly. If precise shader behaviour matters under a translucent
* surface, pass the opaque colour you want the shader to reason against.
*
* Useful for keeping colours in sync when the host card surface changes
* (e.g. dark-mode card swatch). When omitted, the preset's bundled
* `cardBg` (always opaque hex) is used.
*/
cardBg?: string;
/**
* Optional palette override for the running effect (up to 7 CSS colors,
* one per shader palette slot). A slot with a value replaces the preset
* color verbatim; `null` / `undefined` slots keep the preset color. Pass
* `undefined` (or omit) to use the preset palette.
*
* Useful for re-tinting the effect from external data — e.g. colors
* sampled from the image being regenerated — while keeping the exact same
* animation and cell grid.
*/
colors?: (string | null | undefined)[];
/**
* Image pool used by the reveal animation. Pass a single string to reveal
* the same image every cycle, or an array for random pick (never
* repeating the previous index).
* @default []
*/
images?: string | string[];
/**
* When true, runs the auto-reveal scheduler:
* shader (random delay) -> reveal -> hold -> hide -> repeat.
* @default false
*/
autoReveal?: boolean;
/**
* Random shader-only delay range in seconds between reveals.
* @default [2, 4]
*/
revealDelayRange?: [number, number];
/**
* One-time delay applied before the very first reveal (after `autoReveal`
* is enabled or `triggerReveal` runs the first cycle). Subsequent reveals
* follow `revealDelayRange`.
*
* - `number`: exact seconds.
* - `[min, max]`: seconds range, randomised once at cycle creation.
* - `undefined` (default): library picks a small jitter (0-1.5 s) so
* multiple instances on the same page don't tick in lockstep.
*
* Useful for staggering hero cards (e.g. card #2 = `[1, 3]`, card #3 =
* `[1, 3]`) so they don't all reveal at once.
*/
revealInitialDelay?: number | [number, number];
/**
* Time the image stays fully visible after the reveal animation completes,
* before the hide cross-fade kicks in.
*
* - `number` — fixed milliseconds.
* - `[min, max]` — random milliseconds per cycle, picked fresh each reveal.
*
* @default 2000
*/
revealHoldMs?: number | [number, number];
/** ms cross-fade back to the shader. @default 300 */
revealFadeOutMs?: number;
/**
* Optional explicit corner radius (CSS px). When omitted, reads the
* computed `border-radius` of the wrapped child each resize.
*/
borderRadius?: number;
/** Freezes the shader and the auto-reveal scheduler. @default false */
paused?: boolean;
/**
* Escape hatch: a replacement fragment shader (GLSL 1.00 syntax, as the
* bundled one — `gl_FragColor`, `texture2D`). Every uniform the engine
* uploads keeps its name; the shader is free to interpret them however it
* likes. The material is shared, so this applies page-wide while set.
*/
fragmentShader?: string;
/** Phase event hook for the auto-reveal cycle. */
onCycle?: (event: ImageGenerationCycleEvent) => void;
/**
* Optional callback invoked just before each image pick that returns a list
* of src strings the cycle MUST avoid this round. Use this to coordinate
* multiple `<ImageGeneration>` instances that share an image pool so they
* never display the same image at the same time.
*
* The callback runs on every pick (including manual `triggerReveal()`) so
* it can read live state. If every candidate is excluded, the cycle falls
* back to a normal random pick so it never gets stuck.
*/
excludeSrcs?: () => string[] | Set<string> | null | undefined;
/** Forwarded class name for the wrapper element. */
className?: string;
/** Forwarded inline styles for the wrapper element. */
style?: CSSProperties;
}
Destructuring defaults in source
Only defaults written directly in parameter/props destructuring are listed. Conditional expressions, runtime theme and computed values require source inspection; this is not an exhaustive behavior-default list.
// ImageGeneration
preset = 'pixels-organic'
// ImageGeneration
theme = 'auto'
// ImageGeneration
strength = 1
// ImageGeneration
speed = 1
// ImageGeneration
pixelScale = 1
// ImageGeneration
autoReveal = false
// ImageGeneration
revealDelayRange = [2, 4]
// ImageGeneration
revealHoldMs = 2000
// ImageGeneration
revealFadeOutMs = 300
// ImageGeneration
paused = false
Native and external types
External types remain defined by React or native dependencies, not manually expanded into guessed props. The installed version's TypeScript definitions are authoritative.
HTMLDivElement
Omit<HTMLAttributes<HTMLDivElement>, 'children'>
HTMLAttributes<HTMLDivElement>
ReactNode
PresetName
CycleEvent
Set<string>
CSSProperties