206 lines
5.9 KiB
JavaScript
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;
|