图像生成特效
Libraries.dev 的独立组件,请不要替换为 JGuang UI/Base UI 的同名 API。
安装
npm install img-fx@0.5.1 three@0.182.0
安装包后使用它自己的导出组件,支持 React 18+。Canvas/WebGL 组件需要浏览器环境;提供静态降级,并尊重减少动画偏好。声音预览使用模拟信号,不会自动申请麦克风。
使用与验证
在左侧选择组件,在预览中操作它的状态;使用“查看代码”检查真实源码。安装后执行类型检查和构建,再验证键盘焦点、手机布局与深浅色主题。
来源
https://github.com/Jakubantalik/Libraries.dev/tree/b7f44b8744f4e09ff088d824719636a513110e2a MIT 许可证;不包括付费内容。下方提供上游英文参考内容,中文使用指南已适配;上游详细参考暂保留英文。
适用场景
为已经提供的图片展示像素/扫描生成与显隐特效。
关键原生参数
| 参数 | 说明 |
|---|---|
preset | 公开内置预设为 pixels-organic、pixels-mechanic、sweep-gradient。 |
images | triggerReveal 需要非空图片池;没有图片时该方法不会执行显示。 |
autoReveal | 可选自动调度,手动演示使用 false;revealInitialDelay/revealHoldMs 控制时间。 |
paused | 冻结着色器与显隐调度。strength 只改变外观,不决定是否继续运行。 |
onCycle | 报告 idle/reveal/visible/hide 及 src。ref 的 triggerReveal/triggerHide/triggerRegenerate 操作的是已有图片。 |
theme | 同步应用主题,安装固定版本 three 对等依赖,并提供 Canvas/WebGL 降级。 |
交互与状态检查
检查全部预设、暂停/继续、显示后隐藏,以及图片显示时重新生成。示例按有效阶段禁用操作:idle 时显示,visible 时隐藏/重新生成。检查图片失败、跨域和图形不可用;不把阶段事件当作服务端生成成功。
业务接入边界
不含 AI 图片生成服务,重新生成只是已有图片之间的本地视觉变化。fragmentShader 是页面共享原生状态,不在同一预览运行环境混用未经审查的着色器或其他库。
示例文件与宿主配置
从源码面板复制 examples 下的组件文件和 shared.tsx,保留它们的相对位置;不是只复制原生上游实现。先安装固定版本 npm 包,在浏览器组件中使用它自己的导出。示例不需要 Tailwind 或新的主题 Provider,采用宿主 html 的语言和主题,并提供减少动画与图形不可用降级。原生实现参考仅供阅读,通过 npm 导入,不要求手工复制内部引擎。示例场景名称不是新增的原生组件属性。下方上游详细参考保留英文,不宣称全文中文化。
可运行用法
以下代码使用资源原生 API;不是把其他设计系统的同名组件替换进来。
"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>;
}
可用状态与示例
有机像素、机械像素、渐变扫描、暂停动画、手动显示与重新生成
组件中的状态与数据为演示用途,不包含后端接口。接入实际项目时请检查源码中的公开参数,再连接业务数据。
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.
源码参数参考
以下实现签名和类型从本地固定版本源码生成。保留可选属性、原生继承类型、联合分支和 never 限制;不会将其他设计系统的 API 混入。forwardRef 实现中的 ref 属于 React 引用,不是新增业务属性。此区域是原始类型参考,不是上游说明的全文翻译。
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;
}
源码中的解构默认值
只列出直接写在参数或 props 解构中的默认表达式。条件表达式、运行时主题和其他计算值请结合源码;这里不是全部行为的默认值清单。
// 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
原生与外部类型
以下外部类型仍由 React 或资源自身依赖定义,没有手工展开为虚构属性。实际可用类型以安装版本的 TypeScript 检查为准。
HTMLDivElement
Omit<HTMLAttributes<HTMLDivElement>, 'children'>
HTMLAttributes<HTMLDivElement>
ReactNode
PresetName
CycleEvent
Set<string>
CSSProperties