Zum Inhalt springen
UI Kit
Accent

Muster

Hintergrundmuster in den Farben des Themes. Punkte, Raster, Linien, Höhenlinien, Korn, Lichtfelder, Wellen, Raster-Punkte und Ringe, jeweils mit Ton, Ausblendung, Seed und einer sehr langsamen Drift.

Eine von 3 Components in Muster und Formen

8 Variants

Variants

Punkte

Ein Punktfeld, das je Schleife um eine Zelle wandert. Der Hero zeigt Ton und Ausblendung live.

Raster

Feine Linien, jede vierte kräftiger, mit einer kleinen Marke auf jeder Kreuzung.

Linien

Linien und diagonale Schraffur für redaktionelle Seiten und ruhige Bänder.

Höhenlinien

Topografische Linien aus einem Höhenfeld mit Seed, nachgezogen und geglättet. Jede vierte Linie ist eine Zähllinie, die langsam aufleuchtet.

Korn

Ein Korn aus SVG-Turbulenz, gefärbt über einen Token. Über einem Mesh wirkt es wie gedruckte Farbe.

Lichtfelder und Mesh

Weiche Lichtfelder, die auf eigenen langsamen Bahnen wandern, oder ein volles Mesh, das sich um wenige Grad dreht. Beide mischen aus dem Akzent.

Wellen

Wellenlinien, die je Schleife um genau eine Welle seitlich wandern.

Rasterpunkte

Weiche Punkte, gewichtet mit einem Lichtfeld und bei halber Stärke geschnitten, so wachsen sie zum Licht. Das Licht wandert langsam.

Details

Was Muster mitbringt, aus Registry und Quellcode gelesen.

Bereich
Visuals
Variants
8
Version
1.0.0 · seit 1.0.0
Datei
components/ui/pattern.tsx
Bringt mit
utilskitmotion-scene
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle PatternProps aus components/ui/pattern.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • kindoptional
    PatternKind

    dotsgridlinesdiagonalcrosswavescontoursnoisegradientmeshhalftonerings

    What is drawn. Default dots.

  • sizeoptional
    number

    Scale in pixels: the tile for dots, grid, lines, diagonal, cross and waves, the line spacing for contours and rings, the dot pitch for halftone and the grain for noise. Each kind has its own default.

  • toneoptional
    PatternTone

    mutedaccent

    muted (default) draws in the foreground at low strength, accent in the accent color

  • fadeoptional
    PatternFade

    noneradialtopbottomedges

    Where the pattern fades out. top and bottom name the edge that fades. Default none.

  • seedoptional
    number

    Picks positions and shapes for contours, halftone, rings, gradient and mesh

  • animateoptional
    boolean

    A very slow drift or shimmer, paused out of view, off with reduced motion

  • strengthoptional
    number

    0 to 1, how strong the lines are. Default 1.

  • styleoptional
    React.CSSProperties

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.

Bewegung

  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 431 Zeilen
components/ui/pattern.tsx
'use client';

import { useMemo } from 'react';
import { cn, round } from '@/lib/kit';
import { SCENE_CSS, rand, useScenePlay, useSceneId } from '@/components/ui/motion-scene';

/*
 * Background patterns, drawn in SVG and CSS with the kit tokens. They fill
 * their parent (absolute inset-0) and never take clicks. Every pattern is
 * deterministic: the same kind, size and seed give the same picture on the
 * server and in the browser.
 *
 * The optional drift is very slow, moves only transform or opacity, pauses
 * out of view and stays off with reduced motion. Tiled patterns move by
 * exactly one tile per loop, so the loop has no seam.
 */

export type PatternKind = 'dots' | 'grid' | 'lines' | 'diagonal' | 'cross' | 'waves' | 'contours' | 'noise' | 'gradient' | 'mesh' | 'halftone' | 'rings';
export type PatternTone = 'muted' | 'accent';
export type PatternFade = 'none' | 'radial' | 'top' | 'bottom' | 'edges';

export interface PatternProps {
  /** What is drawn. Default dots. */
  kind?: PatternKind;
  /**
   * Scale in pixels: the tile for dots, grid, lines, diagonal, cross and waves,
   * the line spacing for contours and rings, the dot pitch for halftone and the
   * grain for noise. Each kind has its own default.
   */
  size?: number;
  /** muted (default) draws in the foreground at low strength, accent in the accent color */
  tone?: PatternTone;
  /** Where the pattern fades out. top and bottom name the edge that fades. Default none. */
  fade?: PatternFade;
  /** Picks positions and shapes for contours, halftone, rings, gradient and mesh */
  seed?: number;
  /** A very slow drift or shimmer, paused out of view, off with reduced motion */
  animate?: boolean;
  /** 0 to 1, how strong the lines are. Default 1. */
  strength?: number;
  className?: string;
  style?: React.CSSProperties;
}

const SIZE: Record<PatternKind, number> = { dots: 22, grid: 32, lines: 14, diagonal: 12, cross: 28, waves: 16, contours: 24, noise: 2, gradient: 1, mesh: 1, halftone: 14, rings: 34 };

const FADE: Record<PatternFade, React.CSSProperties> = {
  none: {},
  radial: { maskImage: 'radial-gradient(ellipse 72% 70% at 50% 50%, #000 22%, transparent 80%)' },
  top: { maskImage: 'linear-gradient(to top, #000 30%, transparent 96%)' },
  bottom: { maskImage: 'linear-gradient(to bottom, #000 30%, transparent 96%)' },
  edges: {
    maskImage: 'linear-gradient(to right, transparent, #000 16%, #000 84%, transparent), linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent)',
    maskComposite: 'intersect',
    WebkitMaskComposite: 'source-in',
  } as React.CSSProperties,
};

/** The three ink strengths and the solid ink, as CSS variables on the root */
function inks(tone: PatternTone, strength: number): React.CSSProperties {
  const s = Math.max(0, Math.min(1, strength));
  const base = tone === 'accent' ? 'var(--primary)' : 'var(--foreground)';
  const levels = tone === 'accent' ? [24, 42, 74] : [12, 20, 34];
  return {
    '--pt-1': `color-mix(in oklab, ${base} ${round(levels[0] * s, 1)}%, transparent)`,
    '--pt-2': `color-mix(in oklab, ${base} ${round(levels[1] * s, 1)}%, transparent)`,
    '--pt-3': `color-mix(in oklab, ${base} ${round(levels[2] * s, 1)}%, transparent)`,
    '--pt-solid': base,
  } as React.CSSProperties;
}

/* ------------------------------------------------------------------ */
/* Contour lines: marching squares over a seeded height field          */
/* ------------------------------------------------------------------ */

const CW = 1200;
const CH = 800;

function contourPaths(seed: number, levels: number) {
  const NX = 48;
  const NY = 32;
  const sx = CW / NX;
  const sy = CH / NY;
  const hills = Array.from({ length: 7 }, (_, i) => ({
    x: rand(seed, i) * CW,
    y: rand(seed, i + 11) * CH,
    s: 140 + rand(seed, i + 23) * 260,
    h: (rand(seed, i + 37) > 0.28 ? 1 : -0.7) * (0.45 + rand(seed, i + 41) * 0.8),
  }));
  const ph = rand(seed, 53) * 6.28;
  const field = (x: number, y: number) => {
    let v = 0.18 * Math.sin(x / 260 + ph) * Math.cos(y / 210 - ph);
    for (const k of hills) {
      const dx = x - k.x;
      const dy = y - k.y;
      v += k.h * Math.exp(-(dx * dx + dy * dy) / (2 * k.s * k.s));
    }
    return v;
  };
  const v: number[][] = [];
  let min = Infinity;
  let max = -Infinity;
  for (let j = 0; j <= NY; j++) {
    v[j] = [];
    for (let i = 0; i <= NX; i++) {
      const f = field(i * sx, j * sy);
      v[j][i] = f;
      if (f < min) min = f;
      if (f > max) max = f;
    }
  }
  const paths: { d: string; major: boolean }[] = [];
  for (let l = 1; l <= levels; l++) {
    const t = min + ((max - min) * l) / (levels + 1);
    const pts = new Map<string, [number, number]>();
    const links = new Map<string, string[]>();
    const point = (key: string, p: [number, number]) => {
      if (!pts.has(key)) pts.set(key, p);
      return key;
    };
    const lerp = (a: number, b: number) => (t - a) / (b - a);
    // edge points: h(i,j) on the top edge of row j between i and i+1, v(i,j) on the left edge of column i between j and j+1
    const top = (i: number, j: number) => point(`h${i},${j}`, [(i + lerp(v[j][i], v[j][i + 1])) * sx, j * sy]);
    const left = (i: number, j: number) => point(`v${i},${j}`, [i * sx, (j + lerp(v[j][i], v[j + 1][i])) * sy]);
    const link = (a: string, b: string) => {
      links.set(a, [...(links.get(a) ?? []), b]);
      links.set(b, [...(links.get(b) ?? []), a]);
    };
    for (let j = 0; j < NY; j++) {
      for (let i = 0; i < NX; i++) {
        const a = v[j][i] > t ? 1 : 0;
        const b = v[j][i + 1] > t ? 1 : 0;
        const c = v[j + 1][i + 1] > t ? 1 : 0;
        const d = v[j + 1][i] > t ? 1 : 0;
        const code = a * 8 + b * 4 + c * 2 + d;
        if (code === 0 || code === 15) continue;
        const T = () => top(i, j);
        const R = () => left(i + 1, j);
        const B = () => top(i, j + 1);
        const L = () => left(i, j);
        const centre = (v[j][i] + v[j][i + 1] + v[j + 1][i + 1] + v[j + 1][i]) / 4 > t;
        switch (code) {
          case 1: case 14: link(L(), B()); break;
          case 2: case 13: link(B(), R()); break;
          case 3: case 12: link(L(), R()); break;
          case 4: case 11: link(T(), R()); break;
          case 6: case 9: link(T(), B()); break;
          case 7: case 8: link(L(), T()); break;
          case 5: if (centre) { link(L(), T()); link(B(), R()); } else { link(L(), B()); link(T(), R()); } break;
          case 10: if (centre) { link(T(), R()); link(L(), B()); } else { link(L(), T()); link(B(), R()); } break;
        }
      }
    }
    // chain the segments into lines, open ends first
    const used = new Set<string>();
    const chains: [number, number][][] = [];
    const walk = (start: string) => {
      const chain: string[] = [start];
      used.add(start);
      let cur = start;
      for (;;) {
        const next = (links.get(cur) ?? []).find((n) => !used.has(n));
        if (!next) break;
        used.add(next);
        chain.push(next);
        cur = next;
      }
      const closed = chain.length > 2 && (links.get(cur) ?? []).includes(start);
      const out = chain.map((k) => pts.get(k)!);
      if (closed) out.push(out[0]);
      if (out.length > 3) chains.push(out);
    };
    for (const [k, n] of links) if (n.length === 1 && !used.has(k)) walk(k);
    for (const k of links.keys()) if (!used.has(k)) walk(k);
    // smooth through the midpoints, the vertices are the control points
    let d = '';
    for (const c of chains) {
      const m = (p: [number, number], q: [number, number]) => `${Math.round((p[0] + q[0]) / 2)} ${Math.round((p[1] + q[1]) / 2)}`;
      d += `M${Math.round(c[0][0])} ${Math.round(c[0][1])}L${m(c[0], c[1])}`;
      for (let k = 1; k < c.length - 1; k++) d += `Q${Math.round(c[k][0])} ${Math.round(c[k][1])} ${m(c[k], c[k + 1])}`;
      d += `L${Math.round(c[c.length - 1][0])} ${Math.round(c[c.length - 1][1])}`;
    }
    if (d) paths.push({ d, major: l % 4 === 0 });
  }
  return paths;
}

/* ------------------------------------------------------------------ */
/* Grain                                                               */
/* ------------------------------------------------------------------ */

const grain = (freq: number) =>
  `url("data:image/svg+xml;utf8,${encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='g' x='0' y='0'><feTurbulence type='fractalNoise' baseFrequency='${freq}' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 2.4 -1.1'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>`,
  ).replace(/%2523/g, '%23')}")`;

/* ------------------------------------------------------------------ */
/* The component                                                       */
/* ------------------------------------------------------------------ */

/**
 * A background pattern that fills its parent. Put it first inside a
 * `relative` container, the content after it.
 *
 *   <div className="relative isolate overflow-hidden">
 *     <Pattern kind="contours" tone="accent" fade="radial" />
 *     …
 *   </div>
 */
export function Pattern({ kind = 'dots', size, tone = 'muted', fade = 'none', seed = 1, animate = false, strength = 1, className, style }: PatternProps) {
  const sid = useSceneId('kpt');
  const play = useScenePlay<HTMLDivElement>('in-view');
  const s = Math.max(1, size ?? SIZE[kind]);
  const pid = `${sid}p`;

  /* tiled kinds: one tile, the layer is one period larger than its box so the drift never shows an edge */
  let period = 0;
  let tile: React.ReactNode = null;
  let drift: [number, number] = [0, 0];
  if (kind === 'dots') {
    period = s;
    tile = <circle cx={round(s / 2, 2)} cy={round(s / 2, 2)} r={round(Math.max(1.1, s / 16), 2)} style={{ fill: 'var(--pt-3)' }} />;
    drift = [s, s];
  } else if (kind === 'grid') {
    period = s * 4;
    tile = (
      <>
        <path d={[1, 2, 3].map((k) => `M${k * s + 0.5} 0V${period}M0 ${k * s + 0.5}H${period}`).join('')} style={{ stroke: 'var(--pt-1)', fill: 'none' }} shapeRendering="crispEdges" />
        <path d={`M0.5 0V${period}M0 0.5H${period}`} style={{ stroke: 'var(--pt-2)', fill: 'none' }} shapeRendering="crispEdges" />
        <rect x="-1.5" y="-1.5" width="4" height="4" style={{ fill: 'var(--pt-3)' }} />
      </>
    );
    drift = [period, period];
  } else if (kind === 'lines') {
    period = s * 6;
    tile = (
      <>
        <path d={[1, 2, 3, 4, 5].map((k) => `M0 ${k * s + 0.5}H${period}`).join('')} style={{ stroke: 'var(--pt-1)' }} shapeRendering="crispEdges" />
        <path d={`M0 0.5H${period}`} style={{ stroke: 'var(--pt-2)' }} shapeRendering="crispEdges" />
      </>
    );
    drift = [0, period];
  } else if (kind === 'diagonal') {
    period = s;
    tile = <path d={`M${round(s / 2, 2)} -1V${s + 1}`} style={{ stroke: 'var(--pt-2)', strokeWidth: 1 }} />;
    drift = [round(s * Math.SQRT2, 3), 0];
  } else if (kind === 'cross') {
    period = s;
    const c = round(s / 2, 2);
    const a = round(Math.max(2.5, s / 7), 2);
    tile = <path d={`M${round(c - a, 2)} ${c}H${round(c + a, 2)}M${c} ${round(c - a, 2)}V${round(c + a, 2)}`} style={{ stroke: 'var(--pt-3)', strokeWidth: 1.2, strokeLinecap: 'round', fill: 'none' }} />;
    drift = [s, s];
  } else if (kind === 'waves') {
    period = s * 4;
    const y = round(s / 2, 2);
    const a = round(s * 0.62, 2);
    tile = <path d={`M0 ${y}Q${s} ${round(y - a, 2)} ${s * 2} ${y}T${period} ${y}`} style={{ stroke: 'var(--pt-2)', strokeWidth: 1.1, fill: 'none' }} />;
    drift = [period, 0];
  }

  const levels = Math.max(5, Math.min(32, Math.round(340 / s)));
  const contours = useMemo(() => (kind === 'contours' ? contourPaths(seed, levels) : []), [kind, seed, levels]);

  const on = animate ? play.state : undefined;
  const css: string[] = [];
  let body: React.ReactNode = null;

  if (period) {
    const w = period;
    const h = kind === 'waves' ? s : period;
    const patternTransform = kind === 'diagonal' ? 'rotate(45)' : undefined;
    const pad = Math.ceil(Math.max(Math.abs(drift[0]), Math.abs(drift[1]), w, h)) + 2;
    css.push(`@keyframes ${sid}d{from{transform:translate3d(0,0,0)}to{transform:translate3d(${drift[0]}px,${drift[1]}px,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d ${round(Math.max(18, (Math.hypot(drift[0], drift[1]) / 2.2) * 1), 1)}s linear infinite}`);
    body = (
      <svg className={`${sid}m absolute`} style={{ inset: -pad, width: `calc(100% + ${pad * 2}px)`, height: `calc(100% + ${pad * 2}px)` }}>
        <defs>
          <pattern id={pid} width={w} height={h} patternUnits="userSpaceOnUse" patternTransform={patternTransform}>
            {tile}
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill={`url(#${pid})`} />
      </svg>
    );
  } else if (kind === 'contours') {
    css.push(`@keyframes ${sid}d{0%{transform:scale(1.06) translate3d(0,0,0)}50%{transform:scale(1.1) translate3d(-1.2%,0.8%,0)}100%{transform:scale(1.06) translate3d(0,0,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 60s ease-in-out infinite}`);
    css.push(`@keyframes ${sid}s{0%,100%{opacity:.55}50%{opacity:1}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}x{animation:${sid}s 9s ease-in-out infinite}`);
    body = (
      <svg viewBox={`0 0 ${CW} ${CH}`} preserveAspectRatio="xMidYMid slice" className={`${sid}m absolute inset-0 size-full`} style={{ transform: 'scale(1.06)' }}>
        <g fill="none" strokeLinecap="round" strokeLinejoin="round">
          {contours.map((p, i) => (
            <path key={i} d={p.d} className={p.major ? `${sid}x` : undefined} style={{ stroke: p.major ? 'var(--pt-3)' : 'var(--pt-2)', strokeWidth: p.major ? 1.5 : 1, animationDelay: p.major ? `${-i * 0.7}s` : undefined }} vectorEffect="non-scaling-stroke" />
          ))}
        </g>
      </svg>
    );
  } else if (kind === 'rings') {
    const cx = round(CW * (0.35 + rand(seed, 1) * 0.4));
    const cy = round(CH * (0.3 + rand(seed, 2) * 0.4));
    const step = s * 1.4;
    const count = Math.ceil(Math.hypot(CW, CH) / step);
    css.push(`@keyframes ${sid}r{0%,100%{opacity:.45}30%{opacity:1}60%{opacity:.45}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}c{animation:${sid}r 7s ease-in-out infinite;animation-delay:calc(var(--i) * 0.22s)}`);
    body = (
      <svg viewBox={`0 0 ${CW} ${CH}`} preserveAspectRatio="xMidYMid slice" className="absolute inset-0 size-full">
        <g fill="none">
          {Array.from({ length: count }, (_, i) => {
            const major = i % 4 === 3;
            const dashed = !major && rand(seed, i + 7) > 0.72;
            return (
              <circle
                key={i}
                cx={cx}
                cy={cy}
                r={round(step * (i + 1), 1)}
                className={`${sid}c`}
                style={{ '--i': i, stroke: major ? 'var(--pt-3)' : 'var(--pt-2)', strokeWidth: major ? 1.4 : 1, strokeDasharray: dashed ? '2 7' : undefined } as React.CSSProperties}
                vectorEffect="non-scaling-stroke"
              />
            );
          })}
          <circle cx={cx} cy={cy} r="4" style={{ fill: 'var(--pt-3)' }} />
        </g>
      </svg>
    );
  } else if (kind === 'halftone') {
    // soft dots, weighted by a light field, then cut at half alpha: the dots grow where the field is strong
    // the light sits toward one side, so text in the middle stays on small dots
    const fx = round(rand(seed, 3) < 0.5 ? 0.1 + rand(seed, 5) * 0.2 : 0.7 + rand(seed, 5) * 0.2, 3);
    const fy = round(0.2 + rand(seed, 4) * 0.6, 3);
    const p = s;
    css.push(`@keyframes ${sid}h{0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(9%,-7%,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}h 34s ease-in-out infinite;transform-box:view-box}`);
    body = (
      <svg className="absolute inset-0 size-full" style={{ opacity: tone === 'accent' ? round(0.78 * strength, 2) : round(0.3 * strength, 2) }}>
        <defs>
          <radialGradient id={`${pid}d`}>
            <stop offset="0" style={{ stopColor: 'var(--pt-solid)' }} />
            <stop offset="1" style={{ stopColor: 'var(--pt-solid)', stopOpacity: 0 }} />
          </radialGradient>
          <pattern id={pid} width={p} height={p} patternUnits="userSpaceOnUse">
            <circle cx={p / 2} cy={p / 2} r={round(p * 0.7, 2)} fill={`url(#${pid}d)`} />
          </pattern>
          <radialGradient id={`${pid}l`} cx={fx} cy={fy} r="0.72">
            <stop offset="0" stopColor="#fff" />
            <stop offset="0.55" stopColor="#fff" stopOpacity="0.62" />
            <stop offset="1" stopColor="#fff" stopOpacity="0" />
          </radialGradient>
          <mask id={`${pid}m`} maskUnits="userSpaceOnUse" x="0" y="0" width="100%" height="100%">
            <rect className={`${sid}m`} x="-20%" y="-20%" width="140%" height="140%" fill={`url(#${pid}l)`} />
          </mask>
          <filter id={`${pid}f`} x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB">
            <feComponentTransfer>
              <feFuncA type="linear" slope="16" intercept="-7.4" />
            </feComponentTransfer>
          </filter>
        </defs>
        <g filter={`url(#${pid}f)`}>
          <g mask={`url(#${pid}m)`}>
            <rect width="100%" height="100%" fill={`url(#${pid})`} />
          </g>
        </g>
      </svg>
    );
  } else if (kind === 'noise') {
    const freq = round(Math.min(1.2, Math.max(0.25, 1.8 / s)), 3);
    css.push(`@keyframes ${sid}d{from{transform:translate3d(0,0,0)}to{transform:translate3d(240px,240px,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 40s steps(48) infinite}`);
    const url = grain(freq);
    body = (
      <div
        className={`${sid}m absolute`}
        style={{ inset: -242, background: 'var(--pt-solid)', opacity: tone === 'accent' ? round(0.5 * strength, 2) : round(0.2 * strength, 2), maskImage: url, WebkitMaskImage: url, maskSize: '240px 240px', WebkitMaskSize: '240px 240px' }}
      />
    );
  } else if (kind === 'gradient') {
    const fields = [
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 34%, transparent)' : 'color-mix(in oklab, var(--foreground) 11%, transparent)', w: 70 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 22%, color-mix(in oklab, var(--info) 24%, transparent))' : 'color-mix(in oklab, var(--foreground) 7%, transparent)', w: 58 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 14%, color-mix(in oklab, var(--foreground) 6%, transparent))' : 'color-mix(in oklab, var(--foreground) 9%, transparent)', w: 50 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 20%, transparent)' : 'color-mix(in oklab, var(--foreground) 5%, transparent)', w: 44 },
    ];
    fields.forEach((_, i) => {
      css.push(`@keyframes ${sid}g${i}{0%,100%{transform:translate3d(0,0,0) scale(1)}50%{transform:translate3d(${round((rand(seed, i + 60) - 0.5) * 16, 1)}%,${round((rand(seed, i + 70) - 0.5) * 14, 1)}%,0) scale(${round(1.04 + rand(seed, i + 80) * 0.08, 3)})}}`);
      css.push(`.${sid}:not([data-kv="off"]) .${sid}g${i}{animation:${sid}g${i} ${round(22 + i * 5 + rand(seed, i + 90) * 6, 1)}s ease-in-out infinite}`);
    });
    body = fields.map((f, i) => {
      const x = round(rand(seed, i * 3 + 1) * 90 - 10, 1);
      const y = round(rand(seed, i * 3 + 2) * 80 - 15, 1);
      return (
        <div
          key={i}
          className={`${sid}g${i} absolute aspect-square rounded-full`}
          style={{ left: `${round(x - f.w / 2, 1)}%`, top: `${round(y - f.w / 2, 1)}%`, width: `${f.w}%`, background: `radial-gradient(closest-side, ${f.c}, transparent)` }}
        />
      );
    });
  } else if (kind === 'mesh') {
    const at = (i: number) => `${round(rand(seed, i * 2 + 101) * 100)}% ${round(rand(seed, i * 2 + 102) * 100)}%`;
    const colors =
      tone === 'accent'
        ? ['color-mix(in oklab, var(--primary) 78%, var(--background))', 'color-mix(in oklch, var(--primary) 55%, var(--info))', 'color-mix(in oklab, var(--primary) 30%, var(--background))', 'color-mix(in oklch, var(--primary) 60%, var(--success) 40%)', 'color-mix(in oklab, var(--primary) 45%, var(--foreground) 20%)']
        : ['color-mix(in oklab, var(--foreground) 16%, var(--background))', 'color-mix(in oklab, var(--foreground) 6%, var(--background))', 'color-mix(in oklab, var(--foreground) 24%, var(--background))', 'color-mix(in oklab, var(--foreground) 10%, var(--background))', 'color-mix(in oklab, var(--foreground) 3%, var(--background))'];
    const layers = colors.map((c, i) => `radial-gradient(${['60% 70%', '55% 60%', '70% 55%', '50% 65%', '65% 60%'][i]} at ${at(i)}, ${c}, transparent)`).join(',');
    css.push(`@keyframes ${sid}d{0%,100%{transform:rotate(0deg) scale(1.15)}50%{transform:rotate(${round(4 + rand(seed, 9) * 6, 1)}deg) scale(1.24)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 48s ease-in-out infinite}`);
    const url = grain(0.85);
    body = (
      <>
        <div className={`${sid}m absolute inset-0`} style={{ backgroundColor: 'var(--surface)', backgroundImage: layers, transform: 'scale(1.15)', opacity: round(Math.max(0.2, strength), 2) }} />
        <div className="absolute inset-0" style={{ background: 'var(--foreground)', opacity: 0.07, maskImage: url, WebkitMaskImage: url, maskSize: '240px', WebkitMaskSize: '240px' }} />
      </>
    );
  }

  return (
    <div
      ref={play.ref}
      aria-hidden
      data-kv={on}
      data-clip-ok
      className={cn(sid, 'pointer-events-none absolute inset-0 -z-10 overflow-hidden', className)}
      style={{ ...inks(tone, strength), ...FADE[fade], WebkitMaskImage: FADE[fade].maskImage, ...style }}
    >
      {animate && <style dangerouslySetInnerHTML={{ __html: SCENE_CSS + css.join('\n') }} />}
      {body}
    </div>
  );
}

Häufige Fragen

Wie installiere ich Muster?

Mit aiw add @ai-website/pattern. Der Befehl legt die Datei components/ui/pattern.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion-scene. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Muster?

8 Varianten, Punkte, Raster, Linien, Höhenlinien, Korn, Lichtfelder und Mesh, Wellen und Rasterpunkte.

Kann ein KI-Assistent Muster einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Muster mit aiw_components und installieren sie mit aiw_add @ai-website/pattern.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert