From 1d87fa0d837749f739925b4f815e7be5451ca2b0 Mon Sep 17 00:00:00 2001 From: Nagarampalli Sarvan Kumar Date: Tue, 19 May 2026 16:25:27 +0530 Subject: [PATCH 1/2] feat/added glyph matrix component --- .../registry/example/glyph-matrix-demo.tsx | 15 ++ apps/www/registry/magicui/glyph-matrix.tsx | 169 ++++++++++++++++++ apps/www/registry/registry-examples.ts | 13 ++ apps/www/registry/registry-ui.ts | 13 ++ registry.json | 13 ++ 5 files changed, 223 insertions(+) create mode 100644 apps/www/registry/example/glyph-matrix-demo.tsx create mode 100644 apps/www/registry/magicui/glyph-matrix.tsx diff --git a/apps/www/registry/example/glyph-matrix-demo.tsx b/apps/www/registry/example/glyph-matrix-demo.tsx new file mode 100644 index 000000000..3748bd330 --- /dev/null +++ b/apps/www/registry/example/glyph-matrix-demo.tsx @@ -0,0 +1,15 @@ +import { GlyphMatrix } from "@/registry/magicui/glyph-matrix" + +export default function GlyphMatrixDemo() { + return ( +
+ +
+ ) +} diff --git a/apps/www/registry/magicui/glyph-matrix.tsx b/apps/www/registry/magicui/glyph-matrix.tsx new file mode 100644 index 000000000..bd5685b09 --- /dev/null +++ b/apps/www/registry/magicui/glyph-matrix.tsx @@ -0,0 +1,169 @@ +"use client"; +import { useEffect, useRef } from "react"; + +interface GlyphMatrixProps { + /** Characters to randomly pick from */ + glyphs?: string; + /** Cell size in px (also font size) */ + cellSize?: number; + /** Probability (0-1) a cell mutates each tick */ + mutationRate?: number; + /** Tick interval in ms */ + interval?: number; + /** Optional className for the wrapping canvas */ + className?: string; + /** Fade out toward bottom (0 = no fade) */ + fadeBottom?: number; +} + +/** + * GlyphMatrix — an animated grid of subtly shifting glyphs. + * Uses semantic tokens (--foreground / --background) so it adapts to + * both light and dark modes automatically. + */ +export function GlyphMatrix({ + glyphs = "01·•+*/\\<>=", + cellSize = 14, + mutationRate = 0.04, + interval = 90, + className, + fadeBottom = 0.6, +}: GlyphMatrixProps) { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let cols = 0; + let rows = 0; + let cells: string[] = []; + let alphas: number[] = []; + let raf = 0; + let last = 0; + let stopped = false; + + const readColor = () => { + const styles = getComputedStyle(canvas); + // Resolve --foreground via a temp element so oklch() is converted to rgb + const probe = document.createElement("span"); + probe.style.color = "var(--foreground)"; + probe.style.display = "none"; + canvas.parentElement?.appendChild(probe); + const color = getComputedStyle(probe).color || styles.color; + probe.remove(); + return color; + }; + + let fgColor = readColor(); + + const resize = () => { + const dpr = window.devicePixelRatio || 1; + const { clientWidth: w, clientHeight: h } = canvas; + + canvas.width = w * dpr; + canvas.height = h * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + cols = Math.ceil(w / cellSize); + rows = Math.ceil(h / cellSize); + + cells = new Array(cols * rows) + .fill(0) + .map(() => glyphs[Math.floor(Math.random() * glyphs.length)]); + alphas = new Array(cols * rows) + .fill(0) + .map(() => 0.05 + Math.random() * 0.35); + + fgColor = readColor(); + }; + + const parseRgb = (c: string) => { + const m = c.match(/rgba?\(([^)]+)\)/); + if (!m) return { r: 0, g: 0, b: 0 }; + const [r, g, b] = m[1].split(",").map((v) => parseFloat(v)); + return { r, g, b }; + }; + + const draw = () => { + const { clientWidth: w, clientHeight: h } = canvas; + ctx.clearRect(0, 0, w, h); + + const { r, g, b } = parseRgb(fgColor); + ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`; + ctx.textBaseline = "top"; + + for (let y = 0; y < rows; y++) { + const fade = + fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1; + for (let x = 0; x < cols; x++) { + const i = y * cols + x; + const a = alphas[i] * fade; + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; + ctx.fillText(cells[i], x * cellSize, y * cellSize); + } + } + }; + + const tick = (t: number) => { + if (stopped) return; + + if (t - last >= interval) { + last = t; + + const total = cols * rows; + const mutations = Math.max(1, Math.floor(total * mutationRate)); + + for (let n = 0; n < mutations; n++) { + const i = Math.floor(Math.random() * total); + cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)]; + alphas[i] = 0.05 + Math.random() * 0.45; + } + + draw(); + } + + raf = requestAnimationFrame(tick); + }; + + resize(); + draw(); + raf = requestAnimationFrame(tick); + + const ro = new ResizeObserver(() => { + resize(); + draw(); + }); + ro.observe(canvas); + + // Re-read color when theme changes (class on ) + const mo = new MutationObserver(() => { + fgColor = readColor(); + }); + mo.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme"], + }); + + return () => { + stopped = true; + cancelAnimationFrame(raf); + ro.disconnect(); + mo.disconnect(); + }; + }, [glyphs, cellSize, mutationRate, interval, fadeBottom]); + + return ( +