Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/r/LetterGlitch-JS-CSS.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "LetterGlitch/LetterGlitch.jsx",
"content": "import { useRef, useEffect } from 'react';\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n className = '',\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}) => {\n const canvasRef = useRef(null);\n const animationRef = useRef(null);\n const letters = useRef([]);\n const grid = useRef({ columns: 0, rows: 0 });\n const context = useRef(null);\n const lastGlitchTime = useRef(Date.now());\n\n const lettersAndSymbols = Array.from(characters);\n\n const fontSize = 16;\n const charWidth = 10;\n const charHeight = 20;\n\n const getRandomChar = () => {\n return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];\n };\n\n const getRandomColor = () => {\n return glitchColors[Math.floor(Math.random() * glitchColors.length)];\n };\n\n const hexToRgb = hex => {\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 = (start, end, factor) => {\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, height) => {\n const columns = Math.ceil(width / charWidth);\n const rows = Math.ceil(height / charHeight);\n return { columns, rows };\n };\n\n const initializeLetters = (columns, rows) => {\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 = () => {\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 rect = parent.getBoundingClientRect();\n\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n\n if (context.current) {\n context.current.setTransform(dpr, 0, 0, dpr, 0, 0);\n }\n\n const { columns, rows } = calculateGrid(rect.width, rect.height);\n initializeLetters(columns, rows);\n\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;\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;\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(startRgb, endRgb, letter.colorProgress);\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;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current);\n resizeCanvas();\n animate();\n }, 100);\n };\n\n window.addEventListener('resize', handleResize);\n\n return () => {\n cancelAnimationFrame(animationRef.current);\n window.removeEventListener('resize', handleResize);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [glitchSpeed, smooth]);\n\n const containerStyle = {\n position: 'relative',\n width: '100%',\n height: '100%',\n backgroundColor: backgroundColor || (lightMode ? '#ffffff' : '#000000'),\n overflow: 'hidden'\n };\n\n const canvasStyle = {\n display: 'block',\n width: '100%',\n height: '100%'\n };\n\n const outerVignetteStyle = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n background: lightMode\n ? 'radial-gradient(circle, rgba(255,255,255,0) 58%, rgba(255,255,255,0.96) 100%)'\n : 'radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,1) 100%)'\n };\n\n const centerVignetteStyle = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n background: lightMode\n ? 'radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0) 60%)'\n : 'radial-gradient(circle, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%)'\n };\n\n return (\n <div style={containerStyle} className={className}>\n <canvas ref={canvasRef} style={canvasStyle} />\n {outerVignette && <div style={outerVignetteStyle}></div>}\n {centerVignette && <div style={centerVignetteStyle}></div>}\n </div>\n );\n};\n\nexport default LetterGlitch;\n"
"content": "import { useRef, useEffect } from 'react';\n\nconst FALLBACK_RGB = { r: 255, g: 255, b: 255 };\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n className = '',\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}) => {\n const canvasRef = useRef(null);\n const animationRef = useRef(null);\n const letters = useRef([]);\n const grid = useRef({ columns: 0, rows: 0 });\n const context = useRef(null);\n const lastGlitchTime = useRef(Date.now());\n\n const lettersAndSymbols = Array.from(characters);\n\n const fontSize = 16;\n const charWidth = 10;\n const charHeight = 20;\n\n const getRandomChar = () => {\n return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];\n };\n\n const getRandomColor = () => {\n return glitchColors[Math.floor(Math.random() * glitchColors.length)];\n };\n\n const hexToRgb = hex => {\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 // Interpolation happens in numbers, and the CSS string is only built at\n // paint time. Previously the formatted `rgb(...)` string was stored back\n // on the letter and fed to hexToRgb on the next frame, which returned null\n // and froze the transition after a single step.\n const mixRgb = (start, end, factor) => ({\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\n const rgbToCss = ({ r, g, b }) => `rgb(${r}, ${g}, ${b})`;\n\n // An unparseable entry in glitchColors must not stall the animation.\n const getRandomRgb = () => hexToRgb(getRandomColor()) || FALLBACK_RGB;\n\n const calculateGrid = (width, height) => {\n const columns = Math.ceil(width / charWidth);\n const rows = Math.ceil(height / charHeight);\n return { columns, rows };\n };\n\n const initializeLetters = (columns, rows) => {\n grid.current = { columns, rows };\n const totalLetters = columns * rows;\n letters.current = Array.from({ length: totalLetters }, () => {\n const rgb = getRandomRgb();\n return {\n char: getRandomChar(),\n rgb,\n fromRgb: rgb,\n targetRgb: getRandomRgb(),\n colorProgress: 1\n };\n });\n };\n\n const resizeCanvas = () => {\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 rect = parent.getBoundingClientRect();\n\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n\n if (context.current) {\n context.current.setTransform(dpr, 0, 0, dpr, 0, 0);\n }\n\n const { columns, rows } = calculateGrid(rect.width, rect.height);\n initializeLetters(columns, rows);\n\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 = rgbToCss(letter.rgb);\n ctx.fillText(letter.char, x, y);\n });\n };\n\n const updateLetters = () => {\n if (!letters.current || letters.current.length === 0) return;\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;\n\n letters.current[index].char = getRandomChar();\n // A new transition starts from the colour currently on screen, so a\n // letter picked again mid-fade continues instead of jumping.\n letters.current[index].fromRgb = letters.current[index].rgb;\n letters.current[index].targetRgb = getRandomRgb();\n\n if (!smooth) {\n letters.current[index].rgb = letters.current[index].targetRgb;\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 letter.rgb = mixRgb(letter.fromRgb, letter.targetRgb, letter.colorProgress);\n needsRedraw = true;\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;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current);\n resizeCanvas();\n animate();\n }, 100);\n };\n\n window.addEventListener('resize', handleResize);\n\n return () => {\n cancelAnimationFrame(animationRef.current);\n window.removeEventListener('resize', handleResize);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [glitchSpeed, smooth]);\n\n const containerStyle = {\n position: 'relative',\n width: '100%',\n height: '100%',\n backgroundColor: backgroundColor || (lightMode ? '#ffffff' : '#000000'),\n overflow: 'hidden'\n };\n\n const canvasStyle = {\n display: 'block',\n width: '100%',\n height: '100%'\n };\n\n const outerVignetteStyle = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n background: lightMode\n ? 'radial-gradient(circle, rgba(255,255,255,0) 58%, rgba(255,255,255,0.96) 100%)'\n : 'radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,1) 100%)'\n };\n\n const centerVignetteStyle = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n background: lightMode\n ? 'radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0) 60%)'\n : 'radial-gradient(circle, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%)'\n };\n\n return (\n <div style={containerStyle} className={className}>\n <canvas ref={canvasRef} style={canvasStyle} />\n {outerVignette && <div style={outerVignetteStyle}></div>}\n {centerVignette && <div style={centerVignetteStyle}></div>}\n </div>\n );\n};\n\nexport default LetterGlitch;\n"
}
],
"registryDependencies": [],
Expand Down
Loading