{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "matrix-code", "title": "Matrix Code", "description": "Matrix code component with glitch effect and optional vignette effect. | グリッチ効果とオプションのビネット効果を備えたマトリックスコードコンポーネント。", "files": [ { "path": "components/background/matrix-code.tsx", "content": "\"use client\";\n\nimport { useRef, useEffect } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface MatrixCodeProps {\n glitchColors?: string[]; // グリッチの色\n glitchSpeed?: number; // グリッチの速度 (ms)\n centerVignette?: boolean; // 真ん中の暗いエフェクト\n outerVignette?: boolean; // 外側の暗いエフェクト\n smooth?: boolean; // スムーズなアニメーション\n className?: string; // コンテナの class name\n}\n\nexport default function MatrixCode({\n glitchColors = [\"#2b4539\", \"#61dca3\", \"#61b3dc\"],\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n className,\n}: MatrixCodeProps) {\n const canvasRef = useRef(null);\n const animationRef = useRef(null);\n const letters = useRef<\n {\n char: string;\n color: string;\n targetColor: string;\n colorProgress: number;\n }[]\n >([]);\n const grid = useRef({ columns: 0, rows: 0 });\n const context = useRef(null);\n const lastGlitchTime = useRef(Date.now());\n const pendingResizeRect = useRef<{ width: number; height: number } | null>(\n null,\n );\n\n const fontSize = 16;\n const charWidth = 10;\n const charHeight = 20;\n\n const lettersAndSymbols = [\n \"A\",\n \"B\",\n \"C\",\n \"D\",\n \"E\",\n \"F\",\n \"G\",\n \"H\",\n \"I\",\n \"J\",\n \"K\",\n \"L\",\n \"M\",\n \"N\",\n \"O\",\n \"P\",\n \"Q\",\n \"R\",\n \"S\",\n \"T\",\n \"U\",\n \"V\",\n \"W\",\n \"X\",\n \"Y\",\n \"Z\",\n \"!\",\n \"@\",\n \"#\",\n \"$\",\n \"&\",\n \"*\",\n \"(\",\n \")\",\n \"-\",\n \"_\",\n \"+\",\n \"=\",\n \"/\",\n \"[\",\n \"]\",\n \"{\",\n \"}\",\n \";\",\n \":\",\n \"<\",\n \">\",\n \",\",\n \"0\",\n \"1\",\n \"2\",\n \"3\",\n \"4\",\n \"5\",\n \"6\",\n \"7\",\n \"8\",\n \"9\",\n ];\n\n const getRandomChar = () => {\n return lettersAndSymbols[\n Math.floor(Math.random() * lettersAndSymbols.length)\n ];\n };\n\n const getRandomColor = () => {\n return glitchColors[Math.floor(Math.random() * glitchColors.length)];\n };\n\n const hexToRgb = (hex: string) => {\n const shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;\n hex = hex.replace(shorthandRegex, (m, r, g, b) => {\n return r + r + g + g + b + b;\n });\n\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result\n ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16),\n }\n : null;\n };\n\n const interpolateColor = (\n start: { r: number; g: number; b: number },\n end: { r: number; g: number; b: number },\n factor: number,\n ) => {\n const result = {\n r: Math.round(start.r + (end.r - start.r) * factor),\n g: Math.round(start.g + (end.g - start.g) * factor),\n b: Math.round(start.b + (end.b - start.b) * factor),\n };\n return `rgb(${result.r}, ${result.g}, ${result.b})`;\n };\n\n const calculateGrid = (width: number, height: number) => {\n const columns = Math.ceil(width / charWidth);\n const rows = Math.ceil(height / charHeight);\n return { columns, rows };\n };\n\n const initializeLetters = (columns: number, rows: number) => {\n grid.current = { columns, rows };\n const totalLetters = columns * rows;\n letters.current = Array.from({ length: totalLetters }, () => ({\n char: getRandomChar(),\n color: getRandomColor(),\n targetColor: getRandomColor(),\n colorProgress: 1,\n }));\n };\n\n const resizeCanvas = (rect?: { width: number; height: number }) => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const parent = canvas.parentElement;\n if (!parent) return;\n\n const dpr = window.devicePixelRatio || 1;\n const dimensions = rect ?? parent.getBoundingClientRect();\n\n canvas.width = dimensions.width * dpr;\n canvas.height = dimensions.height * dpr;\n\n canvas.style.width = `${dimensions.width}px`;\n canvas.style.height = `${dimensions.height}px`;\n\n if (context.current) {\n context.current.setTransform(dpr, 0, 0, dpr, 0, 0);\n }\n\n const { columns, rows } = calculateGrid(dimensions.width, dimensions.height);\n initializeLetters(columns, rows);\n drawLetters();\n };\n\n const drawLetters = () => {\n if (!context.current || letters.current.length === 0) return;\n const ctx = context.current;\n const { width, height } = canvasRef.current!.getBoundingClientRect();\n ctx.clearRect(0, 0, width, height);\n ctx.font = `${fontSize}px monospace`;\n ctx.textBaseline = \"top\";\n\n letters.current.forEach((letter, index) => {\n const x = (index % grid.current.columns) * charWidth;\n const y = Math.floor(index / grid.current.columns) * charHeight;\n ctx.fillStyle = letter.color;\n ctx.fillText(letter.char, x, y);\n });\n };\n\n const updateLetters = () => {\n if (!letters.current || letters.current.length === 0) return; // Prevent accessing empty array\n\n const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));\n\n for (let i = 0; i < updateCount; i++) {\n const index = Math.floor(Math.random() * letters.current.length);\n if (!letters.current[index]) continue; // Skip if index is invalid\n\n letters.current[index].char = getRandomChar();\n letters.current[index].targetColor = getRandomColor();\n\n if (!smooth) {\n letters.current[index].color = letters.current[index].targetColor;\n letters.current[index].colorProgress = 1;\n } else {\n letters.current[index].colorProgress = 0;\n }\n }\n };\n\n const handleSmoothTransitions = () => {\n let needsRedraw = false;\n letters.current.forEach((letter) => {\n if (letter.colorProgress < 1) {\n letter.colorProgress += 0.05;\n if (letter.colorProgress > 1) letter.colorProgress = 1;\n\n const startRgb = hexToRgb(letter.color);\n const endRgb = hexToRgb(letter.targetColor);\n if (startRgb && endRgb) {\n letter.color = interpolateColor(\n startRgb,\n endRgb,\n letter.colorProgress,\n );\n needsRedraw = true;\n }\n }\n });\n\n if (needsRedraw) {\n drawLetters();\n }\n };\n\n const animate = () => {\n const now = Date.now();\n if (now - lastGlitchTime.current >= glitchSpeed) {\n updateLetters();\n drawLetters();\n lastGlitchTime.current = now;\n }\n\n if (smooth) {\n handleSmoothTransitions();\n }\n\n animationRef.current = requestAnimationFrame(animate);\n };\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n context.current = canvas.getContext(\"2d\");\n resizeCanvas();\n animate();\n\n let resizeTimeout: NodeJS.Timeout;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current as number);\n const rect = pendingResizeRect.current;\n pendingResizeRect.current = null;\n resizeCanvas(rect ?? undefined);\n animate();\n }, 150);\n };\n\n window.addEventListener(\"resize\", handleResize);\n\n const parent = canvas.parentElement;\n const containerToObserve = parent?.parentElement ?? parent;\n const resizeObserver = containerToObserve\n ? new ResizeObserver((entries) => {\n const entry = entries[0];\n if (entry?.contentRect) {\n const { width, height } = entry.contentRect;\n if (width > 0 && height > 0) {\n pendingResizeRect.current = { width, height };\n handleResize();\n }\n }\n })\n : null;\n if (resizeObserver && containerToObserve) {\n resizeObserver.observe(containerToObserve);\n }\n\n return () => {\n clearTimeout(resizeTimeout);\n resizeObserver?.disconnect();\n cancelAnimationFrame(animationRef.current!);\n window.removeEventListener(\"resize\", handleResize);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [glitchSpeed, smooth]);\n\n return (\n \n \n {outerVignette && (\n
\n )}\n {centerVignette && (\n
\n )}\n \n );\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }