fix cv animations
This commit is contained in:
@@ -20,6 +20,25 @@ ScrollTrigger.config({ ignoreMobileResize: true })
|
||||
// element and mount the GSDevTools timeline scrubber. Handy for seeing exactly
|
||||
// where each card's enter/exit lines sit relative to the viewport.
|
||||
export const GSAP_DEBUG = false
|
||||
|
||||
export function nearestScroller(el: Element): Element | Window {
|
||||
let node: Element | null = el.parentElement
|
||||
while (node) {
|
||||
if (node.getAttribute('data-slot') === 'scroll-area-viewport') {
|
||||
const viewport = node as HTMLElement
|
||||
const rect = viewport.getBoundingClientRect()
|
||||
const hasUsableBox = rect.width > 0 && rect.height > 0
|
||||
const canScroll =
|
||||
viewport.scrollHeight > viewport.clientHeight ||
|
||||
viewport.scrollWidth > viewport.clientWidth
|
||||
|
||||
if (hasUsableBox && canScroll) return viewport
|
||||
}
|
||||
node = node.parentElement
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
const GsapContext = createContext<{
|
||||
// Add a real animation (with its own duration) to the entrance timeline.
|
||||
addAnimation: (
|
||||
@@ -47,14 +66,24 @@ export function useGsapContext() {
|
||||
export const useTimeLine = (dep:any,all?:boolean) => {
|
||||
const gsapContext = useGsapContext()
|
||||
useEffect(() => {
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][useTimeLine:effect]", {
|
||||
hasDep: !!dep,
|
||||
isArray: dep instanceof Array,
|
||||
length: dep instanceof Array ? dep.length : undefined,
|
||||
all,
|
||||
})
|
||||
}
|
||||
if (dep instanceof Array && all) {
|
||||
let acc = true;
|
||||
let allDepsSatisfied = dep.reduce((p,c) => c !== undefined && p ,acc )
|
||||
if (allDepsSatisfied) {
|
||||
if (GSAP_DEBUG) console.log("[cv-debug][useTimeLine:resume-all]")
|
||||
gsapContext?.resumeTimeline()
|
||||
}
|
||||
} else {
|
||||
if (dep) {
|
||||
if (GSAP_DEBUG) console.log("[cv-debug][useTimeLine:resume]")
|
||||
gsapContext?.resumeTimeline()
|
||||
}
|
||||
}
|
||||
@@ -100,7 +129,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
devToolsCreated.current = true
|
||||
GSDevTools.create({ animation: tl.current })
|
||||
}
|
||||
return () => { console.log("gsap cleanup") }
|
||||
return () => { if (GSAP_DEBUG) console.log("gsap cleanup") }
|
||||
})
|
||||
|
||||
// Handoff: fire registered callbacks once, when the entrance finishes.
|
||||
@@ -108,11 +137,19 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
const readyCbs = useRef<Array<() => void>>([])
|
||||
const fireReady = useCallback(() => {
|
||||
if (readyFired.current) return
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:ready]", {
|
||||
callbacks: readyCbs.current.length,
|
||||
duration: tl.current?.duration(),
|
||||
progress: tl.current?.progress(),
|
||||
})
|
||||
}
|
||||
readyFired.current = true
|
||||
readyCbs.current.forEach((cb) => cb())
|
||||
readyCbs.current = []
|
||||
},[])
|
||||
const onReady = useCallback((cb: () => void) => {
|
||||
if (GSAP_DEBUG) console.log("[cv-debug][gsap:onReady]", { readyFired: readyFired.current })
|
||||
if (readyFired.current) cb()
|
||||
else readyCbs.current.push(cb)
|
||||
},[])
|
||||
@@ -122,14 +159,43 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
// entrance already played). Parking a tween in a finished, paused timeline
|
||||
// would freeze it at its from-state, so once the entrance is done let the
|
||||
// (live) tween play on its own instead.
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:addAnimation]", {
|
||||
position,
|
||||
readyFired: readyFired.current,
|
||||
durationBefore: tl.current?.duration(),
|
||||
})
|
||||
}
|
||||
if (readyFired.current) return
|
||||
tl.current?.add(animation, position);
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:addAnimation:done]", {
|
||||
position,
|
||||
durationAfter: tl.current?.duration(),
|
||||
children: tl.current?.getChildren(false, true, true).length,
|
||||
})
|
||||
}
|
||||
},[])
|
||||
const schedule = useCallback((fn: () => void, position: gsap.Position) => {
|
||||
// Same late-arrival case: a callback added past the playhead never fires, so
|
||||
// run the reveal immediately once the entrance has finished.
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:schedule]", {
|
||||
position,
|
||||
readyFired: readyFired.current,
|
||||
durationBefore: tl.current?.duration(),
|
||||
childrenBefore: tl.current?.getChildren(false, true, true).length,
|
||||
})
|
||||
}
|
||||
if (readyFired.current) { fn(); return }
|
||||
tl.current?.add(fn, position)
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:schedule:done]", {
|
||||
position,
|
||||
durationAfter: tl.current?.duration(),
|
||||
childrenAfter: tl.current?.getChildren(false, true, true).length,
|
||||
})
|
||||
}
|
||||
},[])
|
||||
|
||||
// Throttle ScrollTrigger.refresh() to once per frame so the ResizeObserver
|
||||
@@ -148,6 +214,12 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
const resizeObserver = useRef<ResizeObserver | null>(null)
|
||||
|
||||
const resetTimeline = useCallback(() => {
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:reset]", {
|
||||
duration: tl.current?.duration(),
|
||||
progress: tl.current?.progress(),
|
||||
})
|
||||
}
|
||||
tl.current?.kill()
|
||||
tl.current?.revert()
|
||||
ScrollTrigger.getAll().forEach(st => st.kill())
|
||||
@@ -161,7 +233,19 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const resumeTimeline = useCallback(() => {
|
||||
const t = tl.current
|
||||
if (!t) return
|
||||
if (!t) {
|
||||
if (GSAP_DEBUG) console.log("[cv-debug][gsap:resume:skip-no-timeline]")
|
||||
return
|
||||
}
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:resume:start]", {
|
||||
duration: t.duration(),
|
||||
progress: t.progress(),
|
||||
paused: t.paused(),
|
||||
readyFired: readyFired.current,
|
||||
children: t.getChildren(false, true, true).length,
|
||||
})
|
||||
}
|
||||
// When the orchestrated entrance finishes, hand off to scroll control and
|
||||
// realign triggers against the now-settled layout.
|
||||
t.eventCallback("onComplete", () => { fireReady(); ScrollTrigger.refresh() })
|
||||
@@ -185,6 +269,13 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
t.resume()
|
||||
if (GSAP_DEBUG) {
|
||||
console.log("[cv-debug][gsap:resume:after]", {
|
||||
duration: t.duration(),
|
||||
progress: t.progress(),
|
||||
paused: t.paused(),
|
||||
})
|
||||
}
|
||||
},[getScroller, fireReady, scheduleRefresh])
|
||||
|
||||
// Fonts/markdown/images loading also change content height after the triggers
|
||||
|
||||
Reference in New Issue
Block a user