fix scroll smooth
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
|
||||
import gsap from "gsap";
|
||||
@@ -12,6 +12,23 @@ const StrategyCanvas = dynamic(() => import("./StrategyCanvas"), { ssr: false })
|
||||
/** Center of each stage's scroll window (0…1). */
|
||||
const CENTER = (i: number) => i / (N - 1);
|
||||
|
||||
/** True only while a card's own opacity window is open (tiny buffer). Keeps
|
||||
* not-yet-reached stage cards out of the DOM / off the compositor until their
|
||||
* stage is on screen — no future workflow state is rendered before activation.
|
||||
* Visually identical: a card outside its window is opacity:0 regardless. */
|
||||
function useInWindow(mv: MotionValue<number>, threshold = 0.01): boolean {
|
||||
// `mv` is an external mutable store (a MotionValue). useTransform `.set()`s its
|
||||
// output synchronously while the PARENT renders, so a plain `.on("change") -> setState`
|
||||
// updates this component during the parent's render (React warns). useSyncExternalStore
|
||||
// is built for exactly this: it reads a snapshot and reconciles store-changes-during-
|
||||
// render safely. The snapshot is a primitive boolean, so it never re-renders needlessly.
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => mv.on("change", onStoreChange),
|
||||
() => mv.get() > threshold,
|
||||
() => mv.get() > threshold,
|
||||
);
|
||||
}
|
||||
|
||||
/** Persistent top rail: the 5 stages, current one highlighted. */
|
||||
function StageRail({ active }: { active: number }) {
|
||||
return (
|
||||
@@ -45,9 +62,13 @@ function StageCard({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const c = CENTER(i);
|
||||
const opacity = useTransform(scroll, [c - 0.14, c - 0.06, c + 0.06, c + 0.14], [0, 1, 1, 0]);
|
||||
const y = useTransform(scroll, [c - 0.14, c - 0.05], [34, 0]);
|
||||
// Window kept < the 0.25 per-stage span (5 stages) so two adjacent stage cards
|
||||
// never overlap — one stage's card is fully out before the next fades in.
|
||||
const opacity = useTransform(scroll, [c - 0.1, c - 0.05, c + 0.05, c + 0.1], [0, 1, 1, 0]);
|
||||
const y = useTransform(scroll, [c - 0.1, c - 0.05], [34, 0]);
|
||||
const s = STAGES[i];
|
||||
// Only mount the card while its stage's cross-fade window is open.
|
||||
if (!useInWindow(opacity)) return null;
|
||||
return (
|
||||
<motion.div
|
||||
className={`dm-st-card-story is-${side}`}
|
||||
@@ -123,7 +144,9 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
trigger: el,
|
||||
start: "top top",
|
||||
end: "bottom bottom",
|
||||
scrub: 0.5,
|
||||
// Match Workflow 1's responsiveness (0.4) for consistent pacing across all
|
||||
// three workflows — 0.5 made the dolly feel slower / disconnected here.
|
||||
scrub: 0.4,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
const p = self.progress;
|
||||
@@ -135,7 +158,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
if (na !== lastActive) { lastActive = na; setActive(na); }
|
||||
},
|
||||
});
|
||||
const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
|
||||
const refresh = setTimeout(() => ScrollTrigger.refresh(), 120);
|
||||
return () => { clearTimeout(refresh); st.kill(); };
|
||||
}, [scroll]);
|
||||
|
||||
@@ -154,10 +177,14 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
<div className="dm-st-card">
|
||||
{mountScene && (
|
||||
<div className="dm-st-canvas">
|
||||
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} stage={active} />
|
||||
<StrategyCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive && pinState === "pinned"} stage={active} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay mounts only once the section is pinned/activated — its content
|
||||
can never be seen during the approach ("before"), where the sticky sits
|
||||
at the top of the tall section near the previous workflow's seam. */}
|
||||
{pinState !== "before" && (
|
||||
<div className="dm-st-ui">
|
||||
{/* Persistent header */}
|
||||
<motion.div className="dm-st-top" style={{ opacity: headerOpacity }}>
|
||||
@@ -229,6 +256,7 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
</div>
|
||||
</StageCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<style>{styles}</style>
|
||||
@@ -237,8 +265,12 @@ export default function StrategySection({ connected = false }: { connected?: boo
|
||||
}
|
||||
|
||||
const styles = `
|
||||
.dm-st { position: relative; height: 720vh; background: transparent; }
|
||||
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
|
||||
/* Scroll length tuned for pacing: ~100vh per stage (was 144vh) so the 5 stages
|
||||
complete in noticeably less scrolling and the workflow feels tighter / faster.
|
||||
Stage cross-fade windows are progress-based (0…1), so they stay aligned. */
|
||||
.dm-st { position: relative; height: 500vh; background: transparent; }
|
||||
.dm-st-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden;
|
||||
will-change: transform; transform: translateZ(0); backface-visibility: hidden; }
|
||||
.dm-st.is-pinned .dm-st-sticky { position: fixed; top: 0; left: 0; }
|
||||
.dm-st.is-after .dm-st-sticky { position: absolute; top: auto; bottom: 0; }
|
||||
|
||||
@@ -257,6 +289,9 @@ const styles = `
|
||||
.dm-st.is-connected .dm-st-card {
|
||||
top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important;
|
||||
border-radius: 28px 28px 0 0 !important; border-bottom: none !important;
|
||||
/* Flush against the Strategy card below — drop the heavy downward shadow so it
|
||||
doesn't cast a dark band onto that card's top edge (the two read as one container). */
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-st.is-connected .dm-st-card {
|
||||
@@ -397,7 +432,7 @@ const styles = `
|
||||
.dm-st-rail__line { width: 9px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dm-st { height: 640vh; }
|
||||
.dm-st { height: 420vh; }
|
||||
.dm-st-card-story { left: 0 !important; right: 0 !important; margin: 0 auto; width: calc(100% - 28px);
|
||||
bottom: clamp(18px, 4vh, 40px); padding: 15px 16px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user