Zum Inhalt springen
UI Kit
Accent

Formen

Abstrakte Formen zur Dekoration, gezeichnet in SVG mit den Farben des Themes. Blobs, Ringe, Bögen, Stapel, ein Umlauf, ein Wellenband, eine Blüte, eine Punktkugel, ein Schnörkel und ein Stern, mit ruhigem Schweben, Drehen und Atmen.

Eine von 3 Components in Muster und Formen

4 Variants

Variants

Blobs

Organische Umrisse aus einem Seed in drei Tönen. Sie schweben im eigenen Takt und ziehen beim Scrollen in verschiedener Tiefe.

Ringe und Umläufe

Ringe, ein Umlauf mit Mond, ein Stern und eine Punktkugel mit Licht von oben links. Die Bewegung gilt für alle vier.

Geometrisches Set

Bogen, Stapel, Wellenband, Blüte, Schnörkel und Stern. Der Umschalter zeigt Akzent, ruhig und Kontur.

Komposition

Formen um eine Überschrift. Jede Ebene folgt dem Zeiger in eigener Tiefe und zieht beim Scrollen in eigenem Tempo.

Details

Was Formen mitbringt, aus Registry und Quellcode gelesen.

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

Props

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

  • kindoptional
    ShapeKind

    blobringsarchstackorbitwaveflowerspheresquiggleburst

  • seedoptional
    number
  • toneoptional
    ShapeTone

    accentmutedoutline

    accent (default) fills with the accent color, muted with a quiet neutral, outline draws only lines

  • sizeoptional
    number | string

    Width and height, a number in pixels or any CSS length. Default 160.

  • animateoptional
    ShapeMotion

    nonefloatspinbreathe

    float, spin and breathe are slow loops, default none

  • durationoptional
    number

    Seconds per loop, each motion has its own calm default

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, 253 Zeilen
components/ui/shapes.tsx
'use client';

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

/*
 * Abstract shapes for decoration: blobs, rings, arches, stacks, an orbit, a
 * wave band, a flower, a dotted sphere, a squiggle and a star burst. Drawn in
 * SVG with the kit tokens, so they follow the theme and the accent. Seeds
 * change the outline of blob, squiggle and burst and the angle of the orbit.
 *
 * The motion is calm and moves only transform. It pauses out of view and is
 * off with reduced motion, the shape then simply stands still.
 */

export type ShapeKind = 'blob' | 'rings' | 'arch' | 'stack' | 'orbit' | 'wave' | 'flower' | 'sphere' | 'squiggle' | 'burst';
export type ShapeTone = 'accent' | 'muted' | 'outline';
export type ShapeMotion = 'none' | 'float' | 'spin' | 'breathe';

export interface ShapeProps {
  kind?: ShapeKind;
  seed?: number;
  /** accent (default) fills with the accent color, muted with a quiet neutral, outline draws only lines */
  tone?: ShapeTone;
  /** Width and height, a number in pixels or any CSS length. Default 160. */
  size?: number | string;
  /** float, spin and breathe are slow loops, default none */
  animate?: ShapeMotion;
  /** Seconds per loop, each motion has its own calm default */
  duration?: number;
  className?: string;
}

/** A closed smooth outline through points (Catmull-Rom as cubic Bézier) */
function closedPath(pts: [number, number][]) {
  const n = pts.length;
  const p = (i: number) => pts[(i + n) % n];
  let d = `M${round(p(0)[0], 1)} ${round(p(0)[1], 1)}`;
  for (let i = 0; i < n; i++) {
    const [p0, p1, p2, p3] = [p(i - 1), p(i), p(i + 1), p(i + 2)];
    const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
    const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
    d += `C${round(c1[0], 1)} ${round(c1[1], 1)} ${round(c2[0], 1)} ${round(c2[1], 1)} ${round(p2[0], 1)} ${round(p2[1], 1)}`;
  }
  return `${d}Z`;
}

/** An open smooth line through points */
function openPath(pts: [number, number][]) {
  let d = `M${round(pts[0][0], 1)} ${round(pts[0][1], 1)}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[Math.max(0, i - 1)];
    const p1 = pts[i];
    const p2 = pts[i + 1];
    const p3 = pts[Math.min(pts.length - 1, i + 2)];
    d += `C${round(p1[0] + (p2[0] - p0[0]) / 6, 1)} ${round(p1[1] + (p2[1] - p0[1]) / 6, 1)} ${round(p2[0] - (p3[0] - p1[0]) / 6, 1)} ${round(p2[1] - (p3[1] - p1[1]) / 6, 1)} ${round(p2[0], 1)} ${round(p2[1], 1)}`;
  }
  return d;
}

const blobPoints = (seed: number, cx: number, cy: number, r: number, n = 7, wobble = 0.34): [number, number][] =>
  Array.from({ length: n }, (_, i) => {
    const a = (i / n) * Math.PI * 2 + rand(seed, 99) * 0.6;
    const rr = r * (1 - wobble / 2 + rand(seed, i) * wobble);
    return [cx + Math.cos(a) * rr, cy + Math.sin(a) * rr];
  });

const MOTION: Record<Exclude<ShapeMotion, 'none'>, { frames: string; time: number; ease: string }> = {
  float: { frames: '0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(0,-6%,0)}', time: 7, ease: 'ease-in-out' },
  spin: { frames: 'to{transform:rotate(360deg)}', time: 60, ease: 'linear' },
  breathe: { frames: '0%,100%{transform:scale(0.95)}50%{transform:scale(1.04)}', time: 8, ease: 'ease-in-out' },
};

/**
 * One decorative shape. Sits inline at its size, place it absolutely to put it
 * behind or beside content.
 *
 *   <Shape kind="blob" seed={4} tone="accent" animate="float" size={220} />
 */
export function Shape({ kind = 'blob', seed = 1, tone = 'accent', size = 160, animate = 'none', duration, className }: ShapeProps) {
  const sid = useSceneId('ksh');
  const play = useScenePlay<HTMLSpanElement>('in-view');
  const g = `${sid}g`;
  const outline = tone === 'outline';
  const base = tone === 'accent' ? 'var(--primary)' : 'var(--foreground)';
  const lo = tone === 'accent' ? `color-mix(in oklab, var(--primary) 34%, var(--background))` : `color-mix(in oklab, var(--foreground) 9%, var(--background))`;
  const hi = tone === 'accent' ? `var(--primary)` : `color-mix(in oklab, var(--foreground) 26%, var(--background))`;
  const mid = tone === 'accent' ? `color-mix(in oklab, var(--primary) 66%, var(--background))` : `color-mix(in oklab, var(--foreground) 16%, var(--background))`;
  const line = outline ? 'color-mix(in oklab, var(--foreground) 46%, transparent)' : base;
  const fillA: React.CSSProperties = outline ? { fill: 'none', stroke: line, strokeWidth: 1.5 } : { fill: `url(#${g})` };
  const fill = (c: string): React.CSSProperties => (outline ? { fill: 'none', stroke: line, strokeWidth: 1.5 } : { fill: c });
  const stroke = (c: string, w: number): React.CSSProperties => ({ fill: 'none', stroke: outline ? line : c, strokeWidth: outline ? Math.min(w, 1.5) : w, strokeLinecap: 'round', strokeLinejoin: 'round' });

  let art: React.ReactNode = null;
  let spinPart: React.ReactNode = null;
  switch (kind) {
    case 'blob': {
      const d = closedPath(blobPoints(seed, 100, 100, 78));
      art = outline ? (
        <>
          <path d={d} style={fillA} vectorEffect="non-scaling-stroke" />
          <path d={closedPath(blobPoints(seed, 100, 100, 58))} style={fillA} vectorEffect="non-scaling-stroke" opacity="0.7" />
          <path d={closedPath(blobPoints(seed, 100, 100, 36))} style={fillA} vectorEffect="non-scaling-stroke" opacity="0.45" />
        </>
      ) : (
        <>
          <path d={d} style={fillA} />
        </>
      );
      break;
    }
    case 'rings':
      art = (
        <>
          {[86, 70, 54, 38].map((r, i) => (
            <circle key={r} cx="100" cy="100" r={r} style={{ ...stroke(i % 2 ? mid : hi, 7 - i), opacity: outline ? 1 - i * 0.12 : 0.55 + i * 0.15 }} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />
          ))}
          <circle cx="100" cy="100" r="18" style={outline ? stroke(line, 1.5) : { fill: hi }} />
        </>
      );
      break;
    case 'arch':
      art = (
        <>
          {[0, 1, 2, 3].map((i) => {
            const r = 84 - i * 18;
            return <path key={i} d={`M${100 - r} 178A${r} ${r} 0 0 1 ${100 + r} 178`} style={{ ...stroke([hi, mid, lo, hi][i], 16), strokeLinecap: 'butt' }} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />;
          })}
          <path d="M14 186H186" style={stroke(outline ? line : mid, 2)} />
        </>
      );
      break;
    case 'stack':
      art = (
        <>
          {[0, 1, 2].map((i) => (
            <rect key={i} x={30 + i * 22} y={66 - i * 22} width="96" height="96" rx="18" transform={`rotate(${-10 + i * 10} ${78 + i * 22} ${114 - i * 22})`} style={outline ? { ...fill(''), opacity: 0.55 + i * 0.22 } : { fill: [lo, mid, hi][i], stroke: 'var(--background)', strokeWidth: 2 }} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />
          ))}
        </>
      );
      break;
    case 'orbit': {
      const a = rand(seed, 3) * 360;
      art = (
        <>
          <circle cx="100" cy="100" r="80" style={{ ...stroke(mid, 1.5), strokeDasharray: outline ? undefined : '2 5' }} vectorEffect="non-scaling-stroke" />
          <circle cx="100" cy="100" r="44" style={fillA} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />
                  </>
      );
      spinPart = (
        <g transform={`rotate(${round(a, 1)} 100 100)`}>
          <circle cx="180" cy="100" r="10" style={outline ? stroke(line, 1.5) : { fill: hi, stroke: 'var(--background)', strokeWidth: 3 }} />
        </g>
      );
      break;
    }
    case 'wave':
      art = (
        <>
          {[0, 1, 2].map((i) => {
            const y = 70 + i * 30;
            return <path key={i} d={`M12 ${y}C42 ${y - 30} 58 ${y - 30} 78 ${y}S114 ${y + 30} 134 ${y}S170 ${y - 30} 188 ${y}`} style={stroke([hi, mid, lo][i], 14)} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />;
          })}
        </>
      );
      break;
    case 'flower': {
      const n = 6 + (seed % 3) * 2;
      art = (
        <>
          {Array.from({ length: n }, (_, i) => (
            <ellipse key={i} cx="100" cy="54" rx="22" ry="44" transform={`rotate(${round((i / n) * 360, 2)} 100 100)`} style={outline ? fill('') : { fill: i % 2 ? mid : hi, opacity: 0.92 }} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />
          ))}
          <circle cx="100" cy="100" r="20" style={outline ? stroke(line, 1.5) : { fill: lo, stroke: 'var(--background)', strokeWidth: 3 }} />
        </>
      );
      break;
    }
    case 'sphere': {
      const dots: React.ReactNode[] = [];
      const step = 8.5;
      const L = [-0.52, -0.6, 0.6];
      const ln = Math.hypot(...L);
      for (let y = -80; y <= 80; y += step) {
        for (let x = -80; x <= 80; x += step) {
          const nx = x / 82;
          const ny = y / 82;
          const q = 1 - nx * nx - ny * ny;
          if (q <= 0) continue;
          const nz = Math.sqrt(q);
          const lit = Math.max(0, (nx * L[0] + ny * L[1] + nz * L[2]) / ln);
          const r = round(0.5 + lit * 3.4, 2);
          dots.push(<circle key={`${x}-${y}`} cx={round(100 + x, 1)} cy={round(100 + y, 1)} r={r} />);
        }
      }
      art = <g style={outline ? { fill: 'none', stroke: line, strokeWidth: 0.8 } : { fill: tone === 'accent' ? hi : 'color-mix(in oklab, var(--foreground) 52%, var(--background))' }}>{dots}</g>;
      break;
    }
    case 'squiggle': {
      // a cursive curl: a line that moves forward while it circles, so it loops
      const loops = 3 + (seed % 2);
      const amp = 24 + rand(seed, 1) * 8;
      const pts: [number, number][] = Array.from({ length: 64 }, (_, i) => {
        const u = i / 63;
        const a = u * Math.PI * 2 * (loops + 0.5) - Math.PI / 2;
        return [round(18 + u * 150 + Math.cos(a) * 15, 2), round(100 + Math.sin(a) * amp * (0.75 + u * 0.4), 2)];
      });
      art = <path d={openPath(pts)} style={stroke(hi, 9)} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />;
      break;
    }
    case 'burst': {
      const n = 8 + (seed % 5) * 2;
      const inner = 0.36 + rand(seed, 5) * 0.16;
      const pts = Array.from({ length: n * 2 }, (_, i) => {
        const a = (i / (n * 2)) * Math.PI * 2 - Math.PI / 2;
        const r = i % 2 ? 88 * inner : 88;
        return `${round(100 + Math.cos(a) * r, 1)},${round(100 + Math.sin(a) * r, 1)}`;
      });
      art = (
        <>
          <polygon points={pts.join(' ')} style={{ ...fillA, strokeLinejoin: 'round' }} vectorEffect={outline ? 'non-scaling-stroke' : undefined} />
        </>
      );
      break;
    }
  }

  const m = animate !== 'none' ? MOTION[animate] : null;
  const time = round(duration ?? (kind === 'orbit' && animate === 'spin' ? 14 : m?.time ?? 0), 2);
  const css = m
    ? `${SCENE_CSS}@keyframes ${sid}a{${m.frames}}.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}a ${time}s ${m.ease} infinite}.${sid}m{transform-box:view-box;transform-origin:50% 50%}`
    : '';
  const spinWhole = !(kind === 'orbit' && animate === 'spin');
  const dim = typeof size === 'number' ? `${size}px` : size;

  return (
    <span ref={play.ref} aria-hidden data-kv={m ? play.state : undefined} className={cn(sid, 'inline-block aspect-square shrink-0', className)} style={{ width: dim }}>
      {css && <style dangerouslySetInnerHTML={{ __html: css }} />}
      <svg viewBox="0 0 200 200" className="block size-full overflow-visible">
        <defs>
          <radialGradient id={g} cx="0.32" cy="0.26" r="0.85">
            <stop offset="0" style={{ stopColor: mid }} />
            <stop offset="0.75" style={{ stopColor: hi }} />
          </radialGradient>
        </defs>
        <g className={spinWhole ? `${sid}m` : undefined}>
          {art}
          {spinPart && <g className={spinWhole ? undefined : `${sid}m`}>{spinPart}</g>}
        </g>
      </svg>
    </span>
  );
}

Häufige Fragen

Wie installiere ich Formen?

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

Welche Varianten hat Formen?

4 Varianten, Blobs, Ringe und Umläufe, Geometrisches Set und Komposition.

Kann ein KI-Assistent Formen einbauen?

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

Oft gesucht

Direkt zu

In die Zwischenablage kopiert