{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "star-background", "title": "Star Background", "description": "Canvas 2D starfield background. Configurable star count, twinkle speed, and color. Suited for hero sections or page backgrounds with optional overlay content. | Canvas 2D の星空背景。星の数・きらめきの速さ・色を設定可能。", "files": [ { "path": "components/background/star-background.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { CSSProperties } from \"react\";\nimport { useEffect, useRef } from \"react\";\n\nconst TWO_PI = 2 * Math.PI;\n\ninterface Star {\n x: number;\n y: number;\n size: number;\n baseOpacity: number;\n phase: number;\n}\n\nexport interface StarBackgroundProps {\n starCount?: number;\n twinkleSpeed?: number;\n starColor?: string;\n backgroundColor?: string;\n className?: string;\n containerClassName?: string;\n children?: React.ReactNode;\n style?: CSSProperties;\n}\n\nfunction initStars(width: number, height: number, count: number): Star[] {\n const stars: Star[] = [];\n for (let i = 0; i < count; i++) {\n stars.push({\n x: Math.random() * width,\n y: Math.random() * height,\n size: 0.1 + Math.random() * 1.5,\n baseOpacity: 0.3 + Math.random() * 0.7,\n phase: Math.random() * TWO_PI,\n });\n }\n return stars;\n}\n\nexport function StarBackground({\n starCount = 200,\n twinkleSpeed = 0.002,\n starColor = \"255, 255, 255\",\n backgroundColor = \"transparent\",\n className,\n containerClassName,\n children,\n style = {},\n}: StarBackgroundProps): React.ReactElement {\n const containerRef = useRef(null);\n const canvasRef = useRef(null);\n const ctxRef = useRef(null);\n const starsRef = useRef([]);\n const frameIdRef = useRef(null);\n const sizeRef = useRef({ width: 0, height: 0 });\n\n const configRef = useRef({\n starCount,\n twinkleSpeed,\n starColor,\n backgroundColor,\n });\n\n useEffect(() => {\n configRef.current = {\n starCount,\n twinkleSpeed,\n starColor,\n backgroundColor,\n };\n }, [starCount, twinkleSpeed, starColor, backgroundColor]);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n if (!canvas || !container) return;\n\n ctxRef.current = canvas.getContext(\"2d\");\n\n function setSize() {\n if (!container || !canvas) return;\n const rect = container.getBoundingClientRect();\n const width = Math.max(1, Math.floor(rect.width));\n const height = Math.max(1, Math.floor(rect.height));\n sizeRef.current = { width, height };\n canvas.width = width;\n canvas.height = height;\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n starsRef.current = initStars(width, height, configRef.current.starCount);\n }\n\n function tick(t: number) {\n const ctx = ctxRef.current;\n const stars = starsRef.current;\n const { width, height } = sizeRef.current;\n const {\n twinkleSpeed: speed,\n starColor: color,\n backgroundColor: bg,\n } = configRef.current;\n\n if (!ctx || !width || !height) {\n frameIdRef.current = requestAnimationFrame(tick);\n return;\n }\n\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, width, height);\n\n const rgb = color.replace(/\\s/g, \"\");\n for (let i = 0; i < stars.length; i++) {\n const star = stars[i];\n const opacity =\n speed === 0\n ? star.baseOpacity\n : star.baseOpacity *\n (0.4 + 0.6 * (Math.sin(star.phase + t * speed) * 0.5 + 0.5));\n ctx.fillStyle = `rgba(${rgb}, ${opacity})`;\n ctx.beginPath();\n ctx.arc(star.x, star.y, star.size, 0, TWO_PI);\n ctx.fill();\n }\n\n frameIdRef.current = requestAnimationFrame(tick);\n }\n\n setSize();\n frameIdRef.current = requestAnimationFrame(tick);\n\n const observer = new ResizeObserver(() => {\n setSize();\n });\n observer.observe(container);\n\n return () => {\n observer.disconnect();\n if (frameIdRef.current !== null) {\n cancelAnimationFrame(frameIdRef.current);\n }\n };\n }, []);\n\n return (\n \n \n {children != null ? (\n
{children}
\n ) : null}\n \n );\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }