Skip to content

Commit 983e7e3

Browse files
committed
FEAT: 🎉 Add New <FuzzyText /> text animation
1 parent 60b13af commit 983e7e3

File tree

9 files changed

+889
-1
lines changed

9 files changed

+889
-1
lines changed

index.html

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
<link rel="preconnect" href="https://fonts.googleapis.com">
2626
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2727
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
28+
<link rel="preconnect" href="https://fonts.googleapis.com">
29+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
30+
<link href="https://fonts.googleapis.com/css2?family=Gochi+Hand&display=swap" rel="stylesheet">
2831

2932
<!-- Icons -->
3033
<link rel="icon" type="image/svg+xml" sizes="16x16 32x32" href="favicon.ico" />

src/constants/Categories.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Highlighted sidebar items
2-
export const NEW = ['Glitch Text', 'Circular Text', 'Glass Icons', 'Scroll Reveal', 'Scroll Float', 'Lanyard', 'Particles', 'Scroll Velocity', 'Counter', 'Balatro'];
2+
export const NEW = ['Glitch Text', 'Fuzzy Text', 'Circular Text', 'Glass Icons', 'Scroll Reveal', 'Scroll Float', 'Lanyard', 'Particles', 'Scroll Velocity', 'Counter', 'Balatro'];
33
export const UPDATED = [];
44

55
// Used for main sidebar navigation
@@ -12,6 +12,7 @@ export const CATEGORIES = [
1212
'Circular Text',
1313
'Shiny Text',
1414
'Text Pressure',
15+
'Fuzzy Text',
1516
'Gradient Text',
1617
'Falling Text',
1718
'Decrypted Text',

src/constants/Components.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const textAnimations = {
3535
'scroll-reveal': () => import("../demo/TextAnimations/ScrollRevealDemo"),
3636
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo"),
3737
'glitch-text': () => import("../demo/TextAnimations/GlitchTextDemo"),
38+
'fuzzy-text': () => import("../demo/TextAnimations/FuzzyTextDemo"),
3839
};
3940

4041
const components = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { generateCliCommands } from '@/utils/utils';
2+
3+
import code from '@content/TextAnimations/FuzzyText/FuzzyText.jsx?raw';
4+
import tailwind from '@tailwind/TextAnimations/FuzzyText/FuzzyText.jsx?raw';
5+
import tsCode from '@ts-default/TextAnimations/FuzzyText/FuzzyText.tsx?raw';
6+
import tsTailwind from '@ts-tailwind/TextAnimations/FuzzyText/FuzzyText.tsx?raw';
7+
8+
export const fuzzyText = {
9+
...(generateCliCommands('TextAnimations/FuzzyText')),
10+
usage: `import FuzzyText from './FuzzyText';
11+
12+
<FuzzyText
13+
baseIntensity={0.2}
14+
hoverIntensity={hoverIntensity}
15+
enableHover={enableHover}
16+
>
17+
404
18+
</FuzzyText>`,
19+
code,
20+
tailwind,
21+
tsCode,
22+
tsTailwind
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { useEffect, useRef } from "react";
2+
3+
const FuzzyText = ({
4+
children,
5+
fontSize = "clamp(2rem, 10vw, 10rem)",
6+
fontWeight = 900,
7+
fontFamily = "inherit",
8+
color = "#fff",
9+
enableHover = true,
10+
baseIntensity = 0.18,
11+
hoverIntensity = 0.5,
12+
}) => {
13+
const canvasRef = useRef(null);
14+
15+
useEffect(() => {
16+
const canvas = canvasRef.current;
17+
if (!canvas) return;
18+
const ctx = canvas.getContext("2d");
19+
if (!ctx) return;
20+
21+
const computedFontFamily =
22+
fontFamily === "inherit"
23+
? window.getComputedStyle(canvas).fontFamily || "sans-serif"
24+
: fontFamily;
25+
26+
const fontSizeStr =
27+
typeof fontSize === "number" ? `${fontSize}px` : fontSize;
28+
let numericFontSize;
29+
if (typeof fontSize === "number") {
30+
numericFontSize = fontSize;
31+
} else {
32+
const temp = document.createElement("span");
33+
temp.style.fontSize = fontSize;
34+
document.body.appendChild(temp);
35+
const computedSize = window.getComputedStyle(temp).fontSize;
36+
numericFontSize = parseFloat(computedSize);
37+
document.body.removeChild(temp);
38+
}
39+
40+
const text = React.Children.toArray(children).join("");
41+
42+
const offscreen = document.createElement("canvas");
43+
const offCtx = offscreen.getContext("2d");
44+
if (!offCtx) return;
45+
46+
offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;
47+
offCtx.textBaseline = "alphabetic";
48+
const metrics = offCtx.measureText(text);
49+
50+
const actualLeft = metrics.actualBoundingBoxLeft ?? 0;
51+
const actualRight = metrics.actualBoundingBoxRight ?? metrics.width;
52+
const actualAscent = metrics.actualBoundingBoxAscent ?? numericFontSize;
53+
const actualDescent =
54+
metrics.actualBoundingBoxDescent ?? numericFontSize * 0.2;
55+
56+
const textBoundingWidth = Math.ceil(actualLeft + actualRight);
57+
const tightHeight = Math.ceil(actualAscent + actualDescent);
58+
59+
const extraWidthBuffer = 10;
60+
const offscreenWidth = textBoundingWidth + extraWidthBuffer;
61+
62+
offscreen.width = offscreenWidth;
63+
offscreen.height = tightHeight;
64+
65+
const xOffset = extraWidthBuffer / 2;
66+
offCtx.font = `${fontWeight} ${fontSizeStr} ${computedFontFamily}`;
67+
offCtx.textBaseline = "alphabetic";
68+
offCtx.fillStyle = color;
69+
offCtx.fillText(text, xOffset - actualLeft, actualAscent);
70+
71+
const horizontalMargin = 50;
72+
const verticalMargin = 0;
73+
canvas.width = offscreenWidth + horizontalMargin * 2;
74+
canvas.height = tightHeight + verticalMargin * 2;
75+
ctx.translate(horizontalMargin, verticalMargin);
76+
77+
const interactiveLeft = horizontalMargin + xOffset;
78+
const interactiveTop = verticalMargin;
79+
const interactiveRight = interactiveLeft + textBoundingWidth;
80+
const interactiveBottom = interactiveTop + tightHeight;
81+
82+
let isHovering = false;
83+
const fuzzRange = 30;
84+
let animationFrameId;
85+
86+
const run = () => {
87+
ctx.clearRect(
88+
-fuzzRange,
89+
-fuzzRange,
90+
offscreenWidth + 2 * fuzzRange,
91+
tightHeight + 2 * fuzzRange
92+
);
93+
const intensity = isHovering ? hoverIntensity : baseIntensity;
94+
for (let j = 0; j < tightHeight; j++) {
95+
const dx = Math.floor(intensity * (Math.random() - 0.5) * fuzzRange);
96+
ctx.drawImage(
97+
offscreen,
98+
0,
99+
j,
100+
offscreenWidth,
101+
1,
102+
dx,
103+
j,
104+
offscreenWidth,
105+
1
106+
);
107+
}
108+
animationFrameId = window.requestAnimationFrame(run);
109+
};
110+
111+
run();
112+
113+
const isInsideTextArea = (x, y) => {
114+
return (
115+
x >= interactiveLeft &&
116+
x <= interactiveRight &&
117+
y >= interactiveTop &&
118+
y <= interactiveBottom
119+
);
120+
};
121+
122+
const handleMouseMove = (e) => {
123+
if (!enableHover) return;
124+
const rect = canvas.getBoundingClientRect();
125+
const x = e.clientX - rect.left;
126+
const y = e.clientY - rect.top;
127+
isHovering = isInsideTextArea(x, y);
128+
};
129+
130+
const handleMouseLeave = () => {
131+
isHovering = false;
132+
};
133+
134+
const handleTouchMove = (e) => {
135+
if (!enableHover) return;
136+
e.preventDefault();
137+
const rect = canvas.getBoundingClientRect();
138+
const touch = e.touches[0];
139+
const x = touch.clientX - rect.left;
140+
const y = touch.clientY - rect.top;
141+
isHovering = isInsideTextArea(x, y);
142+
};
143+
144+
const handleTouchEnd = () => {
145+
isHovering = false;
146+
};
147+
148+
if (enableHover) {
149+
canvas.addEventListener("mousemove", handleMouseMove);
150+
canvas.addEventListener("mouseleave", handleMouseLeave);
151+
canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
152+
canvas.addEventListener("touchend", handleTouchEnd);
153+
}
154+
155+
return () => {
156+
window.cancelAnimationFrame(animationFrameId);
157+
if (enableHover) {
158+
canvas.removeEventListener("mousemove", handleMouseMove);
159+
canvas.removeEventListener("mouseleave", handleMouseLeave);
160+
canvas.removeEventListener("touchmove", handleTouchMove);
161+
canvas.removeEventListener("touchend", handleTouchEnd);
162+
}
163+
};
164+
}, [
165+
children,
166+
fontSize,
167+
fontWeight,
168+
fontFamily,
169+
color,
170+
enableHover,
171+
baseIntensity,
172+
hoverIntensity,
173+
]);
174+
175+
return <canvas ref={canvasRef} />;
176+
};
177+
178+
export default FuzzyText;
+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useState } from "react";
2+
import { CodeTab, PreviewTab, CliTab, TabbedLayout } from "../../components/common/TabbedLayout";
3+
import { Box, Flex, Spacer } from "@chakra-ui/react";
4+
5+
import PreviewSlider from "../../components/common/PreviewSlider";
6+
import PreviewSwitch from "../../components/common/PreviewSwitch";
7+
import Customize from "../../components/common/Customize";
8+
import CodeExample from "../../components/code/CodeExample";
9+
import CliInstallation from "../../components/code/CliInstallation";
10+
import PropTable from "../../components/common/PropTable";
11+
12+
import FuzzyText from "../../content/TextAnimations/FuzzyText/FuzzyText";
13+
import { fuzzyText } from "../../constants/code/TextAnimations/fuzzyTextCode";
14+
15+
const FuzzyTextDemo = () => {
16+
const [baseIntensity, setBaseIntensity] = useState(0.2);
17+
const [hoverIntensity, setHoverIntensity] = useState(0.5);
18+
const [enableHover, setEnableHover] = useState(true);
19+
20+
const propData = [
21+
{
22+
name: "children",
23+
type: "React.ReactNode",
24+
default: "",
25+
description: "The text content to display inside the fuzzy text component."
26+
},
27+
{
28+
name: "fontSize",
29+
type: "number | string",
30+
default: `"clamp(2rem, 8vw, 8rem)"`,
31+
description: "Specifies the font size of the text. Accepts any valid CSS font-size value or a number (interpreted as pixels)."
32+
},
33+
{
34+
name: "fontWeight",
35+
type: "string | number",
36+
default: "900",
37+
description: "Specifies the font weight of the text."
38+
},
39+
{
40+
name: "fontFamily",
41+
type: "string",
42+
default: `"inherit"`,
43+
description: "Specifies the font family of the text. 'inherit' uses the computed style from the parent."
44+
},
45+
{
46+
name: "color",
47+
type: "string",
48+
default: "#fff",
49+
description: "Specifies the text color."
50+
},
51+
{
52+
name: "enableHover",
53+
type: "boolean",
54+
default: "true",
55+
description: "Enables the hover effect for the fuzzy text."
56+
},
57+
{
58+
name: "baseIntensity",
59+
type: "number",
60+
default: "0.18",
61+
description: "The fuzz intensity when the text is not hovered."
62+
},
63+
{
64+
name: "hoverIntensity",
65+
type: "number",
66+
default: "0.5",
67+
description: "The fuzz intensity when the text is hovered."
68+
}
69+
];
70+
71+
return (
72+
<TabbedLayout>
73+
<PreviewTab>
74+
<Box position="relative" className="demo-container" h={500} overflow="hidden">
75+
<Flex direction='column'>
76+
<FuzzyText baseIntensity={baseIntensity} hoverIntensity={hoverIntensity} enableHover={enableHover}>
77+
404
78+
</FuzzyText>
79+
<Spacer my={1} />
80+
<FuzzyText baseIntensity={baseIntensity} hoverIntensity={hoverIntensity} enableHover={enableHover} fontSize={70} fontFamily="Gochi Hand">
81+
not found
82+
</FuzzyText>
83+
</Flex>
84+
</Box>
85+
86+
<Customize>
87+
<PreviewSlider
88+
title="Base Intensity"
89+
min={0}
90+
max={1}
91+
step={0.01}
92+
value={baseIntensity}
93+
onChange={(val) => {
94+
setBaseIntensity(val);
95+
}}
96+
/>
97+
98+
<PreviewSlider
99+
title="Hover Intensity"
100+
min={0}
101+
max={2}
102+
step={0.01}
103+
value={hoverIntensity}
104+
onChange={(val) => {
105+
setHoverIntensity(val);
106+
}}
107+
/>
108+
109+
<PreviewSwitch title="Enable Hover" isChecked={enableHover} onChange={(e) => { setEnableHover(e.target.checked); }} />
110+
</Customize>
111+
112+
<PropTable data={propData} />
113+
</PreviewTab>
114+
115+
<CodeTab>
116+
<CodeExample codeObject={fuzzyText} />
117+
</CodeTab>
118+
119+
<CliTab>
120+
<CliInstallation {...fuzzyText} />
121+
</CliTab>
122+
</TabbedLayout>
123+
);
124+
};
125+
126+
export default FuzzyTextDemo;

0 commit comments

Comments
 (0)