animation

This commit is contained in:
Mohammad Yaseen 2025-12-19 12:47:57 +05:30
parent 5abc04c382
commit 98c7939fad
11 changed files with 938 additions and 86 deletions

View File

@ -124,9 +124,17 @@
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
scrollbar-width: none;
-ms-overflow-style: none;
} }
*::-webkit-scrollbar {
display: none;
}
html,
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
overflow-x: hidden;
} }
} }

View File

@ -15,15 +15,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="en"> <html lang="en">
<body <body
className="font-sans antialiased scrollbar-hide" className="font-sans antialiased scrollbar-hide"
style={{ fontFamily: "DIN Alternate, sans-serif", scrollbarWidth: "none", msOverflowStyle: "none" }} style={{ fontFamily: "DIN Alternate, sans-serif" }}
> >
<style>{`
html::-webkit-scrollbar, <Navbar />
body::-webkit-scrollbar {
display: none;
}
`}</style>
<Navbar />
{/* LadderClimb is now global */} {/* LadderClimb is now global */}
<LadderClimb /> <LadderClimb />

View File

@ -5,7 +5,6 @@ import Testimonials from "@/components/testimonials"
import OurOfferings from "@/components/our-offerings" import OurOfferings from "@/components/our-offerings"
import TrustedBy from "@/components/trusted-by" import TrustedBy from "@/components/trusted-by"
import Footer from "@/components/footer" import Footer from "@/components/footer"
import LadderClimb from "@/components/ladderclimber"
export default function Home() { export default function Home() {
return ( return (

9
components/dotlottie.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import * as React from 'react';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'dotlottie-wc': any;
}
}
}

View File

@ -1,62 +1,118 @@
"use client" "use client"
import { useEffect, useRef } from "react"
// import "@lottiefiles/dotlottie-wc" // Removed static import to fix SSR error
import React, { useEffect, useRef, useState } from "react" 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() { export default function LadderClimb() {
const charRef = useRef<HTMLDivElement>(null) const charRef = useRef<HTMLDivElement>(null)
const ladderRef = useRef<HTMLDivElement>(null) const ladderRef = useRef<HTMLDivElement>(null)
const [animation, setAnimation] = useState<"idle" | "climbing">("idle") const idleRef = useRef<HTMLDivElement>(null)
const [visible, setVisible] = useState(true) const climbRef = useRef<HTMLDivElement>(null)
const jumpRef = useRef<HTMLDivElement>(null)
const ladderHeight = useRef(0) const ladderMaxTravel = useRef(0)
const scrollTimeout = useRef<any>(null) 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 = () => { const updateDimensions = () => {
if (ladderRef.current) { if (ladderRef.current) {
ladderHeight.current = ladderRef.current.clientHeight // 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
if (charRef.current) {
charRef.current.style.transform = `translateY(0px)`
} }
} }
useEffect(() => { useEffect(() => {
import("@lottiefiles/dotlottie-wc")
updateDimensions() updateDimensions()
setAnimation("idle")
window.addEventListener("resize", updateDimensions) window.addEventListener("resize", updateDimensions)
const handleScroll = () => { const handleScroll = () => {
if (!charRef.current || !ladderRef.current) return if (!charRef.current) return
// show ladder & climbing animation
setVisible(true)
setAnimation("climbing")
const scrollTop = window.scrollY const scrollTop = window.scrollY
const docHeight = document.body.scrollHeight const docHeight = document.body.scrollHeight - window.innerHeight
const viewportHeight = window.innerHeight let progress = docHeight > 0 ? scrollTop / docHeight : 0
let progress = scrollTop / (docHeight - viewportHeight) // Clamp progress between 0 and 1
progress = Math.max(0, Math.min(1, progress)) progress = Math.max(0, Math.min(1, progress))
const topOffset = 174 // Calculate Target Y based on the ladder's actual height constraint
const bottomOffset = 30 const targetY = progress * ladderMaxTravel.current
const maxClimb = ladderHeight.current - topOffset - bottomOffset
const y = progress * maxClimb // Interrupt fall if user starts scrolling
charRef.current.style.transform = `translateY(-${y}px)` if (isFalling.current) {
isFalling.current = false
charRef.current.classList.remove("falling")
}
// hide smoothly after scroll stops // 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) if (scrollTimeout.current) clearTimeout(scrollTimeout.current)
scrollTimeout.current = setTimeout(() => { scrollTimeout.current = setTimeout(() => {
setAnimation("idle") if (targetY > 2) {
setVisible(false) triggerFall()
}, 500) } else {
switchAnimation("idle")
}
}, STOP_DELAY)
} }
window.addEventListener("scroll", handleScroll) window.addEventListener("scroll", handleScroll, { passive: true })
handleScroll()
return () => { return () => {
window.removeEventListener("scroll", handleScroll) window.removeEventListener("scroll", handleScroll)
@ -67,77 +123,65 @@ export default function LadderClimb() {
return ( return (
<> <>
{/* Ladder container */}
<div <div
ref={ladderRef} ref={ladderRef}
style={{ style={{
position: "fixed", position: "fixed",
right: 0, right: 10,
top: 80, top: 80, // Top margin of the ladder rail
height: "100vh", height: "calc(100vh - 80px)", // Ladder length
width: "100px", width: "40px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "flex-end", alignItems: "flex-end",
zIndex: 999, zIndex: 999,
pointerEvents: "none"
// smooth hide / show
opacity: visible ? 1 : 0,
transform: visible ? "translateX(0)" : "translateX(40px)",
pointerEvents: visible ? "auto" : "none",
transition: "opacity 0.8s ease, transform 0.8s ease"
}} }}
> >
{/* Ladder background */} {/* The Visual Ladder */}
<div <div
style={{ style={{
height: "100%", height: "100%",
width: "40px", width: "30px",
background: background: "repeating-linear-gradient(to bottom, #444 0, #444 4px, transparent 4px, transparent 30px)",
"repeating-linear-gradient(to bottom, #666 0, #666 8px, transparent 8px, transparent 30px)", borderLeft: "4px solid #444",
borderLeft: "8px solid #666", borderRight: "4px solid #444",
borderRight: "8px solid #666",
zIndex: 1 zIndex: 1
}} }}
/> />
{/* Character */} {/* The Character Container */}
<div <div
ref={charRef} ref={charRef}
style={{ style={{
position: "absolute", position: "absolute",
bottom: 45, bottom: 0, // Starts at the bottom of the ladderRef
left: "50%", left: "50%",
marginLeft: "-100px", marginLeft: "-90px",
width: "200px",
zIndex: 10, zIndex: 10,
willChange: "transform", willChange: "transform"
transition: "transform 0.15s linear"
}} }}
> >
{/* CLIMBING */} <style>{`
{animation === "climbing" && ( .falling {
<img transition: transform 0.7s cubic-bezier(0.55, 0.055, 0.675, 0.19) !important;
src="./climber21.png" }
alt="Climbing" `}</style>
style={{ width: "400px", height: "200px" }}
/>
)}
{/* IDLE */} <div ref={idleRef} style={{ display: 'block' }}>
{animation === "idle" && ( <dotlottie-wc src="https://lottie.host/13145c5f-8acc-4ea9-8842-58a458ef5553/b1W7KoG3lt.lottie" autoplay loop style={{ width: '180px' }} />
<img </div>
src="./Idle3.png"
alt="Idle" <div ref={climbRef} style={{ display: 'none' }}>
style={{ <dotlottie-wc src="https://lottie.host/97e44afd-2200-44a5-8d60-8db614c54362/cqzrUWVWdo.lottie" autoplay loop style={{ width: '180px' }} />
marginLeft: "10px", </div>
width: "400px",
height: "200px" <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>
</div> </div>
</> </>
) )
} }

View File

@ -46,6 +46,7 @@ interface ContentItem {
export default function MainHero() { export default function MainHero() {
const [content, setContent] = useState<ContentItem[]>([]) const [content, setContent] = useState<ContentItem[]>([])
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
console.log("content", content)
// Fetch content from API // Fetch content from API
useEffect(() => { useEffect(() => {

793
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
"@lottiefiles/dotlottie-wc": "^0.8.11",
"@radix-ui/react-accordion": "1.2.2", "@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4", "@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1", "@radix-ui/react-aspect-ratio": "1.1.1",
@ -40,6 +41,9 @@
"@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6", "@radix-ui/react-tooltip": "1.1.6",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.2",
"@types/three": "^0.182.0",
"@vercel/analytics": "latest", "@vercel/analytics": "latest",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -61,6 +65,7 @@
"sonner": "^1.7.4", "sonner": "^1.7.4",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "3.25.76" "zod": "3.25.76"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB