SFS/components/ladderclimber.tsx
2025-12-19 12:47:57 +05:30

188 lines
6.0 KiB
TypeScript

"use client"
import { useEffect, useRef } from "react"
// import "@lottiefiles/dotlottie-wc" // Removed static import to fix SSR error
const STOP_DELAY = 700
const LADDER_OFFSET_BOTTOM = 80
const LADDER_OFFSET_TOP = 20 // Buffer to keep character from going off-screen at the top
export default function LadderClimb() {
const charRef = useRef<HTMLDivElement>(null)
const ladderRef = useRef<HTMLDivElement>(null)
const idleRef = useRef<HTMLDivElement>(null)
const climbRef = useRef<HTMLDivElement>(null)
const jumpRef = useRef<HTMLDivElement>(null)
const ladderMaxTravel = useRef(0)
const activeState = useRef<"idle" | "climbing" | "jumping">("idle")
const scrollTimeout = useRef<NodeJS.Timeout | null>(null)
const isFalling = useRef(false)
const switchAnimation = (newState: "idle" | "climbing" | "jumping") => {
if (activeState.current === newState) return
activeState.current = newState
if (!idleRef.current || !climbRef.current || !jumpRef.current) return
idleRef.current.style.display = "none"
climbRef.current.style.display = "none"
jumpRef.current.style.display = "none"
if (newState === "idle") idleRef.current.style.display = "block"
else if (newState === "climbing") climbRef.current.style.display = "block"
else if (newState === "jumping") jumpRef.current.style.display = "block"
}
const triggerFall = () => {
if (!charRef.current || isFalling.current) return
const style = window.getComputedStyle(charRef.current)
const matrix = new DOMMatrix(style.transform)
const currentVisualY = Math.abs(matrix.m42)
if (currentVisualY < 5) {
switchAnimation("idle")
return
}
isFalling.current = true
switchAnimation("jumping")
charRef.current.classList.add("falling")
charRef.current.style.transform = `translateY(0px)`
const handleLanding = (e: TransitionEvent) => {
if (e.propertyName !== "transform") return
isFalling.current = false
charRef.current?.classList.remove("falling")
// Force idle animation upon landing
switchAnimation("idle")
charRef.current?.removeEventListener("transitionend", handleLanding)
}
charRef.current.addEventListener("transitionend", handleLanding)
}
const updateDimensions = () => {
if (ladderRef.current) {
// Logic: The character can only travel the height of the ladder minus the offsets
ladderMaxTravel.current = ladderRef.current.clientHeight - LADDER_OFFSET_BOTTOM - LADDER_OFFSET_TOP
}
}
useEffect(() => {
import("@lottiefiles/dotlottie-wc")
updateDimensions()
window.addEventListener("resize", updateDimensions)
const handleScroll = () => {
if (!charRef.current) return
const scrollTop = window.scrollY
const docHeight = document.body.scrollHeight - window.innerHeight
let progress = docHeight > 0 ? scrollTop / docHeight : 0
// Clamp progress between 0 and 1
progress = Math.max(0, Math.min(1, progress))
// Calculate Target Y based on the ladder's actual height constraint
const targetY = progress * ladderMaxTravel.current
// Interrupt fall if user starts scrolling
if (isFalling.current) {
isFalling.current = false
charRef.current.classList.remove("falling")
}
// Apply transformation (constrained to the top of the ladder)
charRef.current.style.transform = `translateY(-${targetY}px)`
// Set climbing animation
if (activeState.current !== "climbing") switchAnimation("climbing")
// Stop detection
if (scrollTimeout.current) clearTimeout(scrollTimeout.current)
scrollTimeout.current = setTimeout(() => {
if (targetY > 2) {
triggerFall()
} else {
switchAnimation("idle")
}
}, STOP_DELAY)
}
window.addEventListener("scroll", handleScroll, { passive: true })
handleScroll()
return () => {
window.removeEventListener("scroll", handleScroll)
window.removeEventListener("resize", updateDimensions)
if (scrollTimeout.current) clearTimeout(scrollTimeout.current)
}
}, [])
return (
<>
<div
ref={ladderRef}
style={{
position: "fixed",
right: 10,
top: 80, // Top margin of the ladder rail
height: "calc(100vh - 80px)", // Ladder length
width: "40px",
display: "flex",
justifyContent: "center",
alignItems: "flex-end",
zIndex: 999,
pointerEvents: "none"
}}
>
{/* The Visual Ladder */}
<div
style={{
height: "100%",
width: "30px",
background: "repeating-linear-gradient(to bottom, #444 0, #444 4px, transparent 4px, transparent 30px)",
borderLeft: "4px solid #444",
borderRight: "4px solid #444",
zIndex: 1
}}
/>
{/* The Character Container */}
<div
ref={charRef}
style={{
position: "absolute",
bottom: 0, // Starts at the bottom of the ladderRef
left: "50%",
marginLeft: "-90px",
zIndex: 10,
willChange: "transform"
}}
>
<style>{`
.falling {
transition: transform 0.7s cubic-bezier(0.55, 0.055, 0.675, 0.19) !important;
}
`}</style>
<div ref={idleRef} style={{ display: 'block' }}>
<dotlottie-wc src="https://lottie.host/13145c5f-8acc-4ea9-8842-58a458ef5553/b1W7KoG3lt.lottie" autoplay loop style={{ width: '180px' }} />
</div>
<div ref={climbRef} style={{ display: 'none' }}>
<dotlottie-wc src="https://lottie.host/97e44afd-2200-44a5-8d60-8db614c54362/cqzrUWVWdo.lottie" autoplay loop style={{ width: '180px' }} />
</div>
<div ref={jumpRef} style={{ display: 'none' }}>
<dotlottie-wc src="https://lottie.host/5fc8dea4-e109-4c3b-813d-afd4da96973b/fdslVCb92l.lottie" autoplay style={{ width: '180px' }} />
</div>
</div>
</div>
</>
)
}