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; } });