diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts index 92df27973..19e1ea203 100644 --- a/apps/www/config/docs.ts +++ b/apps/www/config/docs.ts @@ -651,6 +651,12 @@ export const docsConfig: DocsConfig = { items: [], label: "New", }, + { + title: "Glyph Matrix", + href: `/docs/components/glyph-matrix`, + items: [], + label: "New", + }, ], }, ], diff --git a/apps/www/content/docs/components/glyph-matrix.mdx b/apps/www/content/docs/components/glyph-matrix.mdx new file mode 100644 index 000000000..761833a63 --- /dev/null +++ b/apps/www/content/docs/components/glyph-matrix.mdx @@ -0,0 +1,69 @@ +--- +title: Glyph Matrix +date: 2026-06-12 +description: An animated grid of subtly shifting glyphs on a canvas, with a theme-aware color driven by the consumer. +author: magicui +published: true +--- + + + +## Installation + + + + + CLI + Manual + + + +```bash +npx shadcn@latest add @magicui/glyph-matrix +``` + + + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + + + +## Usage + +```tsx showLineNumbers +import { GlyphMatrix } from "@/registry/magicui/glyph-matrix" +``` + +```tsx showLineNumbers +
+ +
+``` + +## Props + +| Prop | Type | Default | Description | +| -------------- | -------- | ---------------- | -------------------------------------------- | +| `glyphs` | `string` | `"01·•+*/\\<>="` | Characters to randomly pick from. | +| `cellSize` | `number` | `14` | Cell size in pixels and font size. | +| `mutationRate` | `number` | `0.04` | Probability a cell mutates each tick. | +| `interval` | `number` | `90` | Tick interval in milliseconds. | +| `className` | `string` | `-` | Classes applied to the canvas element. | +| `fadeBottom` | `number` | `0.6` | Fade strength toward the bottom of the grid. | +| `color` | `string` | `"#6B7280"` | Glyph color (any CSS color). | + +## Notes + +The component draws with the `color` prop, which accepts any CSS color (hex, `rgb()`, `hsl()`, `oklch()`, ...) and is normalized through canvas before drawing. For light/dark support, drive `color` from the consumer — e.g. with `next-themes` as shown in the demo above. diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index ec3bbf83f..c84bf080e 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -8829,6 +8829,212 @@ export default function GlobeDemo() { +===== COMPONENT: glyph-matrix ===== +Title: Glyph Matrix +Description: An animated grid of subtly shifting glyphs with fade effect and theme support. + +--- file: magicui/glyph-matrix.tsx --- +"use client" + +import { useEffect, useRef } from "react" + +import { cn } from "@/lib/utils" + +interface GlyphMatrixProps extends React.HTMLAttributes { + /** 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 + /** Fade out toward bottom (0 = no fade) */ + fadeBottom?: number + /** Glyph color (any CSS color). Pass a theme-aware value from the consumer. */ + color?: string +} + +/** + * GlyphMatrix — an animated grid of subtly shifting glyphs. + * Pass a `color` prop (e.g. driven by next-themes) to adapt it to + * light and dark modes. + */ +export function GlyphMatrix({ + glyphs = "01·•+*/\\<>=", + cellSize = 14, + mutationRate = 0.04, + interval = 90, + className, + fadeBottom = 0.6, + color = "#6B7280", + style, + ...props +}: GlyphMatrixProps) { + const canvasRef = useRef(null) + // Current glyph color as RGBA (a in 0-1). Kept in a ref so a color change + // (e.g. theme toggle) recolors the next frame without restarting the + // animation. Defaults to #6B7280. + const rgbaRef = useRef({ r: 107, g: 114, b: 128, a: 1 }) + + // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...). + useEffect(() => { + const probe = document.createElement("canvas") + probe.width = 1 + probe.height = 1 + const probeCtx = probe.getContext("2d") + if (!probeCtx) return + // Seed with the default so an invalid color falls back to it: the 2d + // context keeps the previous fillStyle when assigned an invalid value + // instead of silently turning black. + probeCtx.fillStyle = "#6B7280" + probeCtx.fillStyle = color + probeCtx.fillRect(0, 0, 1, 1) + const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data + rgbaRef.current = { r, g, b, a: a / 255 } + }, [color]) + + 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 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) + } + + const draw = () => { + const { clientWidth: w, clientHeight: h } = canvas + ctx.clearRect(0, 0, w, h) + + ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace` + ctx.textBaseline = "top" + + const { r, g, b, a: colorAlpha } = rgbaRef.current + 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 * colorAlpha + 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) + + return () => { + stopped = true + cancelAnimationFrame(raf) + ro.disconnect() + } + }, [glyphs, cellSize, mutationRate, interval, fadeBottom]) + + return ( +