CampFireCritics/src/components/reactbits/TextAnimations/FuzzyText/FuzzyText.jsx
2025-05-21 11:05:20 +03:00

206 lines
5.9 KiB
JavaScript

/*
Installed from https://reactbits.dev/tailwind/
*/
import React, { useEffect, useRef } from "react";
const FuzzyText = ({
children,
fontSize = "clamp(2rem, 10vw, 10rem)",
fontWeight = 900,
fontFamily = "inherit",
color = "#fff",
enableHover = true,
baseIntensity = 0.18,
hoverIntensity = 0.5,
}) => {
const canvasRef = useRef(null);
useEffect(() => {
let animationFrameId;
let isCancelled = false;
const canvas = canvasRef.current;
if (!canvas) return;
const init = async () => {
if (document.fonts?.ready) {
await document.fonts.ready;
}
if (isCancelled) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const computedFontFamily =
fontFamily === "inherit"
? window.getComputedStyle(canvas).fontFamily || "sans-serif"
: fontFamily;
const fontSizeStr =
typeof fontSize === "number" ? `${fontSize}px` : fontSize;
let numericFontSize;
if (typeof fontSize === "number") {
numericFontSize = fontSize;
} else {
const temp = document.createElement("span");
temp.style.fontSize = fontSize;
document.body.appendChild(temp);
const computedSize = window.getComputedStyle(temp).fontSize;
numericFontSize = parseFloat(computedSize);
document.body.removeChild(temp);
}
const text = React.Children.toArray(children).join("");
// Create offscreen canvas
const offscreen = document.createElement("canvas");
const offCtx = offscreen.getContext("2d");
if (!offCtx) return;
offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;
offCtx.textBaseline = "alphabetic";
const metrics = offCtx.measureText(text);
const actualLeft = metrics.actualBoundingBoxLeft ?? 0;
const actualRight = metrics.actualBoundingBoxRight ?? metrics.width;
const actualAscent = metrics.actualBoundingBoxAscent ?? numericFontSize;
const actualDescent =
metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;
const textBoundingWidth = Math.ceil(actualLeft + actualRight);
const tightHeight = Math.ceil(actualAscent + actualDescent);
const extraWidthBuffer = 10;
const offscreenWidth = textBoundingWidth + extraWidthBuffer;
offscreen.width = offscreenWidth;
offscreen.height = tightHeight;
const xOffset = extraWidthBuffer / 2;
offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;
offCtx.textBaseline = "alphabetic";
offCtx.fillStyle = color;
offCtx.fillText(text, xOffset - actualLeft, actualAscent);
const horizontalMargin = 50;
const verticalMargin = 0;
canvas.width = offscreenWidth + horizontalMargin * 2;
canvas.height = tightHeight + verticalMargin * 2;
ctx.translate(horizontalMargin, verticalMargin);
const interactiveLeft = horizontalMargin + xOffset;
const interactiveTop = verticalMargin;
const interactiveRight = interactiveLeft + textBoundingWidth;
const interactiveBottom = interactiveTop + tightHeight;
let isHovering = false;
const fuzzRange = 30;
const run = () => {
if (isCancelled) return;
ctx.clearRect(
-fuzzRange,
-fuzzRange,
offscreenWidth + 2 * fuzzRange,
tightHeight + 2 * fuzzRange
);
const intensity = isHovering ? hoverIntensity : baseIntensity;
for (let j = 0; j < tightHeight; j++) {
const dx = Math.floor(intensity * (Math.random() - 0.5) * fuzzRange);
ctx.drawImage(
offscreen,
0,
j,
offscreenWidth,
1,
dx,
j,
offscreenWidth,
1
);
}
animationFrameId = window.requestAnimationFrame(run);
};
run();
const isInsideTextArea = (x, y) => {
return (
x >= interactiveLeft &&
x <= interactiveRight &&
y >= interactiveTop &&
y <= interactiveBottom
);
};
const handleMouseMove = (e) => {
if (!enableHover) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
isHovering = isInsideTextArea(x, y);
};
const handleMouseLeave = () => {
isHovering = false;
};
const handleTouchMove = (e) => {
if (!enableHover) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
isHovering = isInsideTextArea(x, y);
};
const handleTouchEnd = () => {
isHovering = false;
};
if (enableHover) {
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseleave", handleMouseLeave);
canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
canvas.addEventListener("touchend", handleTouchEnd);
}
const cleanup = () => {
window.cancelAnimationFrame(animationFrameId);
if (enableHover) {
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
canvas.removeEventListener("touchmove", handleTouchMove);
canvas.removeEventListener("touchend", handleTouchEnd);
}
};
canvas.cleanupFuzzyText = cleanup;
};
init();
return () => {
isCancelled = true;
window.cancelAnimationFrame(animationFrameId);
if (canvas && canvas.cleanupFuzzyText) {
canvas.cleanupFuzzyText();
}
};
}, [
children,
fontSize,
fontWeight,
fontFamily,
color,
enableHover,
baseIntensity,
hoverIntensity,
]);
return <canvas ref={canvasRef} />;
};
export default FuzzyText;