Skip to content

Commit fcb18b1

Browse files
committed
refactor: Morpher as reusable component
1 parent 6e1c936 commit fcb18b1

File tree

4 files changed

+128
-205
lines changed

4 files changed

+128
-205
lines changed
Lines changed: 39 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,50 @@
11
"use client"
22

3-
import { useEffect, useState } from "react"
4-
53
import { Image } from "@/components/Image"
64
import ParallaxImage from "@/components/Image/ParallaxImage"
5+
import Morpher from "@/components/Morpher"
76

87
import TenYearBackgroundImage from "@/public/images/10-year-anniversary/10-year-background.png"
98
import TenYearGraphicImage from "@/public/images/10-year-anniversary/10-year-graphic.png"
109

11-
const TenYearHero = () => {
12-
const [words, setWords] = useState<{ text: string; words: string[] }>({
13-
text: "censorship resistance",
14-
words: [
15-
"censorship resistance",
16-
"100% uptime",
17-
"decentralization",
18-
"community building",
19-
"developer growth",
20-
"global collaboration",
21-
"cypherpunk values",
22-
"hackathons",
23-
"censorship resistance",
24-
"permissionless finance",
25-
"credible neutrality",
26-
"the infinite garden",
27-
"client diversity",
28-
],
29-
})
30-
31-
// loops over chars to morph a text to another
32-
const morpher = (start: string, end: string): void => {
33-
// array of chars to randomly morph the text between start and end
34-
const chars = "abcdefghijklmnopqrstuvwxyz".split("")
35-
// duration of the global morph
36-
const duration = 3
37-
// speed of the morph for each letter
38-
const frameRate = 30
39-
40-
// text variables
41-
const textString = start.split("")
42-
const result = end.split("")
43-
const slen = textString.length
44-
const rlen = result.length
45-
46-
// time variables
47-
let present = new Date()
48-
let past = present.getTime()
49-
let count = 0
50-
let spentTime = 0
51-
// splitTime = milliseconds / letters
52-
const splitTime = (duration * 70) / Math.max(slen, rlen)
53-
54-
function update() {
55-
// Update present date and spent time
56-
present = new Date()
57-
spentTime += present.getTime() - past
58-
59-
// Random letters
60-
for (let i = count; i < Math.min(slen, rlen, 18); i++) {
61-
const random = Math.floor(Math.random() * (chars.length - 1))
62-
// Change letter
63-
textString[i] = chars[random]
64-
}
65-
66-
// Morph letters from start to end
67-
if (spentTime >= splitTime) {
68-
// Update count of letters to morph
69-
count += Math.floor(spentTime / splitTime)
70-
// Morphing
71-
for (let j = 0; j < count; j++) {
72-
textString[j] = result[j] || ""
73-
}
74-
// Reset spent time
75-
spentTime = 0
76-
}
77-
78-
// Update DOM
79-
setWords({ ...words, text: textString.join("") })
80-
81-
// Save present date
82-
past = present.getTime()
83-
84-
// Loop
85-
if (count < Math.max(slen, rlen)) {
86-
// Only use a setTimeout if the frameRate is lower than 60FPS
87-
// Remove the setTimeout if the frameRate is equal to 60FPS
88-
morphTimeout = setTimeout(() => {
89-
window.requestAnimationFrame(update)
90-
}, 1000 / frameRate)
91-
}
92-
}
93-
94-
// Start loop
95-
update()
96-
}
97-
98-
let morphTimeout: NodeJS.Timeout
99-
100-
useEffect(() => {
101-
let counter = 0
102-
103-
const morphInterval = setInterval(() => {
104-
const start = words.text
105-
const end = words.words[counter]
106-
107-
morpher(start, end)
108-
109-
if (counter < words.words.length - 1) {
110-
counter++
111-
} else {
112-
counter = 0
113-
}
114-
}, 3000)
115-
116-
return () => {
117-
clearInterval(morphInterval)
118-
clearTimeout(morphTimeout)
119-
}
120-
// eslint-disable-next-line react-hooks/exhaustive-deps
121-
}, [])
122-
123-
return (
124-
<div>
125-
<div className="relative mb-16">
126-
<Image
127-
src={TenYearBackgroundImage}
128-
alt="10 Year Anniversary"
129-
className="max-h-[350px] object-cover"
130-
/>
131-
<ParallaxImage
132-
src={TenYearGraphicImage}
133-
alt="10 Year Anniversary"
134-
className="absolute left-0 top-0 max-h-[350px] object-contain transition-transform duration-200 ease-out"
135-
/>
136-
</div>
137-
<p className="text-center text-3xl">
138-
Celebrating 10 years of{" "}
139-
<span className="font-bold text-accent-b">{words.text}</span>
140-
</p>
10+
const TenYearHero = () => (
11+
<div>
12+
<div className="relative mb-16">
13+
<Image
14+
src={TenYearBackgroundImage}
15+
alt="10 Year Anniversary"
16+
className="max-h-[350px] object-cover"
17+
/>
18+
<ParallaxImage
19+
src={TenYearGraphicImage}
20+
alt="10 Year Anniversary"
21+
className="absolute left-0 top-0 max-h-[350px] object-contain transition-transform duration-200 ease-out"
22+
/>
14123
</div>
142-
)
143-
}
24+
<p className="text-center text-3xl">
25+
Celebrating 10 years of{" "}
26+
<span className="font-bold text-accent-b">
27+
<Morpher
28+
text="censorship resistance"
29+
words={[
30+
"censorship resistance",
31+
"100% uptime",
32+
"decentralization",
33+
"community building",
34+
"developer growth",
35+
"global collaboration",
36+
"cypherpunk values",
37+
"hackathons",
38+
"censorship resistance",
39+
"permissionless finance",
40+
"credible neutrality",
41+
"the infinite garden",
42+
"client diversity",
43+
]}
44+
/>
45+
</span>
46+
</p>
47+
</div>
48+
)
14449

14550
export default TenYearHero

src/components/Hero/HomeHero/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ClassNameProp, CommonHeroProps } from "@/lib/types"
22

3+
import LanguageMorpher from "@/components/Homepage/LanguageMorpher"
34
import { Image } from "@/components/Image"
4-
import Morpher from "@/components/Morpher"
55

66
import useTranslation from "@/hooks/useTranslation"
77

@@ -23,7 +23,7 @@ const HomeHero = ({ heroImg, className }: HomeHeroProps) => {
2323
/>
2424
</div>
2525
<div className="flex flex-col items-center border-t-[3px] border-primary-low-contrast px-4 py-10 text-center">
26-
<Morpher />
26+
<LanguageMorpher />
2727
<div className="flex flex-col items-center gap-y-5 lg:max-w-2xl">
2828
<h1 className="font-black">{t("page-index:page-index-title")}</h1>
2929
<p className="max-w-96 text-md text-body-medium lg:text-lg">
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client"
2+
3+
import Morpher from "@/components/Morpher"
4+
import { Button } from "@/components/ui/buttons/Button"
5+
6+
import {
7+
DESKTOP_LANGUAGE_BUTTON_NAME,
8+
HAMBURGER_BUTTON_ID,
9+
MOBILE_LANGUAGE_BUTTON_NAME,
10+
} from "@/lib/constants"
11+
12+
import { useMediaQuery } from "@/hooks/useMediaQuery"
13+
14+
const LanguageMorpher = () => {
15+
const handleMobileClick = () => {
16+
;(document.getElementById(HAMBURGER_BUTTON_ID) as HTMLButtonElement).click()
17+
setTimeout(
18+
() =>
19+
(
20+
document.querySelector(
21+
`button[name="${MOBILE_LANGUAGE_BUTTON_NAME}"`
22+
) as HTMLButtonElement
23+
).click(),
24+
1
25+
)
26+
}
27+
const handleDesktopClick = () => {
28+
;(
29+
document.querySelector(
30+
`button[name="${DESKTOP_LANGUAGE_BUTTON_NAME}"`
31+
) as HTMLButtonElement
32+
).click()
33+
}
34+
35+
const [isLarge] = useMediaQuery(["(min-width: 48rem)"]) // TW md breakpoint, 768px
36+
37+
return (
38+
<Button
39+
className="mx-auto w-fit text-md text-primary no-underline"
40+
onClick={isLarge ? handleDesktopClick : handleMobileClick}
41+
variant="ghost"
42+
>
43+
<Morpher
44+
text="Ethereum"
45+
words={[
46+
"以太坊",
47+
"イーサリアム",
48+
"Etérium",
49+
"이더리움",
50+
"اتریوم",
51+
"Αιθέριο",
52+
"Eterijum",
53+
"إثيريوم",
54+
"อีเธอเรียม",
55+
"Эфириум",
56+
"इथीरियम",
57+
"ಇಥೀರಿಯಮ್",
58+
"אתריום",
59+
"Ξ",
60+
"ইথেরিয়াম",
61+
"எதீரியம்",
62+
"ఇథిరియూమ్",
63+
]}
64+
charSet="abcdxyz01234567{}%$?!"
65+
/>
66+
</Button>
67+
)
68+
}
69+
70+
LanguageMorpher.displayName = "LanguageMorpher"
71+
72+
export default LanguageMorpher

src/components/Morpher.tsx

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,24 @@
11
"use client"
22

33
import { useEffect, useState } from "react"
4-
import { useMediaQuery } from "usehooks-ts"
5-
6-
import { Button } from "@/components/ui/buttons/Button"
7-
8-
import {
9-
DESKTOP_LANGUAGE_BUTTON_NAME,
10-
HAMBURGER_BUTTON_ID,
11-
MOBILE_LANGUAGE_BUTTON_NAME,
12-
} from "@/lib/constants"
13-
14-
const Morpher = () => {
15-
const [state, setState] = useState({
16-
text: "Ethereum",
17-
words: [
18-
"以太坊",
19-
"イーサリアム",
20-
"Etérium",
21-
"이더리움",
22-
"اتریوم",
23-
"Αιθέριο",
24-
"Eterijum",
25-
"إثيريوم",
26-
"อีเธอเรียม",
27-
"Эфириум",
28-
"इथीरियम",
29-
"ಇಥೀರಿಯಮ್",
30-
"אתריום",
31-
"Ξ",
32-
"ইথেরিয়াম",
33-
"எதீரியம்",
34-
"ఇథిరియూమ్",
35-
],
36-
})
4+
5+
type MorpherProps = {
6+
text: string
7+
words: string[]
8+
charSet?: string
9+
}
10+
11+
const Morpher = ({
12+
text,
13+
words,
14+
charSet = "abcdefghijklmnopqrstuvwxyz",
15+
}: MorpherProps) => {
16+
const [state, setState] = useState({ text, words })
3717

3818
// loops over chars to morph a text to another
3919
const morpher = (start: string, end: string): void => {
4020
// array of chars to randomly morph the text between start and end
41-
const chars = "abcdxyz01234567{}%$?!".split("")
21+
const chars = charSet.split("")
4222
// duration of the global morph
4323
const duration = 3
4424
// speed of the morph for each letter
@@ -127,41 +107,7 @@ const Morpher = () => {
127107
// eslint-disable-next-line react-hooks/exhaustive-deps
128108
}, [])
129109

130-
const isLarge = useMediaQuery("(min-width: 48rem)") // TW md breakpoint, 768px
131-
132-
const handleMobileClick = () => {
133-
if (!document) return
134-
;(document.getElementById(HAMBURGER_BUTTON_ID) as HTMLButtonElement).click()
135-
setTimeout(
136-
() =>
137-
(
138-
document.querySelector(
139-
`button[name="${MOBILE_LANGUAGE_BUTTON_NAME}"`
140-
) as HTMLButtonElement
141-
).click(),
142-
1
143-
)
144-
}
145-
const handleDesktopClick = () => {
146-
if (!document) return
147-
;(
148-
document.querySelector(
149-
`button[name="${DESKTOP_LANGUAGE_BUTTON_NAME}"`
150-
) as HTMLButtonElement
151-
).click()
152-
}
153-
154-
return (
155-
<>
156-
<Button
157-
className="mx-auto w-fit text-md text-primary no-underline"
158-
onClick={isLarge ? handleDesktopClick : handleMobileClick}
159-
variant="ghost"
160-
>
161-
{state.text}
162-
</Button>
163-
</>
164-
)
110+
return state.text
165111
}
166112

167113
export default Morpher

0 commit comments

Comments
 (0)