Lichtstrahlen
Licht, das über Rasterlinien wandert, als Strahl fällt oder Kurven entlangläuft.
Eine von 6 Components in Beams, Partikel und Bewegung
Details
Was Lichtstrahlen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Beams, Partikel und Bewegung
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/beams.tsx
- Bringt mit
- utilskit
- 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 BeamsProps aus components/ui/beams.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalBeamsVariantgridrayswaves
grid: pulses travel along the lines of a fine grid, both directions. rays: vertical beams fall along hidden lines, no grid drawn. waves: light runs along a fan of soft curves from left to right.
celloptionalnumberGrid cell in px (grid and rays), default 56
countoptionalnumberNumber of beams, default 9
speedoptionalnumberAverage seconds for one beam to cross, default 7
clearCenteroptionalbooleanDims the beams behind the middle so a headline stays calm, default true
seedoptionalnumberSame seed, same picture
scrollRevealoptionalbooleanThe beams gather from a quiet third to full strength while the stage scrolls up to the middle of the window. Default false.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 237 Zeilen
'use client';
import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion, round } from '@/lib/kit';
export type BeamsVariant = 'grid' | 'rays' | 'waves';
export interface BeamsProps {
/**
* grid: pulses travel along the lines of a fine grid, both directions.
* rays: vertical beams fall along hidden lines, no grid drawn.
* waves: light runs along a fan of soft curves from left to right.
*/
variant?: BeamsVariant;
/** Grid cell in px (grid and rays), default 56 */
cell?: number;
/** Number of beams, default 9 */
count?: number;
/** Average seconds for one beam to cross, default 7 */
speed?: number;
/** Dims the beams behind the middle so a headline stays calm, default true */
clearCenter?: boolean;
/** Same seed, same picture */
seed?: number;
/** The beams gather from a quiet third to full strength while the stage scrolls up to the middle of the window. Default false. */
scrollReveal?: boolean;
className?: string;
}
const CSS = `
@keyframes kitfx-beams-y { from { transform: translate3d(0, -100%, 0); } to { transform: translate3d(0, var(--kitfx-to), 0); } }
@keyframes kitfx-beams-x { from { transform: translate3d(-100%, 0, 0); } to { transform: translate3d(var(--kitfx-to), 0, 0); } }
@keyframes kitfx-beams-dash { from { stroke-dashoffset: 16; } to { stroke-dashoffset: -100; } }
.kitfx-beams * { animation-play-state: var(--kitfx-play, running); }
.kitfx-beams[data-scroll] { opacity: calc(0.3 + 0.7 * min(1, var(--kitfx-p, 0.5) * 2)); transition: opacity .3s linear; }
@media (prefers-reduced-motion: reduce) {
.kitfx-beams[data-scroll] { opacity: 1; }
.kitfx-beams * { animation: none !important; }
.kitfx-beams [data-still-y] { transform: translate3d(0, var(--kitfx-still), 0); }
.kitfx-beams [data-still-x] { transform: translate3d(var(--kitfx-still), 0, 0); }
.kitfx-beams [data-still-dash] { stroke-dashoffset: var(--kitfx-still); }
}
`;
/** Small seeded generator, the same numbers on server and client */
function seeded(seed: number) {
let a = seed >>> 0 || 1;
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const LINE = 'color-mix(in oklch, var(--foreground) 7%, transparent)';
const HEAD = 'color-mix(in oklab, var(--primary) 90%, var(--foreground))';
/**
* Scroll pass. While `on`, writes --kitfx-p on the element: 0 when its top
* meets the bottom of the window, 1 when its bottom leaves at the top. Never
* with reduced motion, the CSS then falls back to the resting value.
*/
function useScrollPass(ref: { current: HTMLElement | null }, on: boolean) {
useEffect(() => {
const el = ref.current;
if (!el || !on || prefersReducedMotion()) return;
let frame = 0;
const update = () => {
frame = 0;
const r = el.getBoundingClientRect();
const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
el.style.setProperty('--kitfx-p', p.toFixed(4));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
el.style.removeProperty('--kitfx-p');
};
}, [ref, on]);
}
/**
* Beams of light that travel along a line grid, fall as rays or run along
* curves. Fills its parent (parent `relative isolate overflow-hidden`,
* content after it with `relative`). Decorative, pauses out of view, a
* still picture with reduced motion.
*/
export function Beams({ variant = 'grid', cell = 56, count = 9, speed = 7, seed = 7, clearCenter = true, scrollReveal = false, className }: BeamsProps) {
const ref = useRef<HTMLDivElement>(null);
useScrollPass(ref, scrollReveal);
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
io.observe(el);
return () => io.disconnect();
}, []);
const rnd = seeded(seed);
const edge =
variant === 'waves'
? 'linear-gradient(to right, transparent, #000 18%, #000 82%, transparent)'
: 'radial-gradient(ellipse 75% 70% at 50% 45%, #000 30%, transparent 80%)';
const fadeMask = clearCenter ? `${edge}, radial-gradient(ellipse var(--kitfx-clear, 36% 34%) at 50% 50%, rgb(0 0 0 / 0.1) 35%, #000 100%)` : edge;
return (
<div ref={ref} aria-hidden data-clip-ok data-scroll={scrollReveal ? '' : undefined} className={cn('kitfx-beams pointer-events-none absolute inset-0 overflow-hidden max-sm:[--kitfx-clear:62%_40%]', className)} style={{ maskImage: fadeMask, WebkitMaskImage: fadeMask, maskComposite: 'intersect', WebkitMaskComposite: 'source-in' }}>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
{variant === 'waves' ? <Waves count={count} speed={speed} rnd={rnd} /> : <GridBeams variant={variant} cell={cell} count={count} speed={speed} rnd={rnd} />}
</div>
);
}
function GridBeams({ variant, cell, count, speed, rnd }: { variant: BeamsVariant; cell: number; count: number; speed: number; rnd: () => number }) {
const half = round(cell / 2);
// Lines run through the center, so beams on k * cell from the middle sit exactly on them
const used = new Set<string>();
const beams = Array.from({ length: count }, (_, i) => {
const vertical = variant === 'rays' || i % 3 !== 2;
let k = 0;
for (let n = 0; n < 12; n++) {
k = Math.round((rnd() - 0.5) * (vertical ? 18 : 8));
if (!used.has(`${vertical}${k}`)) break;
}
used.add(`${vertical}${k}`);
const len = round(variant === 'rays' ? 18 + rnd() * 16 : 12 + rnd() * 10);
const dur = round(speed * (0.75 + rnd() * 0.6), 1);
const delay = round(-rnd() * dur * 2, 1);
const reverse = variant === 'grid' && rnd() > 0.6;
return { vertical, k, len, dur, delay, reverse, still: round(20 + rnd() * 60) };
});
return (
<>
{variant === 'grid' && (
<div
className="absolute inset-0"
style={{
backgroundImage: `linear-gradient(to right, ${LINE} 1px, transparent 1px), linear-gradient(to bottom, ${LINE} 1px, transparent 1px)`,
backgroundSize: `${cell}px ${cell}px`,
backgroundPosition: `calc(50% + ${half}px) calc(50% + ${half}px)`,
}}
/>
)}
{beams.map((b, i) => {
const to = `${round((100 / b.len) * 100)}%`;
const pos = `calc(50% + ${b.k * cell - 2}px)`;
const anim = `${b.vertical ? 'kitfx-beams-y' : 'kitfx-beams-x'} ${b.dur}s linear ${b.delay}s infinite${b.reverse ? ' reverse' : ''}`;
const still = `${round(((b.still / 100) * 100) / b.len * 100 - 100)}%`;
const dir = b.vertical ? (b.reverse ? 'to top' : 'to bottom') : b.reverse ? 'to left' : 'to right';
return (
<div key={i} className={cn('absolute overflow-hidden', b.vertical ? 'inset-y-0 w-[5px]' : 'inset-x-0 h-[5px]')} style={b.vertical ? { left: pos } : { top: pos }}>
<div
{...(b.vertical ? { 'data-still-y': '' } : { 'data-still-x': '' })}
className={cn('absolute', b.vertical ? 'inset-x-0 top-0' : 'inset-y-0 left-0')}
style={{
[b.vertical ? 'height' : 'width']: `${b.len}%`,
['--kitfx-to' as string]: to,
['--kitfx-still' as string]: still,
transform: `translate3d(${b.vertical ? `0, ${still}` : `${still}, 0`}, 0)`,
animation: anim,
}}
>
{/* soft halo and the hairline core, brightest at the head */}
<span
className={cn('absolute opacity-60 blur-[1.5px] dark:opacity-90', b.vertical ? 'inset-y-0 left-px w-[3px]' : 'inset-x-0 top-px h-[3px]')}
style={{ background: `linear-gradient(${dir}, transparent 10%, color-mix(in oklch, ${HEAD} 45%, transparent) 70%, ${HEAD})` }}
/>
<span
className={cn('absolute', b.vertical ? 'inset-y-0 left-[2px] w-px' : 'inset-x-0 top-[2px] h-px')}
style={{ background: `linear-gradient(${dir}, transparent, color-mix(in oklch, ${HEAD} 60%, transparent) 55%, ${HEAD} 94%, var(--foreground))` }}
/>
</div>
</div>
);
})}
</>
);
}
function Waves({ count, speed, rnd }: { count: number; speed: number; rnd: () => number }) {
const lines = Math.max(5, Math.min(count, 12));
const W = 1200;
const H = 600;
const paths = Array.from({ length: lines }, (_, i) => {
const t = i / (lines - 1);
// A fan: the lines meet in the middle band and open toward both edges
const spread = (t - 0.5) * 2;
const y0 = round(H / 2 + spread * 250);
const y1 = round(H / 2 + spread * 46);
const y2 = round(H / 2 + spread * 230);
const d = `M -20 ${y0} C ${W * 0.22} ${y0}, ${W * 0.3} ${y1}, ${W / 2} ${y1} S ${W * 0.78} ${y2}, ${W + 20} ${y2}`;
const dur = round(speed * (0.8 + rnd() * 0.5), 1);
return { d, dur, delay: round(-rnd() * dur, 1), still: round(-(10 + rnd() * 70)) };
});
return (
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" className="absolute inset-0 h-full w-full" fill="none">
{paths.map((p, i) => (
<path key={`l${i}`} d={p.d} stroke="var(--foreground)" className="opacity-[0.13] dark:opacity-[0.1]" strokeWidth={1} vectorEffect="non-scaling-stroke" />
))}
{paths.map((p, i) => (
<g key={`b${i}`}>
<path
data-still-dash=""
d={p.d}
pathLength={100}
stroke={HEAD}
strokeWidth={4}
strokeLinecap="round"
strokeDasharray="9 300"
className="opacity-40 blur-[3px] dark:opacity-60"
style={{ ['--kitfx-still' as string]: p.still, strokeDashoffset: p.still, animation: `kitfx-beams-dash ${p.dur}s linear ${p.delay}s infinite` }}
/>
<path
data-still-dash=""
d={p.d}
pathLength={100}
stroke={HEAD}
strokeWidth={1.5}
strokeLinecap="round"
strokeDasharray="9 300"
vectorEffect="non-scaling-stroke"
style={{ ['--kitfx-still' as string]: p.still, strokeDashoffset: p.still, animation: `kitfx-beams-dash ${p.dur}s linear ${p.delay}s infinite` }}
/>
</g>
))}
</svg>
);
}Häufige Fragen
Wie installiere ich Lichtstrahlen?
Mit aiw add @ai-website/beams. Der Befehl legt die Datei components/ui/beams.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Lichtstrahlen?
3 Varianten, Raster, Strahlen und Wellen. Du wählst sie mit der Prop variant, zum Beispiel variant="rays".
Kann ein KI-Assistent Lichtstrahlen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Lichtstrahlen mit aiw_components und installieren sie mit aiw_add @ai-website/beams. Nenn im Prompt die Variante, etwa variant="grid", dann schreibt der Assistent deine Inhalte direkt in die Props.