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
Variants
Details
Was Formen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Muster und Formen
- 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
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle ShapeProps aus components/ui/shapes.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
kindoptionalShapeKindblobringsarchstackorbitwaveflowerspheresquiggleburst
seedoptionalnumbertoneoptionalShapeToneaccentmutedoutline
accent (default) fills with the accent color, muted with a quiet neutral, outline draws only lines
sizeoptionalnumber | stringWidth and height, a number in pixels or any CSS length. Default 160.
animateoptionalShapeMotionnonefloatspinbreathe
float, spin and breathe are slow loops, default none
durationoptionalnumberSeconds 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
'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.