Skip to content

Commit 46f2b34

Browse files
authored
feat: create text explosion effect (codse#189)
1 parent 59a3122 commit 46f2b34

File tree

5 files changed

+238
-15
lines changed

5 files changed

+238
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ next-env.d.ts
3939

4040
*storybook.log
4141
public/preview
42+
_internal/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import TextExplodeIMessage from "@/animata/text/text-explode-imessage";
2+
import { Meta, StoryObj } from "@storybook/react";
3+
4+
const meta = {
5+
title: "Text/Text Explode Imessage",
6+
component: TextExplodeIMessage,
7+
parameters: {
8+
layout: "centered",
9+
},
10+
tags: ["autodocs"],
11+
argTypes: {},
12+
} satisfies Meta<typeof TextExplodeIMessage>;
13+
14+
export default meta;
15+
type Story = StoryObj<typeof meta>;
16+
17+
export const Primary: Story = {
18+
args: {
19+
text: "iMessage text explode effect 🧨 🔥 🎃 🎉 🪅",
20+
className: "text-red-500",
21+
},
22+
};
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { useCallback, useEffect, useRef } from "react";
2+
import { motion, useAnimationControls, Variants } from "framer-motion";
3+
4+
import { cn } from "@/lib/utils";
5+
6+
const containerVariants: Variants = {
7+
initial: {
8+
opacity: 1,
9+
translateY: 0,
10+
transition: {
11+
duration: 0.5,
12+
},
13+
letterSpacing: "0px",
14+
},
15+
shrink: {
16+
// Scale might need to be adjust according to font size for better effect
17+
scale: 0.8,
18+
letterSpacing: "-10%",
19+
},
20+
jitter: {
21+
x: [0, -3, 3, -3, 3, 0],
22+
y: [0, -2, 2, -2, 2, 0],
23+
transition: {
24+
duration: 0.5,
25+
times: [0, 0.2, 0.4, 0.6, 0.8, 1],
26+
ease: "easeInOut",
27+
},
28+
},
29+
explode: {
30+
scale: [0.7, 0.9, 1],
31+
opacity: [1, 0.7, 0],
32+
letterSpacing: "0px",
33+
transition: {
34+
times: [0, 0.9, 1],
35+
},
36+
},
37+
end: {
38+
scale: 1,
39+
letterSpacing: "0px",
40+
translateY: 50,
41+
},
42+
};
43+
44+
const createExplosion = ({ index, total }: { index: number; total: number }) => {
45+
const direction = Math.random() > Math.random() ? -1 : 1;
46+
47+
const x = Math.random() * 10 * total * direction;
48+
49+
const radius = total * 4;
50+
const angleRange = Math.PI;
51+
const angle = (index / (total - 1)) * angleRange;
52+
const y = radius * -Math.sin(angle) * Math.random();
53+
54+
const rotation = Math.random() * 360 * direction;
55+
56+
return {
57+
translateX: [0, x * 0.5, x * 0.7, x],
58+
translateY: [0, y, -y / 5, 0, 5],
59+
rotate: [0, rotation * 0.4, rotation * 0.8, rotation],
60+
scale: [0.9, 1.2, 1 + Math.random() + 0.2, 1 + Math.random() * 2],
61+
opacity: [1, 0.8, 0.5, 0],
62+
};
63+
};
64+
65+
const characterVariants: Variants = {
66+
jitter: () => ({
67+
x: [0, -3 + Math.random() * 6, 3 - Math.random() * 6, 0],
68+
y: [0, -2 + Math.random() * 4, 2 - Math.random() * 4, 0],
69+
transition: {
70+
duration: 0.5,
71+
times: [0, 0.33, 0.66, 1],
72+
ease: "easeInOut",
73+
},
74+
}),
75+
shrink: {
76+
scale: 1.1,
77+
},
78+
explode: createExplosion,
79+
end: {
80+
translateY: 0,
81+
translateX: 0,
82+
rotate: 0,
83+
scale: 1,
84+
},
85+
initial: {
86+
opacity: 1,
87+
},
88+
};
89+
90+
const splitText = (text: string) => String(text).split(/(?:)/u);
91+
92+
export default function TextExplodeIMessage({
93+
text,
94+
mode = "loop",
95+
className,
96+
}: {
97+
text: string;
98+
className?: string;
99+
mode?: "loop" | "hover";
100+
}) {
101+
const characters = splitText(text);
102+
const controls = useAnimationControls();
103+
const isPlaying = useRef(false);
104+
105+
const animateSequence = useCallback(async () => {
106+
await Promise.all([
107+
controls.start("shrink", {
108+
duration: 1,
109+
ease: "easeOut",
110+
}),
111+
controls.start("jitter", {
112+
delay: 0.1,
113+
}),
114+
]);
115+
await controls.start("explode", {});
116+
await controls.start("end");
117+
await controls.start("initial", {
118+
delay: 0.5,
119+
duration: 1,
120+
type: "spring",
121+
});
122+
123+
if (mode === "loop") {
124+
requestAnimationFrame(() => animateSequence());
125+
} else {
126+
isPlaying.current = false;
127+
}
128+
}, [mode, controls]);
129+
130+
useEffect(() => {
131+
if (!characters.length || mode === "hover") {
132+
return;
133+
}
134+
135+
animateSequence();
136+
}, [characters.length, mode, animateSequence]);
137+
138+
return (
139+
<motion.div
140+
variants={containerVariants}
141+
animate={controls}
142+
onPointerDown={() => {
143+
if (mode === "hover" && !isPlaying.current) {
144+
isPlaying.current = true;
145+
animateSequence();
146+
}
147+
}}
148+
onMouseEnter={() => {
149+
if (mode === "hover" && !isPlaying.current) {
150+
isPlaying.current = true;
151+
animateSequence();
152+
}
153+
}}
154+
className={cn(
155+
"flex items-center justify-center text-3xl tracking-normal text-foreground",
156+
className,
157+
)}
158+
>
159+
{characters.map((char, index) => (
160+
<motion.span
161+
key={index}
162+
variants={characterVariants}
163+
custom={{ index, total: characters.length }}
164+
className="inline-block"
165+
>
166+
{char === " " ? "\u00A0" : char}
167+
</motion.span>
168+
))}
169+
</motion.div>
170+
);
171+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: Text Explode (iMessage)
3+
description: Text explode effect as seen in iMessage
4+
author: harimanok_
5+
---
6+
7+
<ComponentPreview name="text-text-explode-imessage--docs" />
8+
9+
## Installation
10+
11+
<Steps>
12+
<Step>Install dependencies</Step>
13+
14+
```bash
15+
npm install framer-motion
16+
```
17+
18+
<Step>Run the following command</Step>
19+
20+
It will create a new file `text-explode-imessage.tsx` inside the `components/animata/text` directory.
21+
22+
```bash
23+
mkdir -p components/animata/text && touch components/animata/text/text-explode-imessage.tsx
24+
```
25+
26+
<Step>Paste the code</Step>{" "}
27+
28+
Open the newly created file and paste the following code:
29+
30+
```jsx file=<rootDir>/animata/text/text-explode-imessage.tsx
31+
32+
```
33+
34+
</Steps>
35+
36+
## Credits
37+
38+
Built by [hari](https://github.com/hari)

tsconfig.json

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
{
22
"compilerOptions": {
33
"baseUrl": ".",
4-
"lib": [
5-
"dom",
6-
"dom.iterable",
7-
"esnext"
8-
],
4+
"lib": ["dom", "dom.iterable", "esnext"],
95
"allowJs": true,
106
"skipLibCheck": true,
117
"strict": true,
@@ -17,18 +13,15 @@
1713
"isolatedModules": true,
1814
"jsx": "preserve",
1915
"incremental": true,
16+
"target": "ESNext",
2017
"plugins": [
2118
{
2219
"name": "next"
2320
}
2421
],
2522
"paths": {
26-
"@/*": [
27-
"./*"
28-
],
29-
"contentlayer/generated": [
30-
"./.contentlayer/generated"
31-
]
23+
"@/*": ["./*"],
24+
"contentlayer/generated": ["./.contentlayer/generated"]
3225
}
3326
},
3427
"include": [
@@ -38,7 +31,5 @@
3831
".next/types/**/*.ts",
3932
".contentlayer/generated"
4033
],
41-
"exclude": [
42-
"node_modules"
43-
]
44-
}
34+
"exclude": ["node_modules"]
35+
}

0 commit comments

Comments
 (0)