diff --git a/public/r/LetterGlitch-JS-CSS.json b/public/r/LetterGlitch-JS-CSS.json
index 04cd8f4fa..71168498e 100644
--- a/public/r/LetterGlitch-JS-CSS.json
+++ b/public/r/LetterGlitch-JS-CSS.json
@@ -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
\n
\n {outerVignette &&
}\n {centerVignette &&
}\n
\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 \n
\n {outerVignette &&
}\n {centerVignette &&
}\n
\n );\n};\n\nexport default LetterGlitch;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LetterGlitch-JS-TW.json b/public/r/LetterGlitch-JS-TW.json
index e672d99ab..1e3c6c154 100644
--- a/public/r/LetterGlitch-JS-TW.json
+++ b/public/r/LetterGlitch-JS-TW.json
@@ -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 glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\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 return (\n \n
\n {outerVignette && (\n
\n )}\n {centerVignette && (\n
\n )}\n
\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 glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\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 return (\n \n
\n {outerVignette && (\n
\n )}\n {centerVignette && (\n
\n )}\n
\n );\n};\n\nexport default LetterGlitch;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LetterGlitch-TS-CSS.json b/public/r/LetterGlitch-TS-CSS.json
index bd2452205..b1e8956ee 100644
--- a/public/r/LetterGlitch-TS-CSS.json
+++ b/public/r/LetterGlitch-TS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "LetterGlitch/LetterGlitch.tsx",
- "content": "import { useRef, useEffect } from 'react';\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}: {\n glitchColors: string[];\n glitchSpeed: number;\n centerVignette: boolean;\n outerVignette: boolean;\n smooth: boolean;\n lightMode?: boolean;\n backgroundColor?: string;\n className?: string;\n characters: string;\n}) => {\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\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: 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 = () => {\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 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: ReturnType;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current as number);\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 \n
\n {outerVignette &&
}\n {centerVignette &&
}\n
\n );\n};\n\nexport default LetterGlitch;\n"
+ "content": "import { useRef, useEffect } from 'react';\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nconst FALLBACK_RGB: Rgb = { r: 255, g: 255, b: 255 };\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}: {\n glitchColors: string[];\n glitchSpeed: number;\n centerVignette: boolean;\n outerVignette: boolean;\n smooth: boolean;\n lightMode?: boolean;\n backgroundColor?: string;\n className?: string;\n characters: string;\n}) => {\n const canvasRef = useRef(null);\n const animationRef = useRef(null);\n const letters = useRef<\n {\n char: string;\n rgb: Rgb;\n fromRgb: Rgb;\n targetRgb: Rgb;\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\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: 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 // 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: Rgb, end: Rgb, factor: number): Rgb => ({\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) => `rgb(${r}, ${g}, ${b})`;\n\n // An unparseable entry in glitchColors must not stall the animation.\n const getRandomRgb = (): Rgb => hexToRgb(getRandomColor()) || FALLBACK_RGB;\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 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 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: ReturnType;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current as number);\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 \n
\n {outerVignette &&
}\n {centerVignette &&
}\n
\n );\n};\n\nexport default LetterGlitch;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/LetterGlitch-TS-TW.json b/public/r/LetterGlitch-TS-TW.json
index d620871a3..dfcee2e34 100644
--- a/public/r/LetterGlitch-TS-TW.json
+++ b/public/r/LetterGlitch-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "LetterGlitch/LetterGlitch.tsx",
- "content": "import { useRef, useEffect } from 'react';\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}: {\n glitchColors: string[];\n glitchSpeed: number;\n centerVignette: boolean;\n outerVignette: boolean;\n smooth: boolean;\n lightMode?: boolean;\n backgroundColor?: string;\n className?: string;\n characters: string;\n}) => {\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\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: 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 = () => {\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 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: ReturnType;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current as number);\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 return (\n \n
\n {outerVignette && (\n
\n )}\n {centerVignette && (\n
\n )}\n
\n );\n};\n\nexport default LetterGlitch;\n"
+ "content": "import { useRef, useEffect } from 'react';\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nconst FALLBACK_RGB: Rgb = { r: 255, g: 255, b: 255 };\n\nconst LetterGlitch = ({\n glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],\n glitchSpeed = 50,\n centerVignette = false,\n outerVignette = true,\n smooth = true,\n lightMode = false,\n backgroundColor,\n className = '',\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'\n}: {\n glitchColors: string[];\n glitchSpeed: number;\n centerVignette: boolean;\n outerVignette: boolean;\n smooth: boolean;\n lightMode?: boolean;\n backgroundColor?: string;\n className?: string;\n characters: string;\n}) => {\n const canvasRef = useRef(null);\n const animationRef = useRef(null);\n const letters = useRef<\n {\n char: string;\n rgb: Rgb;\n fromRgb: Rgb;\n targetRgb: Rgb;\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\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: 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 // 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: Rgb, end: Rgb, factor: number): Rgb => ({\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) => `rgb(${r}, ${g}, ${b})`;\n\n // An unparseable entry in glitchColors must not stall the animation.\n const getRandomRgb = (): Rgb => hexToRgb(getRandomColor()) || FALLBACK_RGB;\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 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 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: ReturnType;\n\n const handleResize = () => {\n clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n cancelAnimationFrame(animationRef.current as number);\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 return (\n \n
\n {outerVignette && (\n
\n )}\n {centerVignette && (\n
\n )}\n
\n );\n};\n\nexport default LetterGlitch;\n"
}
],
"registryDependencies": [],
diff --git a/src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx b/src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx
index 64c2f8d35..e18cf3d21 100644
--- a/src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx
+++ b/src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx
@@ -1,5 +1,7 @@
import { useRef, useEffect } from 'react';
+const FALLBACK_RGB = { r: 255, g: 255, b: 255 };
+
const LetterGlitch = ({
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
className = '',
@@ -48,14 +50,20 @@ const LetterGlitch = ({
: null;
};
- const interpolateColor = (start, end, factor) => {
- const result = {
- r: Math.round(start.r + (end.r - start.r) * factor),
- g: Math.round(start.g + (end.g - start.g) * factor),
- b: Math.round(start.b + (end.b - start.b) * factor)
- };
- return `rgb(${result.r}, ${result.g}, ${result.b})`;
- };
+ // Interpolation happens in numbers, and the CSS string is only built at
+ // paint time. Previously the formatted `rgb(...)` string was stored back
+ // on the letter and fed to hexToRgb on the next frame, which returned null
+ // and froze the transition after a single step.
+ const mixRgb = (start, end, factor) => ({
+ r: Math.round(start.r + (end.r - start.r) * factor),
+ g: Math.round(start.g + (end.g - start.g) * factor),
+ b: Math.round(start.b + (end.b - start.b) * factor)
+ });
+
+ const rgbToCss = ({ r, g, b }) => `rgb(${r}, ${g}, ${b})`;
+
+ // An unparseable entry in glitchColors must not stall the animation.
+ const getRandomRgb = () => hexToRgb(getRandomColor()) || FALLBACK_RGB;
const calculateGrid = (width, height) => {
const columns = Math.ceil(width / charWidth);
@@ -66,12 +74,16 @@ const LetterGlitch = ({
const initializeLetters = (columns, rows) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
- letters.current = Array.from({ length: totalLetters }, () => ({
- char: getRandomChar(),
- color: getRandomColor(),
- targetColor: getRandomColor(),
- colorProgress: 1
- }));
+ letters.current = Array.from({ length: totalLetters }, () => {
+ const rgb = getRandomRgb();
+ return {
+ char: getRandomChar(),
+ rgb,
+ fromRgb: rgb,
+ targetRgb: getRandomRgb(),
+ colorProgress: 1
+ };
+ });
};
const resizeCanvas = () => {
@@ -110,7 +122,7 @@ const LetterGlitch = ({
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
- ctx.fillStyle = letter.color;
+ ctx.fillStyle = rgbToCss(letter.rgb);
ctx.fillText(letter.char, x, y);
});
};
@@ -125,10 +137,13 @@ const LetterGlitch = ({
if (!letters.current[index]) continue;
letters.current[index].char = getRandomChar();
- letters.current[index].targetColor = getRandomColor();
+ // A new transition starts from the colour currently on screen, so a
+ // letter picked again mid-fade continues instead of jumping.
+ letters.current[index].fromRgb = letters.current[index].rgb;
+ letters.current[index].targetRgb = getRandomRgb();
if (!smooth) {
- letters.current[index].color = letters.current[index].targetColor;
+ letters.current[index].rgb = letters.current[index].targetRgb;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
@@ -143,12 +158,8 @@ const LetterGlitch = ({
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
- const startRgb = hexToRgb(letter.color);
- const endRgb = hexToRgb(letter.targetColor);
- if (startRgb && endRgb) {
- letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
- needsRedraw = true;
- }
+ letter.rgb = mixRgb(letter.fromRgb, letter.targetRgb, letter.colorProgress);
+ needsRedraw = true;
}
});
diff --git a/src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx b/src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx
index a7277d448..d46c54252 100644
--- a/src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx
+++ b/src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx
@@ -1,5 +1,7 @@
import { useRef, useEffect } from 'react';
+const FALLBACK_RGB = { r: 255, g: 255, b: 255 };
+
const LetterGlitch = ({
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
glitchSpeed = 50,
@@ -48,14 +50,20 @@ const LetterGlitch = ({
: null;
};
- const interpolateColor = (start, end, factor) => {
- const result = {
- r: Math.round(start.r + (end.r - start.r) * factor),
- g: Math.round(start.g + (end.g - start.g) * factor),
- b: Math.round(start.b + (end.b - start.b) * factor)
- };
- return `rgb(${result.r}, ${result.g}, ${result.b})`;
- };
+ // Interpolation happens in numbers, and the CSS string is only built at
+ // paint time. Previously the formatted `rgb(...)` string was stored back
+ // on the letter and fed to hexToRgb on the next frame, which returned null
+ // and froze the transition after a single step.
+ const mixRgb = (start, end, factor) => ({
+ r: Math.round(start.r + (end.r - start.r) * factor),
+ g: Math.round(start.g + (end.g - start.g) * factor),
+ b: Math.round(start.b + (end.b - start.b) * factor)
+ });
+
+ const rgbToCss = ({ r, g, b }) => `rgb(${r}, ${g}, ${b})`;
+
+ // An unparseable entry in glitchColors must not stall the animation.
+ const getRandomRgb = () => hexToRgb(getRandomColor()) || FALLBACK_RGB;
const calculateGrid = (width, height) => {
const columns = Math.ceil(width / charWidth);
@@ -66,12 +74,16 @@ const LetterGlitch = ({
const initializeLetters = (columns, rows) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
- letters.current = Array.from({ length: totalLetters }, () => ({
- char: getRandomChar(),
- color: getRandomColor(),
- targetColor: getRandomColor(),
- colorProgress: 1
- }));
+ letters.current = Array.from({ length: totalLetters }, () => {
+ const rgb = getRandomRgb();
+ return {
+ char: getRandomChar(),
+ rgb,
+ fromRgb: rgb,
+ targetRgb: getRandomRgb(),
+ colorProgress: 1
+ };
+ });
};
const resizeCanvas = () => {
@@ -110,7 +122,7 @@ const LetterGlitch = ({
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
- ctx.fillStyle = letter.color;
+ ctx.fillStyle = rgbToCss(letter.rgb);
ctx.fillText(letter.char, x, y);
});
};
@@ -125,10 +137,13 @@ const LetterGlitch = ({
if (!letters.current[index]) continue;
letters.current[index].char = getRandomChar();
- letters.current[index].targetColor = getRandomColor();
+ // A new transition starts from the colour currently on screen, so a
+ // letter picked again mid-fade continues instead of jumping.
+ letters.current[index].fromRgb = letters.current[index].rgb;
+ letters.current[index].targetRgb = getRandomRgb();
if (!smooth) {
- letters.current[index].color = letters.current[index].targetColor;
+ letters.current[index].rgb = letters.current[index].targetRgb;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
@@ -143,12 +158,8 @@ const LetterGlitch = ({
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
- const startRgb = hexToRgb(letter.color);
- const endRgb = hexToRgb(letter.targetColor);
- if (startRgb && endRgb) {
- letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
- needsRedraw = true;
- }
+ letter.rgb = mixRgb(letter.fromRgb, letter.targetRgb, letter.colorProgress);
+ needsRedraw = true;
}
});
diff --git a/src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx b/src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx
index 9ebc89234..3974b1363 100644
--- a/src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx
+++ b/src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx
@@ -1,5 +1,13 @@
import { useRef, useEffect } from 'react';
+interface Rgb {
+ r: number;
+ g: number;
+ b: number;
+}
+
+const FALLBACK_RGB: Rgb = { r: 255, g: 255, b: 255 };
+
const LetterGlitch = ({
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
glitchSpeed = 50,
@@ -26,8 +34,9 @@ const LetterGlitch = ({
const letters = useRef<
{
char: string;
- color: string;
- targetColor: string;
+ rgb: Rgb;
+ fromRgb: Rgb;
+ targetRgb: Rgb;
colorProgress: number;
}[]
>([]);
@@ -65,18 +74,20 @@ const LetterGlitch = ({
: null;
};
- const interpolateColor = (
- start: { r: number; g: number; b: number },
- end: { r: number; g: number; b: number },
- factor: number
- ) => {
- const result = {
- r: Math.round(start.r + (end.r - start.r) * factor),
- g: Math.round(start.g + (end.g - start.g) * factor),
- b: Math.round(start.b + (end.b - start.b) * factor)
- };
- return `rgb(${result.r}, ${result.g}, ${result.b})`;
- };
+ // Interpolation happens in numbers, and the CSS string is only built at
+ // paint time. Previously the formatted `rgb(...)` string was stored back
+ // on the letter and fed to hexToRgb on the next frame, which returned null
+ // and froze the transition after a single step.
+ const mixRgb = (start: Rgb, end: Rgb, factor: number): Rgb => ({
+ r: Math.round(start.r + (end.r - start.r) * factor),
+ g: Math.round(start.g + (end.g - start.g) * factor),
+ b: Math.round(start.b + (end.b - start.b) * factor)
+ });
+
+ const rgbToCss = ({ r, g, b }: Rgb) => `rgb(${r}, ${g}, ${b})`;
+
+ // An unparseable entry in glitchColors must not stall the animation.
+ const getRandomRgb = (): Rgb => hexToRgb(getRandomColor()) || FALLBACK_RGB;
const calculateGrid = (width: number, height: number) => {
const columns = Math.ceil(width / charWidth);
@@ -87,12 +98,16 @@ const LetterGlitch = ({
const initializeLetters = (columns: number, rows: number) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
- letters.current = Array.from({ length: totalLetters }, () => ({
- char: getRandomChar(),
- color: getRandomColor(),
- targetColor: getRandomColor(),
- colorProgress: 1
- }));
+ letters.current = Array.from({ length: totalLetters }, () => {
+ const rgb = getRandomRgb();
+ return {
+ char: getRandomChar(),
+ rgb,
+ fromRgb: rgb,
+ targetRgb: getRandomRgb(),
+ colorProgress: 1
+ };
+ });
};
const resizeCanvas = () => {
@@ -130,7 +145,7 @@ const LetterGlitch = ({
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
- ctx.fillStyle = letter.color;
+ ctx.fillStyle = rgbToCss(letter.rgb);
ctx.fillText(letter.char, x, y);
});
};
@@ -145,10 +160,13 @@ const LetterGlitch = ({
if (!letters.current[index]) continue;
letters.current[index].char = getRandomChar();
- letters.current[index].targetColor = getRandomColor();
+ // A new transition starts from the colour currently on screen, so a
+ // letter picked again mid-fade continues instead of jumping.
+ letters.current[index].fromRgb = letters.current[index].rgb;
+ letters.current[index].targetRgb = getRandomRgb();
if (!smooth) {
- letters.current[index].color = letters.current[index].targetColor;
+ letters.current[index].rgb = letters.current[index].targetRgb;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
@@ -163,12 +181,8 @@ const LetterGlitch = ({
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
- const startRgb = hexToRgb(letter.color);
- const endRgb = hexToRgb(letter.targetColor);
- if (startRgb && endRgb) {
- letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
- needsRedraw = true;
- }
+ letter.rgb = mixRgb(letter.fromRgb, letter.targetRgb, letter.colorProgress);
+ needsRedraw = true;
}
});
diff --git a/src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx b/src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx
index f9cb8f375..41fb5c0b7 100644
--- a/src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx
+++ b/src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx
@@ -1,5 +1,13 @@
import { useRef, useEffect } from 'react';
+interface Rgb {
+ r: number;
+ g: number;
+ b: number;
+}
+
+const FALLBACK_RGB: Rgb = { r: 255, g: 255, b: 255 };
+
const LetterGlitch = ({
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
glitchSpeed = 50,
@@ -26,8 +34,9 @@ const LetterGlitch = ({
const letters = useRef<
{
char: string;
- color: string;
- targetColor: string;
+ rgb: Rgb;
+ fromRgb: Rgb;
+ targetRgb: Rgb;
colorProgress: number;
}[]
>([]);
@@ -65,18 +74,20 @@ const LetterGlitch = ({
: null;
};
- const interpolateColor = (
- start: { r: number; g: number; b: number },
- end: { r: number; g: number; b: number },
- factor: number
- ) => {
- const result = {
- r: Math.round(start.r + (end.r - start.r) * factor),
- g: Math.round(start.g + (end.g - start.g) * factor),
- b: Math.round(start.b + (end.b - start.b) * factor)
- };
- return `rgb(${result.r}, ${result.g}, ${result.b})`;
- };
+ // Interpolation happens in numbers, and the CSS string is only built at
+ // paint time. Previously the formatted `rgb(...)` string was stored back
+ // on the letter and fed to hexToRgb on the next frame, which returned null
+ // and froze the transition after a single step.
+ const mixRgb = (start: Rgb, end: Rgb, factor: number): Rgb => ({
+ r: Math.round(start.r + (end.r - start.r) * factor),
+ g: Math.round(start.g + (end.g - start.g) * factor),
+ b: Math.round(start.b + (end.b - start.b) * factor)
+ });
+
+ const rgbToCss = ({ r, g, b }: Rgb) => `rgb(${r}, ${g}, ${b})`;
+
+ // An unparseable entry in glitchColors must not stall the animation.
+ const getRandomRgb = (): Rgb => hexToRgb(getRandomColor()) || FALLBACK_RGB;
const calculateGrid = (width: number, height: number) => {
const columns = Math.ceil(width / charWidth);
@@ -87,12 +98,16 @@ const LetterGlitch = ({
const initializeLetters = (columns: number, rows: number) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
- letters.current = Array.from({ length: totalLetters }, () => ({
- char: getRandomChar(),
- color: getRandomColor(),
- targetColor: getRandomColor(),
- colorProgress: 1
- }));
+ letters.current = Array.from({ length: totalLetters }, () => {
+ const rgb = getRandomRgb();
+ return {
+ char: getRandomChar(),
+ rgb,
+ fromRgb: rgb,
+ targetRgb: getRandomRgb(),
+ colorProgress: 1
+ };
+ });
};
const resizeCanvas = () => {
@@ -130,7 +145,7 @@ const LetterGlitch = ({
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
- ctx.fillStyle = letter.color;
+ ctx.fillStyle = rgbToCss(letter.rgb);
ctx.fillText(letter.char, x, y);
});
};
@@ -145,10 +160,13 @@ const LetterGlitch = ({
if (!letters.current[index]) continue;
letters.current[index].char = getRandomChar();
- letters.current[index].targetColor = getRandomColor();
+ // A new transition starts from the colour currently on screen, so a
+ // letter picked again mid-fade continues instead of jumping.
+ letters.current[index].fromRgb = letters.current[index].rgb;
+ letters.current[index].targetRgb = getRandomRgb();
if (!smooth) {
- letters.current[index].color = letters.current[index].targetColor;
+ letters.current[index].rgb = letters.current[index].targetRgb;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
@@ -163,12 +181,8 @@ const LetterGlitch = ({
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
- const startRgb = hexToRgb(letter.color);
- const endRgb = hexToRgb(letter.targetColor);
- if (startRgb && endRgb) {
- letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
- needsRedraw = true;
- }
+ letter.rgb = mixRgb(letter.fromRgb, letter.targetRgb, letter.colorProgress);
+ needsRedraw = true;
}
});