-
Notifications
You must be signed in to change notification settings - Fork 523
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Morphing Text component and demo (#294)
* feat: add Morphing Text component and demos * feat: update Morphing Text component title * fix: lint fix --------- Co-authored-by: Arghya Das <arghyadasproject@gmail.com>
- Loading branch information
1 parent
f69220c
commit 4e527dd
Showing
9 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
--- | ||
title: Morphing Text | ||
date: 2024-09-02 | ||
description: A dynamic text morphing component for Magic UI. | ||
author: magicui | ||
published: true | ||
--- | ||
|
||
<ComponentPreview name="morphing-text-demo" /> | ||
|
||
## Installation | ||
|
||
<Tabs defaultValue="cli"> | ||
|
||
<TabsList> | ||
<TabsTrigger value="cli">CLI</TabsTrigger> | ||
<TabsTrigger value="manual">Manual</TabsTrigger> | ||
</TabsList> | ||
<TabsContent value="cli"> | ||
|
||
```bash | ||
npx shadcn@latest add "https://magicui.design/r/morphing-text" | ||
``` | ||
|
||
</TabsContent> | ||
|
||
<TabsContent value="manual"> | ||
|
||
<Steps> | ||
|
||
<Step>Copy and paste the following code into your project.</Step> | ||
|
||
<ComponentSource name="morphing-text" /> | ||
|
||
</Steps> | ||
|
||
</TabsContent> | ||
|
||
</Tabs> | ||
|
||
## Props | ||
|
||
| Prop | Type | Description | Default | | ||
| ----------- | ---------- | ------------------------------------ | ------- | | ||
| `texts` | `string[]` | Array of texts to morph between | `[]` | | ||
| `className` | `string?` | Additional classes for the container | `""` | | ||
|
||
This `MorphingText` component dynamically transitions between an array of text strings, creating a smooth, engaging visual effect. | ||
|
||
## Credits | ||
|
||
- Credit to [@luis-code](https://luis-code.vercel.app/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "morphing-text", | ||
"type": "registry:ui", | ||
"dependencies": [ | ||
"framer-motion" | ||
], | ||
"files": [ | ||
{ | ||
"path": "magicui/morphing-text.tsx", | ||
"content": "\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst morphTime = 1.5;\nconst cooldownTime = 0.5;\n\nconst useMorphingText = (texts: string[]) => {\n const textIndexRef = useRef(0);\n const morphRef = useRef(0);\n const cooldownRef = useRef(0);\n const timeRef = useRef(new Date());\n\n const text1Ref = useRef<HTMLSpanElement>(null);\n const text2Ref = useRef<HTMLSpanElement>(null);\n\n const setStyles = useCallback(\n (fraction: number) => {\n const [current1, current2] = [text1Ref.current, text2Ref.current];\n if (!current1 || !current2) return;\n\n current2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;\n current2.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`;\n\n const invertedFraction = 1 - fraction;\n current1.style.filter = `blur(${Math.min(8 / invertedFraction - 8, 100)}px)`;\n current1.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`;\n\n current1.textContent = texts[textIndexRef.current % texts.length];\n current2.textContent = texts[(textIndexRef.current + 1) % texts.length];\n },\n [texts]\n );\n\n const doMorph = useCallback(() => {\n morphRef.current -= cooldownRef.current;\n cooldownRef.current = 0;\n\n let fraction = morphRef.current / morphTime;\n\n if (fraction > 1) {\n cooldownRef.current = cooldownTime;\n fraction = 1;\n }\n\n setStyles(fraction);\n\n if (fraction === 1) {\n textIndexRef.current++;\n }\n }, [setStyles]);\n\n const doCooldown = useCallback(() => {\n morphRef.current = 0;\n const [current1, current2] = [text1Ref.current, text2Ref.current];\n if (current1 && current2) {\n current2.style.filter = \"none\";\n current2.style.opacity = \"100%\";\n current1.style.filter = \"none\";\n current1.style.opacity = \"0%\";\n }\n }, []);\n\n useEffect(() => {\n let animationFrameId: number;\n\n const animate = () => {\n animationFrameId = requestAnimationFrame(animate);\n\n const newTime = new Date();\n const dt = (newTime.getTime() - timeRef.current.getTime()) / 1000;\n timeRef.current = newTime;\n\n cooldownRef.current -= dt;\n\n if (cooldownRef.current <= 0) doMorph();\n else doCooldown();\n };\n\n animate();\n return () => {\n cancelAnimationFrame(animationFrameId);\n };\n }, [doMorph, doCooldown]);\n\n return { text1Ref, text2Ref };\n};\n\ninterface MorphingTextProps {\n className?: string;\n texts: string[];\n}\n\nconst Texts: React.FC<Pick<MorphingTextProps, \"texts\">> = ({ texts }) => {\n const { text1Ref, text2Ref } = useMorphingText(texts);\n return (\n <>\n <span\n className=\"absolute inset-x-0 top-0 m-auto inline-block w-full\"\n ref={text1Ref}\n />\n <span\n className=\"absolute inset-x-0 top-0 m-auto inline-block w-full\"\n ref={text2Ref}\n />\n </>\n );\n};\n\nconst SvgFilters: React.FC = () => (\n <svg id=\"filters\" className=\"hidden\" preserveAspectRatio=\"xMidYMid slice\">\n <defs>\n <filter id=\"threshold\">\n <feColorMatrix\n in=\"SourceGraphic\"\n type=\"matrix\"\n values=\"1 0 0 0 0\n 0 1 0 0 0\n 0 0 1 0 0\n 0 0 0 255 -140\"\n />\n </filter>\n </defs>\n </svg>\n);\n\nconst MorphingText: React.FC<MorphingTextProps> = ({ texts, className }) => (\n <div\n className={cn(\n \"relative mx-auto h-16 w-full max-w-screen-md text-center font-sans text-[40pt] font-bold leading-none [filter:url(#threshold)_blur(0.6px)] md:h-24 lg:text-[6rem]\",\n className\n )}\n >\n <Texts texts={texts} />\n <SvgFilters />\n </div>\n);\n\nexport default MorphingText;\n", | ||
"type": "registry:ui", | ||
"target": "" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import MorphingText from "@/registry/default/magicui/morphing-text"; | ||
|
||
const texts = [ | ||
"Hello", | ||
"Morphing", | ||
"Text", | ||
"Animation", | ||
"React", | ||
"Component", | ||
"Smooth", | ||
"Transition", | ||
"Engaging", | ||
]; | ||
|
||
export default function MorphingTextDemo() { | ||
return <MorphingText texts={texts} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
"use client"; | ||
|
||
import { useCallback, useEffect, useRef } from "react"; | ||
|
||
import { cn } from "@/lib/utils"; | ||
|
||
const morphTime = 1.5; | ||
const cooldownTime = 0.5; | ||
|
||
const useMorphingText = (texts: string[]) => { | ||
const textIndexRef = useRef(0); | ||
const morphRef = useRef(0); | ||
const cooldownRef = useRef(0); | ||
const timeRef = useRef(new Date()); | ||
|
||
const text1Ref = useRef<HTMLSpanElement>(null); | ||
const text2Ref = useRef<HTMLSpanElement>(null); | ||
|
||
const setStyles = useCallback( | ||
(fraction: number) => { | ||
const [current1, current2] = [text1Ref.current, text2Ref.current]; | ||
if (!current1 || !current2) return; | ||
|
||
current2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`; | ||
current2.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`; | ||
|
||
const invertedFraction = 1 - fraction; | ||
current1.style.filter = `blur(${Math.min(8 / invertedFraction - 8, 100)}px)`; | ||
current1.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`; | ||
|
||
current1.textContent = texts[textIndexRef.current % texts.length]; | ||
current2.textContent = texts[(textIndexRef.current + 1) % texts.length]; | ||
}, | ||
[texts], | ||
); | ||
|
||
const doMorph = useCallback(() => { | ||
morphRef.current -= cooldownRef.current; | ||
cooldownRef.current = 0; | ||
|
||
let fraction = morphRef.current / morphTime; | ||
|
||
if (fraction > 1) { | ||
cooldownRef.current = cooldownTime; | ||
fraction = 1; | ||
} | ||
|
||
setStyles(fraction); | ||
|
||
if (fraction === 1) { | ||
textIndexRef.current++; | ||
} | ||
}, [setStyles]); | ||
|
||
const doCooldown = useCallback(() => { | ||
morphRef.current = 0; | ||
const [current1, current2] = [text1Ref.current, text2Ref.current]; | ||
if (current1 && current2) { | ||
current2.style.filter = "none"; | ||
current2.style.opacity = "100%"; | ||
current1.style.filter = "none"; | ||
current1.style.opacity = "0%"; | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
let animationFrameId: number; | ||
|
||
const animate = () => { | ||
animationFrameId = requestAnimationFrame(animate); | ||
|
||
const newTime = new Date(); | ||
const dt = (newTime.getTime() - timeRef.current.getTime()) / 1000; | ||
timeRef.current = newTime; | ||
|
||
cooldownRef.current -= dt; | ||
|
||
if (cooldownRef.current <= 0) doMorph(); | ||
else doCooldown(); | ||
}; | ||
|
||
animate(); | ||
return () => { | ||
cancelAnimationFrame(animationFrameId); | ||
}; | ||
}, [doMorph, doCooldown]); | ||
|
||
return { text1Ref, text2Ref }; | ||
}; | ||
|
||
interface MorphingTextProps { | ||
className?: string; | ||
texts: string[]; | ||
} | ||
|
||
const Texts: React.FC<Pick<MorphingTextProps, "texts">> = ({ texts }) => { | ||
const { text1Ref, text2Ref } = useMorphingText(texts); | ||
return ( | ||
<> | ||
<span | ||
className="absolute inset-x-0 top-0 m-auto inline-block w-full" | ||
ref={text1Ref} | ||
/> | ||
<span | ||
className="absolute inset-x-0 top-0 m-auto inline-block w-full" | ||
ref={text2Ref} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
const SvgFilters: React.FC = () => ( | ||
<svg id="filters" className="hidden" preserveAspectRatio="xMidYMid slice"> | ||
<defs> | ||
<filter id="threshold"> | ||
<feColorMatrix | ||
in="SourceGraphic" | ||
type="matrix" | ||
values="1 0 0 0 0 | ||
0 1 0 0 0 | ||
0 0 1 0 0 | ||
0 0 0 255 -140" | ||
/> | ||
</filter> | ||
</defs> | ||
</svg> | ||
); | ||
|
||
const MorphingText: React.FC<MorphingTextProps> = ({ texts, className }) => ( | ||
<div | ||
className={cn( | ||
"relative mx-auto h-16 w-full max-w-screen-md text-center font-sans text-[40pt] font-bold leading-none [filter:url(#threshold)_blur(0.6px)] md:h-24 lg:text-[6rem]", | ||
className, | ||
)} | ||
> | ||
<Texts texts={texts} /> | ||
<SvgFilters /> | ||
</div> | ||
); | ||
|
||
export default MorphingText; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters