1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
import { createEffect, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
const px = (value: number | string | undefined, fallback: number) => {
if (typeof value === "number") return `${value}px`
if (typeof value === "string") return value
return `${fallback}px`
}
const ms = (value: number | string | undefined, fallback: number) => {
if (typeof value === "number") return `${value}ms`
if (typeof value === "string") return value
return `${fallback}ms`
}
const pct = (value: number | undefined, fallback: number) => {
const v = value ?? fallback
return `${v}%`
}
export function TextReveal(props: {
text?: string
class?: string
duration?: number | string
/** Gradient edge softness as a percentage of the mask (0 = hard wipe, 17 = soft). */
edge?: number
/** Optional small vertical travel for entering text (px). Default 0. */
travel?: number | string
spring?: string
springSoft?: string
growOnly?: boolean
truncate?: boolean
}) {
const [state, setState] = createStore({
cur: props.text,
old: undefined as string | undefined,
width: "auto",
ready: false,
swapping: false,
})
const cur = () => state.cur
const old = () => state.old
const width = () => state.width
const ready = () => state.ready
const swapping = () => state.swapping
let inRef: HTMLSpanElement | undefined
let outRef: HTMLSpanElement | undefined
let rootRef: HTMLSpanElement | undefined
let frame: number | undefined
const win = () => inRef?.scrollWidth ?? 0
const wout = () => outRef?.scrollWidth ?? 0
const widen = (next: number) => {
if (next <= 0) return
if (props.growOnly ?? true) {
const prev = Number.parseFloat(width())
if (Number.isFinite(prev) && next <= prev) return
}
setState("width", `${next}px`)
}
createEffect(
on(
() => props.text,
(next, prev) => {
if (next === prev) return
if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) {
setState("cur", next)
widen(win())
return
}
setState("swapping", true)
setState("old", prev)
setState("cur", next)
if (typeof requestAnimationFrame !== "function") {
widen(Math.max(win(), wout()))
rootRef?.offsetHeight
setState("swapping", false)
return
}
if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => {
widen(Math.max(win(), wout()))
rootRef?.offsetHeight
setState("swapping", false)
frame = undefined
})
},
),
)
onMount(() => {
widen(win())
const fonts = typeof document !== "undefined" ? document.fonts : undefined
if (typeof requestAnimationFrame !== "function") {
setState("ready", true)
return
}
if (!fonts) {
requestAnimationFrame(() => setState("ready", true))
return
}
void fonts.ready.finally(() => {
widen(win())
requestAnimationFrame(() => setState("ready", true))
})
})
onCleanup(() => {
if (frame === undefined || typeof cancelAnimationFrame !== "function") return
cancelAnimationFrame(frame)
})
return (
<span
ref={rootRef}
data-component="text-reveal"
data-ready={ready() ? "true" : "false"}
data-swapping={swapping() ? "true" : "false"}
data-truncate={props.truncate ? "true" : "false"}
class={props.class}
aria-label={props.text ?? ""}
style={{
"--text-reveal-duration": ms(props.duration, 450),
"--text-reveal-edge": pct(props.edge, 17),
"--text-reveal-travel": px(props.travel, 0),
"--text-reveal-spring": props.spring ?? "cubic-bezier(0.34, 1.08, 0.64, 1)",
"--text-reveal-spring-soft": props.springSoft ?? "cubic-bezier(0.34, 1, 0.64, 1)",
}}
>
<span data-slot="text-reveal-track" style={{ width: props.truncate ? "100%" : width() }}>
<span data-slot="text-reveal-entering" ref={inRef}>
{cur() ?? "\u00A0"}
</span>
<span data-slot="text-reveal-leaving" ref={outRef}>
{old() ?? "\u00A0"}
</span>
</span>
</span>
)
}
|