Skip to content

Commit d89a219

Browse files
authored
Merge pull request #15593 from ethereum/morpher-refactor
refactor: Morpher as reusable component
2 parents 94d2aaa + 6f90875 commit d89a219

File tree

4 files changed

+118
-202
lines changed

4 files changed

+118
-202
lines changed
Lines changed: 31 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
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 [initialText, ...initialWords] = [
10+
const WORDS = [
1211
"censorship resistance",
1312
"100% uptime",
1413
"decentralization",
@@ -24,134 +23,35 @@ const [initialText, ...initialWords] = [
2423
"client diversity",
2524
]
2625

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

15757
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+
words={[
45+
"Ethereum",
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: 13 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,22 @@
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+
words: string[]
7+
charSet?: string
8+
}
9+
10+
const Morpher = ({
11+
words,
12+
charSet = "abcdefghijklmnopqrstuvwxyz",
13+
}: MorpherProps) => {
14+
const [state, setState] = useState({ text: words[0], words })
3715

3816
// loops over chars to morph a text to another
3917
const morpher = (start: string, end: string): void => {
4018
// array of chars to randomly morph the text between start and end
41-
const chars = "abcdxyz01234567{}%$?!".split("")
19+
const chars = charSet.split("")
4220
// duration of the global morph
4321
const duration = 3
4422
// speed of the morph for each letter
@@ -127,41 +105,7 @@ const Morpher = () => {
127105
// eslint-disable-next-line react-hooks/exhaustive-deps
128106
}, [])
129107

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-
)
108+
return state.text
165109
}
166110

167111
export default Morpher

0 commit comments

Comments
 (0)