# 金属质感

- 页面: https://jiguang132.com/zh/resources/libraries-dev/docs/metal-fx
- 资源: Libraries.dev (https://jiguang132.com/zh/resources/libraries-dev.md)
- 全部指南: https://jiguang132.com/zh/resources/libraries-dev/docs.md

Libraries.dev 的独立组件，请不要替换为 JGuang UI/Base UI 的同名 API。

## 安装

```bash
npm install metal-fx@2.0.11
```

安装包后使用它自己的导出组件，支持 React 18+。Canvas/WebGL 组件需要浏览器环境；提供静态降级，并尊重减少动画偏好。声音预览使用模拟信号，不会自动申请麦克风。

## 使用与验证

在左侧选择组件，在预览中操作它的状态；使用“查看代码”检查真实源码。安装后执行类型检查和构建，再验证键盘焦点、手机布局与深浅色主题。

## 来源

https://github.com/Jakubantalik/Libraries.dev/tree/b7f44b8744f4e09ff088d824719636a513110e2a
MIT 许可证；不包括付费内容。下方提供上游英文参考内容，中文使用指南已适配；上游详细参考暂保留英文。

---



## 适用场景

为单个元素包裹原生金属环，或使用独立 MetalText 文字蒙版导出。

## 关键原生参数

| 参数 | 说明 |
|---|---|
| `preset` | MetalFx 支持 chromatic、silver、gold，均为公开 npm 预设。 |
| `theme` | auto 使用系统偏好，网页有手动开关时应传入应用主题。 |
| `paused` | MetalFx 保持最后一帧着色器结果；MetalText 不暴露同一个暂停属性。 |
| `strength` | 0–1 强度只控制透明效果，不停止着色器时钟；控制动画使用 paused 或卸载。 |
| `normalizeHostStyles` | 默认为 true，会规范子元素外观；接入真实控件时检查边框和焦点轮廓。 |

## 交互与状态检查

只包裹一个可测量子元素，切换三种预设、暂停并检查焦点。低性能设备应测量 Canvas/WebGL 与文字蒙版开销，并检查静态降级。

## 业务接入边界

MetalText 有自己的 font/color/蒙版 API，不要把 MetalFx 参数静默套过去。特效不是支付或 Pro 订阅，本例未复制付费徽章素材。

## 示例文件与宿主配置

从源码面板复制 examples 下的组件文件和 shared.tsx，保留它们的相对位置；不是只复制原生上游实现。先安装固定版本 npm 包，在浏览器组件中使用它自己的导出。示例不需要 Tailwind 或新的主题 Provider，采用宿主 html 的语言和主题，并提供减少动画与图形不可用降级。原生实现参考仅供阅读，通过 npm 导入，不要求手工复制内部引擎。示例场景名称不是新增的原生组件属性。下方上游详细参考保留英文，不宣称全文中文化。


## 可运行用法

以下代码使用资源原生 API；不是把其他设计系统的同名组件替换进来。

```tsx
"use client";
import { useState } from "react";
import { MetalFx, MetalText, type MetalFxPreset } from "metal-fx";
import { DemoFrame, GraphicsGuard, control, demoTheme, t } from "./shared";
export default function Example({ variant = "Default" }: { variant?: string }) {
  const [preset, setPreset] = useState<MetalFxPreset>(variant === "Silver" ? "silver" : variant === "Gold" ? "gold" : "chromatic"), [paused, setPaused] = useState(variant === "Paused"), [strength, setStrength] = useState(1);
  if (variant === "Metal text") return <DemoFrame status={t("MetalText uses its own text-mask API, not MetalFx pause props.", "MetalText 使用自己的文字蒙版 API，不接受 MetalFx 的暂停属性。") }><GraphicsGuard kind="webgl2"><MetalText font="600 36px/1.2 system-ui" color={demoTheme() === "dark" ? "#ddd" : "#333"} theme={demoTheme()}>JGuang</MetalText></GraphicsGuard></DemoFrame>;
  return <DemoFrame status={`preset=${preset}, paused=${paused}, strength=${strength.toFixed(2)}`} controls={<>
    <label>{t("Preset", "预设")} <select style={control} aria-label={t("Metal preset", "金属预设")} value={preset} onChange={e => setPreset(e.target.value as MetalFxPreset)}>{["chromatic", "silver", "gold"].map(s => <option key={s}>{s}</option>)}</select></label>
    <label>{t("Strength", "强度")} <input aria-label={t("Metal strength", "金属强度")} type="range" min="0" max="1" step="0.05" value={strength} onChange={e => setStrength(Number(e.target.value))} /></label>
    <button type="button" style={control} aria-pressed={paused} onClick={() => setPaused(!paused)}>{paused ? t("Resume", "继续动画") : t("Pause", "暂停动画")}</button>
  </>}><GraphicsGuard kind="webgl2"><MetalFx preset={preset} paused={paused} strength={strength} theme={demoTheme()}><button type="button" style={{ ...control, padding: "12px 28px", borderRadius: 999 }}>{t("Native metal surface", "原生金属表面")}</button></MetalFx></GraphicsGuard></DemoFrame>;
}

```


## 可用状态与示例

彩色金属、银色金属、金色金属、暂停动画、金属文字

组件中的状态与数据为演示用途，不包含后端接口。接入实际项目时请检查源码中的公开参数，再连接业务数据。
# metal-fx

Animated WebGL "liquid metal" effect for React. Wrap a button, chip, or icon and it gets a real-time metal ring with optional proximity reflection on neighbouring elements.

[Live demo](https://metal.jakubantalik.com) · [Repository](https://github.com/Jakubantalik/metal-fx) · [Report an issue](https://github.com/Jakubantalik/metal-fx/issues)

## v2

v2 replaces the plasma shader with [Paper Shaders'](https://github.com/paper-design/shaders) `liquidMetal` (Apache-2.0, see `NOTICE`) and adds:

- `MetalText` — metal inside the glyphs of any text, with an inner-shadow rim.
- `MetalBadge` — the "New" pill: white fill, metal rim, clean core under the label.
- `useMetalBend(ref)` — cursor-driven liquid dent of the ring (spring physics, vector outline).
- `useMetalTextReflection(ref)` — a neighbouring text catches the metal on its letterforms.
- `innerShadow` prop — light rim along the ring's top inside edge.
- Cursor light — the ring lights the pointer (`setCursorSprite`, `setCursorLightConfig`); opt-in, macOS sprite required.
- Baked-sprite glow: ~10× cheaper than v1's SVG filters, same look.
- Live-tunable `GLOW`, `REFLECTION_OCCLUDER` and `CURSOR_LIGHT` configs.

Requires WebGL2 (`isMetalFxSupported()`); unsupported browsers render the plain child. Presets, variants and the `MetalFx` props from v1 are unchanged. To stay on the old engine: `npm install metal-fx@1`.

## Install

```bash
npm install metal-fx
```

## Quick start

```tsx
import { MetalFx } from 'metal-fx';

function App() {
  return (
    <MetalFx variant="button">
      <button className="upgrade-pill">Upgrade to Pro</button>
    </MetalFx>
  );
}
```

The component wraps a single child host element, measures it, and paints an animated metal ring on top. The child stays fully interactive — overlays sit above it with `pointer-events: none`.

## Variants

```tsx
<MetalFx variant="button">  {/* Pill silhouette, 1 px ring, scale 1.6 */}
  <button>Upgrade to Pro</button>
</MetalFx>

<MetalFx variant="circle">  {/* Compact circle, 2 px ring, scale 1.3 */}
  <button>↑</button>
</MetalFx>
```

## Presets

Three bundled palettes, each with a tuned dark and light mode block:

```tsx
<MetalFx preset="chromatic" />  {/* Iridescent rainbow (default) */}
<MetalFx preset="silver" />     {/* Cool steel */}
<MetalFx preset="gold" />       {/* Warm gold */}
```

## Theme

```tsx
<MetalFx theme="auto" />    {/* Follows prefers-color-scheme (default) */}
<MetalFx theme="dark" />    {/* Pin to dark backgrounds */}
<MetalFx theme="light" />   {/* Pin to light backgrounds */}
```

`auto` reads the OS / browser theme on mount and subscribes to live changes via `matchMedia('(prefers-color-scheme: dark)')`, so the metal frame switches over instantly when the user toggles their system theme. SSR-safe — the initial render falls back to `dark` and rehydrates to the resolved theme on the client.

If your app has its own theme toggle that doesn't follow the OS, drive `theme` from your app state instead:

```tsx
const appTheme = useAppTheme(); // 'dark' | 'light'
<MetalFx theme={appTheme}>...</MetalFx>
```

## Strength

```tsx
<MetalFx strength={0.7}>  {/* 70% effect intensity */}
  <button>Upgrade to Pro</button>
</MetalFx>
```

`strength` runs from `0` (invisible) to `1` (full, default). It scales the canvas and glow opacity without changing the underlying shader animation.

## Paused

```tsx
<MetalFx paused>
  <button>Upgrade to Pro</button>
</MetalFx>
```

Freezes the shader on its current frame. The metal silhouette stays visible.

## Proximity reflection (dark mode only)

Pass refs to neighbouring elements and they receive a soft, mirrored reflection of the metal ring:

```tsx
const sendRef = useRef<HTMLButtonElement>(null);
const chipRef = useRef<HTMLButtonElement>(null);

<>
  <button ref={chipRef}>Tools</button>
  <MetalFx variant="circle" reflectionTargets={[chipRef]}>
    <button ref={sendRef} aria-label="Send">↑</button>
  </MetalFx>
</>
```

Reflections are skipped automatically when the resolved theme is `light` — no DOM scanning, no per-frame work in light mode.

## Performance

- One shared WebGL context is reused across every mounted `<MetalFx>` on the page. The shader is compiled once.
- A single `requestAnimationFrame` loop drives every instance. Per-frame work for one mount: a `gl.drawArrays` plus N×`drawImage` copies (one per visible instance).
- `IntersectionObserver` pauses per-instance copies when the host scrolls offscreen. When every instance is offscreen the GL render is skipped too.
- `ResizeObserver` callbacks are debounced through RAF.
- The GL context, program, and buffer are released when the last `<MetalFx>` unmounts.

## Server-side rendering

The component renders a transparent placeholder during SSR and only mounts the WebGL pipeline after hydration on the client. No flash of broken effect, no SSR errors.

## Sizing

`MetalFx` does not force any dimensions onto the wrapped child — the wrapper sizes itself to whatever the child renders. Style your child the way you normally would (intrinsic content, CSS class, or inline style):

```tsx
// Pattern 1 (recommended): size the child.
<MetalFx variant="circle">
  <button style={{ width: 36, height: 36 }} aria-label="Send">↑</button>
</MetalFx>

<MetalFx>
  <button className="rounded-full px-6 h-10">Upgrade to Pro</button>
</MetalFx>
```

If you want a metal frame larger than the child (e.g. padding around an icon), size the wrapper instead and explicitly stretch the child to fill:

```tsx
// Pattern 2: size the wrapper, child fills.
<MetalFx style={{ width: 36, height: 36 }} variant="circle">
  <button style={{ width: '100%', height: '100%' }} aria-label="Send">↑</button>
</MetalFx>
```

Both patterns work; pick whichever fits your layout. The wrapper is `display: inline-flex` so it lays out inline like a button.

## Custom border radius

By default `MetalFx` reads the computed `border-radius` of the wrapped child each resize. Pass an explicit override when needed:

```tsx
<MetalFx borderRadius={20}>
  <button>Upgrade to Pro</button>
</MetalFx>
```

## License

MIT &copy; [Jakub Antalik](https://github.com/Jakubantalik). See [LICENSE](https://github.com/Jakubantalik/Libraries.dev/blob/b7f44b8744f4e09ff088d824719636a513110e2a/packages/metal-fx/LICENSE).

## Native ports (v2)

The v2 effect is also available for the phone, in `ports/`:

- **SwiftUI** — `ports/ios/MetalFxKit` (iOS 17+, Metal shader via SwiftUI
  `colorEffect` / `layerEffect`). `MetalFx`, `MetalText`, `MetalBadge`,
  `.metalReflection(of:)`, tilt-driven bend, glow, inner shadow, and a
  screen-edge liquid lens (`.metalEdgeHalo()`). Demo app in
  `ports/ios/MetalFxDemo` (`./run.sh`).
- **React Native** — `ports/react-native/metal-fx-native` (react-native-skia +
  Reanimated 4, everything on the UI thread). Same components; `<MetalReflection>`,
  `<MetalReflectionText>`, `<MetalEdgeHalo>`. Expo example in
  `ports/react-native/example`.

Both ports render the same material (Paper's `liquidMetal`, ported to MSL and
SkSL) with the same presets and tuning as the web; there is no cursor on a
phone, so the cursor light is web-only and the phone's tilt plays the pointer.


## 源码参数参考

以下实现签名和类型从本地固定版本源码生成。保留可选属性、原生继承类型、联合分支和 never 限制；不会将其他设计系统的 API 混入。forwardRef 实现中的 ref 属于 React 引用，不是新增业务属性。此区域是原始类型参考，不是上游说明的全文翻译。


```ts
MetalFx({
    children,
    variant = 'button',
    preset = 'chromatic',
    theme = 'auto',
    strength = 1,
    glowGain = 1,
    paused = false,
    borderRadius,
    normalizeHostStyles = true,
    reflectionTargets,
    disableGlow = false,
    innerShadow,
    shaderScale,
    ringCssPx,
    scale = 1,
    mask,
    glowMode = 'mask',
    className,
    style,
    ...rest
  }, forwardedRef)

MetalText({
  children,
  font,
  color,
  strength = 1,
  theme,
  reflectionTargets,
  className,
  innerShadow = FIGMA_INNER_SHADOW,
  glow = false,
  glowGain = METAL_TEXT_DEFAULTS.glowGain,
  metalOpacity = METAL_TEXT_DEFAULTS.metalOpacity,
  shaderScale = METAL_TEXT_DEFAULTS.shaderScale,
}: {
  children: string;
  /** CSS `font` shorthand for the live span, e.g. `500 24px/1 Inter, sans-serif`. */
  font: string;
  /** Base text colour from the design; the metal composites over it. */
  color: string;
  strength?: number;
  theme?: MetalFxTheme;
  reflectionTargets?: ReadonlyArray<MetalFxReflectionTarget>;
  className?: string;
  /** Top-edge light rim inside the glyphs. Pass null to disable. */
  innerShadow?: TextInnerShadow | null;
  /** Halo on the glyphs. Off by default — the design has none. */
  glow?: boolean;
  /** Glow multiplier when `glow` is on. */
  glowGain?: number;
  /** How much metal shows over the base colour (0..1), multiplied with `strength`. */
  metalOpacity?: number;
  /** Zoom of the metal inside the glyphs. */
  shaderScale?: number;
})
```



```ts
export type MetalFxVariant = 'button' | 'circle';

export type MetalFxTheme = 'dark' | 'light' | 'auto';

export type MetalFxPreset = 'chromatic' | 'silver' | 'gold';

export type MetalFxReflectionTarget =
  | RefObject<HTMLElement | null>
  | { ref: RefObject<HTMLElement | null>; strength?: number };

export interface MetalFxProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
  /**
   * The single host element to wrap with the metal effect. Must be a single
   * React node (button, anchor, div, ...). MetalFx measures this element each
   * frame via `ResizeObserver` and paints its canvas + glow on top.
   */
  children: ReactNode;

  /**
   * Variant — controls the shader sampling scale + ring width.
   * - `button` (default): pill-style ring at 1 px wide, scale 1.6
   * - `circle`: compact circle with a 2 px ring, scale 1.3
   */
  variant?: MetalFxVariant;

  /**
   * Color preset. All three presets ship both dark and light mode tunings —
   * `theme` picks the right side at runtime.
   * @default 'chromatic'
   */
  preset?: MetalFxPreset;

  /**
   * Theme mode. `'auto'` (default) resolves via
   * `matchMedia('(prefers-color-scheme: dark)')` and switches live when the
   * OS theme changes. Pass `'dark'` or `'light'` to pin a specific mode.
   * @default 'auto'
   */
  theme?: MetalFxTheme;

  /**
   * Effect strength (0..1). Multiplies the shader bitmap opacity and the glow
   * SVG alpha. The shader continues to animate at full intensity at any value;
   * only the rendered alpha onto the host is scaled. Lighter values let the
   * underlying child surface show through more strongly.
   * @default 1
   */
  strength?: number;

  /**
   * Multiplier on the glow only (halo + catch-light), on top of `strength`.
   * Use it when the shader runs at low `strength` but the glow should still
   * read — e.g. metal text. Clamped to 0..1 after multiplying.
   * @default 1
   */
  glowGain?: number;

  /**
   * Pause the shader animation. The visible canvas keeps the last painted
   * frame so the metal silhouette stays on screen.
   * @default false
   */
  paused?: boolean;

  /**
   * Optional explicit border radius (CSS px). When omitted, MetalFx reads the
   * computed border-radius of the wrapped child each resize.
   */
  borderRadius?: number;

  /**
   * When true, MetalFx normalizes the host element's outer chrome (border /
   * outline / box-shadow) so user-provided component styles don't clash with
   * the metal ring. Inner fills, typography, and content remain untouched.
   * @default true
   */
  normalizeHostStyles?: boolean;

  /**
   * Neighbour elements that should receive a soft proximity reflection of the
   * metal effect. Reflections only render when the resolved theme is `dark` —
   * pass-through in light mode (no DOM scan, no per-frame work).
   *
   * Pass refs to the sibling DOM elements you want to receive the reflection
   * (chips next to a send button, search field next to an Upgrade pill, ...).
   *
   * A target that *contains* the wrapped element (its parent card, say)
   * receives the reflection on both the nearest vertical and horizontal
   * inner edges. Pair with `{ ref, strength }` to keep that subtle.
   */
  reflectionTargets?: ReadonlyArray<MetalFxReflectionTarget>;

  /**
   * Disable the wandering halo overlay. The shader ring still renders.
   * @default false
   */
  disableGlow?: boolean;

  /**
   * Light rim along the top inside edge of the ring — an inner shadow from
   * above, like the one on metal text. `true` uses the design defaults
   * (white 90 %, offset 1 px, blur 0.5 px); pass an object to tune.
   * @default undefined (off)
   */
  innerShadow?: boolean | { offsetY?: number; blur?: number; alpha?: number; color?: string };

  /**
   * Override the shader sampling scale. Larger values zoom into the shared
   * shader (visibly bigger pattern features); smaller values zoom out.
   * Defaults to the variant's baseline (1.6 for `'button'`, 1.3 for
   * `'circle'`) multiplied by `scale`.
   */
  shaderScale?: number;

  /**
   * Override the ring thickness in CSS pixels. Defaults to the variant's
   * baseline (1 for `'button'`, 2 for `'circle'`) multiplied by `scale`.
   */
  ringCssPx?: number;

  /**
   * Master scale multiplier for every absolute-pixel constant the engine
   * uses internally. Set this when you render the wrapped element at a
   * non-1× size (e.g. inside a CSS `zoom: 2` container, or when you've
   * doubled all your Tailwind sizes by hand). It scales:
   *   - shader sampling (so pattern features grow proportionally)
   *   - ring thickness on the canvas
   *   - glow SVG stroke widths, blur radii, fade-circle radius, and the
   *     small inset/outward offsets that position the catch-light
   *   - reflection canvas stroke band, border-highlight thickness, and the
   *     reference draw width baseline
   * @default 1
   */
  scale?: number;

  /**
   * Custom alpha mask painter. When set, the shader is kept only where the
   * mask paints (device px, origin at the wrapper's top-left) and the ring
   * punch is skipped — for metal-filled text or glyphs. Pair with
   * `disableGlow`; the glow assumes a ring perimeter.
   */
  mask?: MaskFn;

  /**
   * How the glow is placed when `mask` is set. `'mask'` (default) samples
   * points inside the mask and clips the halo to it — right for metal text.
   * `'ring'` keeps the rounded-rect perimeter behaviour — a halo along the
   * element's edge — for a full-fill mask like a badge.
   */
  glowMode?: 'mask' | 'ring';

  /**
   * Forwarded class name for the wrapper element.
   */
  className?: string;

  /**
   * Forwarded inline styles for the wrapper element.
   */
  style?: CSSProperties;
}

export interface TextInnerShadow { offsetY: number; blur: number; alpha: number }
```


### 源码中的解构默认值

只列出直接写在参数或 props 解构中的默认表达式。条件表达式、运行时主题和其他计算值请结合源码；这里不是全部行为的默认值清单。


```ts
// MetalFx
variant = 'button'

// MetalFx
preset = 'chromatic'

// MetalFx
theme = 'auto'

// MetalFx
strength = 1

// MetalFx
glowGain = 1

// MetalFx
paused = false

// MetalFx
normalizeHostStyles = true

// MetalFx
disableGlow = false

// MetalFx
scale = 1

// MetalFx
glowMode = 'mask'

// MetalText
strength = 1

// MetalText
innerShadow = FIGMA_INNER_SHADOW

// MetalText
glow = false

// MetalText
glowGain = METAL_TEXT_DEFAULTS.glowGain

// MetalText
metalOpacity = METAL_TEXT_DEFAULTS.metalOpacity

// MetalText
shaderScale = METAL_TEXT_DEFAULTS.shaderScale
```


### 原生与外部类型

以下外部类型仍由 React 或资源自身依赖定义，没有手工展开为虚构属性。实际可用类型以安装版本的 TypeScript 检查为准。


```ts
HTMLDivElement
Omit<HTMLAttributes<HTMLDivElement>, 'children'>
HTMLAttributes<HTMLDivElement>
ReactNode
ReadonlyArray<MetalFxReflectionTarget>
RefObject<HTMLElement | null>
HTMLElement
MaskFn
CSSProperties
```

---

JGuang 全部资源索引: https://jiguang132.com/llms.txt
