Describe the issue
I was using LetterGlitch as a page background and noticed that turning smooth on and off barely changed anything. Turns out the colour fade does start, but it stops on the very first frame — a letter ends up about 5% of the way to its target colour and stays there until the next glitch tick swaps it out for something else.
So smooth={true} is more or less inert right now.
Root cause
In handleSmoothTransitions:
const startRgb = hexToRgb(letter.color);
const endRgb = hexToRgb(letter.targetColor);
if (startRgb && endRgb) {
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
needsRedraw = true;
}
letter.color starts out as a hex string from getRandomColor(), but the line inside the if overwrites it with interpolateColor(...), which returns rgb(r, g, b). On the next frame hexToRgb("rgb(46, 77, 62)") matches neither the shorthand nor the 6-digit regex, so it returns null, the if never runs again, and the colour is stuck. colorProgress still climbs all the way to 1, and needsRedraw stays false.
targetColor isn't affected, since it's always reassigned straight from getRandomColor() and stays hex. It's only the start of the interpolation that gets poisoned.
Same code in all four variants:
src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx:146
src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx:146
src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx:166
src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx:166
I think this is easy to miss because the canvas still animates perfectly well — updateLetters() swaps characters every glitchSpeed ms and calls drawLetters() regardless of any of this. The only thing missing is the fade between colours, which is subtle enough that it just reads as "smooth doesn't do much".
Reproduction Link
https://reactbits.dev/backgrounds/letter-glitch
Steps to reproduce
The two functions don't touch the DOM, so you can watch this happen in plain Node without React or a browser. Both are copied verbatim from the component:
const hexToRgb = hex => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (_m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }
: 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})`;
};
// one letter, stepped through handleSmoothTransitions frame by frame
const letter = { color: '#2b4539', targetColor: '#61dca3', colorProgress: 0 };
let redraws = 0;
for (let frame = 1; frame <= 20; frame++) {
if (letter.colorProgress < 1) {
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);
redraws++;
}
console.log(
`frame ${String(frame).padStart(2)} progress ${letter.colorProgress.toFixed(2)}` +
` color ${letter.color.padEnd(18)} ${startRgb ? 'ok' : 'hexToRgb(letter.color) -> null'}`
);
}
}
console.log(`\nfinal colour: ${letter.color}`);
console.log(`expected: rgb(97, 220, 163) (= #61dca3)`);
console.log(`frames redrawn: ${redraws} of 20`);
Using the component's own default colours (#2b4539 → #61dca3):
frame 1 progress 0.05 color rgb(46, 77, 62) ok
frame 2 progress 0.10 color rgb(46, 77, 62) hexToRgb(letter.color) -> null
frame 3 progress 0.15 color rgb(46, 77, 62) hexToRgb(letter.color) -> null
...
frame 20 progress 1.00 color rgb(46, 77, 62) hexToRgb(letter.color) -> null
final colour: rgb(46, 77, 62)
expected: rgb(97, 220, 163) (= #61dca3)
frames redrawn: 1 of 20
One frame out of twenty actually paints.
Possible fix
Keeping the two interpolation endpoints in hex and letting letter.color be render-only fixes it, since the parser is then never handed its own output:
letters.current = Array.from({ length: totalLetters }, () => ({
char: getRandomChar(),
color: getRandomColor(),
+ startColor: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1
}));
letters.current[index].char = getRandomChar();
+ letters.current[index].startColor = letters.current[index].targetColor;
letters.current[index].targetColor = getRandomColor();
- const startRgb = hexToRgb(letter.color);
+ const startRgb = hexToRgb(letter.startColor);
const endRgb = hexToRgb(letter.targetColor);
The other way round would be to have interpolateColor return {r, g, b} and only format it into a string at the ctx.fillStyle assignment — slightly bigger diff, but no extra field. Happy to open a PR with whichever you'd rather have.
Validations
Describe the issue
I was using LetterGlitch as a page background and noticed that turning
smoothon and off barely changed anything. Turns out the colour fade does start, but it stops on the very first frame — a letter ends up about 5% of the way to its target colour and stays there until the next glitch tick swaps it out for something else.So
smooth={true}is more or less inert right now.Root cause
In
handleSmoothTransitions:letter.colorstarts out as a hex string fromgetRandomColor(), but the line inside theifoverwrites it withinterpolateColor(...), which returnsrgb(r, g, b). On the next framehexToRgb("rgb(46, 77, 62)")matches neither the shorthand nor the 6-digit regex, so it returnsnull, theifnever runs again, and the colour is stuck.colorProgressstill climbs all the way to 1, andneedsRedrawstaysfalse.targetColorisn't affected, since it's always reassigned straight fromgetRandomColor()and stays hex. It's only the start of the interpolation that gets poisoned.Same code in all four variants:
src/content/Backgrounds/LetterGlitch/LetterGlitch.jsx:146src/tailwind/Backgrounds/LetterGlitch/LetterGlitch.jsx:146src/ts-default/Backgrounds/LetterGlitch/LetterGlitch.tsx:166src/ts-tailwind/Backgrounds/LetterGlitch/LetterGlitch.tsx:166I think this is easy to miss because the canvas still animates perfectly well —
updateLetters()swaps characters everyglitchSpeedms and callsdrawLetters()regardless of any of this. The only thing missing is the fade between colours, which is subtle enough that it just reads as "smooth doesn't do much".Reproduction Link
https://reactbits.dev/backgrounds/letter-glitch
Steps to reproduce
The two functions don't touch the DOM, so you can watch this happen in plain Node without React or a browser. Both are copied verbatim from the component:
Using the component's own default colours (
#2b4539→#61dca3):One frame out of twenty actually paints.
Possible fix
Keeping the two interpolation endpoints in hex and letting
letter.colorbe render-only fixes it, since the parser is then never handed its own output:letters.current = Array.from({ length: totalLetters }, () => ({ char: getRandomChar(), color: getRandomColor(), + startColor: getRandomColor(), targetColor: getRandomColor(), colorProgress: 1 }));letters.current[index].char = getRandomChar(); + letters.current[index].startColor = letters.current[index].targetColor; letters.current[index].targetColor = getRandomColor();The other way round would be to have
interpolateColorreturn{r, g, b}and only format it into a string at thectx.fillStyleassignment — slightly bigger diff, but no extra field. Happy to open a PR with whichever you'd rather have.Validations