Interactive graphite card hosting a capped-budget spatial constellation network that links toward the cursor with static fallback behavior.
Preview
Distributed Telemetry Mesh
Capped node network responds to pointer coordinates while maintaining sub-millisecond frame budgets.
Customize
4 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add constellation-cardWrites components/ui/constellation-card.tsx and components/styles/constellation-card.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { ConstellationCard } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <ConstellationCard />6 );7}Props
Accessibility
Marks the interactive node canvas aria-hidden='true' while keeping card headings and copy accessible. Renders a static constellation when prefers-reduced-motion is active.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import styles from "../styles/constellation-card.module.css";5 6export interface ConstellationCardProps {7 /** Card heading */8 title?: string;9 /** Card description */10 description?: string;11 /** Number of constellation vertices (capped at 48) */12 nodeCount?: number;13 /** Maximum distance in pixels for connecting lines */14 linkDistance?: number;15 /** Drift velocity multiplier */16 nodeSpeed?: number;17 /** Optional children */18 children?: React.ReactNode;19 /** Optional className */20 className?: string;21}22 23export function ConstellationCard({24 title = "Distributed Telemetry Mesh",25 description = "Capped node network responds to pointer coordinates while maintaining sub-millisecond frame budgets.",26 nodeCount = 24,27 linkDistance = 88,28 nodeSpeed = 0.4,29 children,30 className = "",31}: ConstellationCardProps) {32 const canvasRef = React.useRef<HTMLCanvasElement>(null);33 const pointerRef = React.useRef<{ x: number | null; y: number | null }>({ x: null, y: null });34 35 React.useEffect(() => {36 const canvas = canvasRef.current;37 if (!canvas) return;38 const ctx = canvas.getContext("2d");39 if (!ctx) return;40 41 const prefersReduced =42 typeof window !== "undefined" &&43 window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;44 45 let rafId = 0;46 const safeCount = Math.min(Math.max(nodeCount, 8), 48);47 48 const resize = () => {49 const parent = canvas.parentElement;50 if (!parent) return;51 canvas.width = parent.clientWidth;52 canvas.height = parent.clientHeight;53 };54 resize();55 56 const nodes = Array.from({ length: safeCount }, (_, i) => ({57 x: ((i * 67) % 100) * 0.01 * (canvas.width || 360),58 y: ((i * 43) % 100) * 0.01 * (canvas.height || 200),59 vx: ((((i * 31) % 9) - 4) || 1) * 0.14 * nodeSpeed,60 vy: ((((i * 59) % 9) - 4) || -1) * 0.14 * nodeSpeed,61 }));62 63 const draw = () => {64 ctx.clearRect(0, 0, canvas.width, canvas.height);65 for (let i = 0; i < nodes.length; i++) {66 const a = nodes[i];67 if (!prefersReduced) {68 a.x += a.vx;69 a.y += a.vy;70 if (a.x < 0 || a.x > canvas.width) a.vx *= -1;71 if (a.y < 0 || a.y > canvas.height) a.vy *= -1;72 }73 ctx.beginPath();74 ctx.arc(a.x, a.y, 1.5, 0, Math.PI * 2);75 ctx.fillStyle = "rgba(244, 244, 246, 0.65)";76 ctx.fill();77 78 for (let j = i + 1; j < nodes.length; j++) {79 const b = nodes[j];80 const dist = Math.hypot(a.x - b.x, a.y - b.y);81 if (dist < linkDistance) {82 ctx.beginPath();83 ctx.moveTo(a.x, a.y);84 ctx.lineTo(b.x, b.y);85 ctx.strokeStyle = `rgba(228, 228, 231, ${(1 - dist / linkDistance) * 0.2})`;86 ctx.lineWidth = 0.75;87 ctx.stroke();88 }89 }90 91 if (pointerRef.current.x !== null && pointerRef.current.y !== null) {92 const pDist = Math.hypot(a.x - pointerRef.current.x, a.y - pointerRef.current.y);93 if (pDist < linkDistance * 1.3) {94 ctx.beginPath();95 ctx.moveTo(a.x, a.y);96 ctx.lineTo(pointerRef.current.x, pointerRef.current.y);97 ctx.strokeStyle = `rgba(244, 244, 246, ${(1 - pDist / (linkDistance * 1.3)) * 0.42})`;98 ctx.stroke();99 }100 }101 }102 103 if (!prefersReduced) {104 rafId = window.requestAnimationFrame(draw);105 }106 };107 108 draw();109 window.addEventListener("resize", resize);110 return () => {111 window.removeEventListener("resize", resize);112 window.cancelAnimationFrame(rafId);113 };114 }, [nodeCount, linkDistance, nodeSpeed]);115 116 return (117 <div118 onPointerMove={(e) => {119 const rect = e.currentTarget.getBoundingClientRect();120 pointerRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };121 }}122 onPointerLeave={() => {123 pointerRef.current = { x: null, y: null };124 }}125 className={`${styles.card} ${className}`.trim()}126 >127 <canvas ref={canvasRef} aria-hidden="true" className={styles.canvas} />128 <div className={styles.content}>129 <span className={styles.badge}>Constellation Network</span>130 <h3 className={styles.title}>{title}</h3>131 <p className={styles.description}>{description}</p>132 {children}133 </div>134 </div>135 );136}