mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-23 09:04:47 +00:00
@@ -1,13 +1,4 @@
|
||||
import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js"
|
||||
import { useReducedMotion } from "../hooks/use-reduced-motion"
|
||||
import {
|
||||
animate,
|
||||
type AnimationPlaybackControls,
|
||||
clearFadeStyles,
|
||||
clearMaskStyles,
|
||||
GROW_SPRING,
|
||||
WIPE_MASK,
|
||||
} from "./motion"
|
||||
|
||||
const px = (value: number | string | undefined, fallback: number) => {
|
||||
if (typeof value === "number") return `${value}px`
|
||||
@@ -26,11 +17,6 @@ const pct = (value: number | undefined, fallback: number) => {
|
||||
return `${v}%`
|
||||
}
|
||||
|
||||
const clearWipe = (el: HTMLElement) => {
|
||||
clearFadeStyles(el)
|
||||
clearMaskStyles(el)
|
||||
}
|
||||
|
||||
export function TextReveal(props: {
|
||||
text?: string
|
||||
class?: string
|
||||
@@ -53,8 +39,10 @@ export function TextReveal(props: {
|
||||
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) {
|
||||
@@ -63,14 +51,21 @@ export function TextReveal(props: {
|
||||
}
|
||||
setWidth(`${next}px`)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.text,
|
||||
(next, prev) => {
|
||||
if (next === prev) return
|
||||
if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) {
|
||||
setCur(next)
|
||||
widen(win())
|
||||
return
|
||||
}
|
||||
setSwapping(true)
|
||||
setOld(prev)
|
||||
setCur(next)
|
||||
|
||||
if (typeof requestAnimationFrame !== "function") {
|
||||
widen(Math.max(win(), wout()))
|
||||
rootRef?.offsetHeight
|
||||
@@ -138,95 +133,3 @@ export function TextReveal(props: {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextWipe(props: { text?: string; class?: string; delay?: number; animate?: boolean }) {
|
||||
let ref: HTMLSpanElement | undefined
|
||||
let frame: number | undefined
|
||||
let anim: AnimationPlaybackControls | undefined
|
||||
const reduce = useReducedMotion()
|
||||
|
||||
const run = () => {
|
||||
if (props.animate === false) return
|
||||
const el = ref
|
||||
if (!el || !props.text || typeof window === "undefined") return
|
||||
if (reduce()) return
|
||||
|
||||
const mask =
|
||||
typeof CSS !== "undefined" &&
|
||||
(CSS.supports("mask-image", "linear-gradient(to right, black, transparent)") ||
|
||||
CSS.supports("-webkit-mask-image", "linear-gradient(to right, black, transparent)"))
|
||||
|
||||
anim?.stop()
|
||||
if (frame !== undefined && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = undefined
|
||||
}
|
||||
|
||||
el.style.opacity = "0"
|
||||
el.style.filter = "blur(3px)"
|
||||
el.style.transform = "translateX(-0.06em)"
|
||||
|
||||
if (mask) {
|
||||
el.style.maskImage = WIPE_MASK
|
||||
el.style.webkitMaskImage = WIPE_MASK
|
||||
el.style.maskSize = "240% 100%"
|
||||
el.style.webkitMaskSize = "240% 100%"
|
||||
el.style.maskRepeat = "no-repeat"
|
||||
el.style.webkitMaskRepeat = "no-repeat"
|
||||
el.style.maskPosition = "100% 0%"
|
||||
el.style.webkitMaskPosition = "100% 0%"
|
||||
}
|
||||
|
||||
if (typeof requestAnimationFrame !== "function") {
|
||||
clearWipe(el)
|
||||
return
|
||||
}
|
||||
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
const node = ref
|
||||
if (!node) return
|
||||
anim = mask
|
||||
? animate(
|
||||
node,
|
||||
{ opacity: 1, filter: "blur(0px)", transform: "translateX(0)", maskPosition: "0% 0%" },
|
||||
{ ...GROW_SPRING, delay: props.delay ?? 0 },
|
||||
)
|
||||
: animate(
|
||||
node,
|
||||
{ opacity: 1, filter: "blur(0px)", transform: "translateX(0)" },
|
||||
{ ...GROW_SPRING, delay: props.delay ?? 0 },
|
||||
)
|
||||
|
||||
anim?.finished.then(() => {
|
||||
const value = ref
|
||||
if (!value) return
|
||||
clearWipe(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [props.text, props.animate] as const,
|
||||
([text, enabled]) => {
|
||||
if (!text || enabled === false) {
|
||||
if (ref) clearWipe(ref)
|
||||
return
|
||||
}
|
||||
run()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame)
|
||||
anim?.stop()
|
||||
})
|
||||
|
||||
return (
|
||||
<span ref={ref} class={props.class} aria-label={props.text ?? ""}>
|
||||
{props.text ?? "\u00A0"}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user