fix: snap scroll to dialogue typing
This commit is contained in:
@@ -220,6 +220,97 @@ export function initRail(lenis) {
|
||||
return { jump, setActive };
|
||||
}
|
||||
|
||||
/* 滚动守卫:避免停在两轮之间只剩背景的中间态 */
|
||||
export function initRoundAutoFocus(lenis) {
|
||||
const targets = rounds
|
||||
.filter((round) => round.question && !round.flow)
|
||||
.map((round) => ({ round, section: document.getElementById(`r-${round.id}`) }))
|
||||
.filter((target) => target.section);
|
||||
|
||||
if (!targets.length) return () => {};
|
||||
|
||||
let lockUntil = 0;
|
||||
let currentTarget = "";
|
||||
|
||||
const readY = () => (typeof lenis?.scroll === "number" ? lenis.scroll : window.scrollY);
|
||||
|
||||
const pinDistance = (round) => {
|
||||
const match = String(round.pinLength || "").match(/^\+=([\d.]+)%$/);
|
||||
const ratio = match ? Number(match[1]) / 100 : 1.6;
|
||||
return ratio * (window.innerHeight || document.documentElement.clientHeight);
|
||||
};
|
||||
|
||||
const typingOffset = (round) => {
|
||||
const endProgress = Math.min(round.qEnd + 0.025, round.thinkEnd - 0.03);
|
||||
return Math.max(112, Math.round(pinDistance(round) * endProgress));
|
||||
};
|
||||
|
||||
const focusRound = ({ round, section }, sectionStartY = null) => {
|
||||
const now = performance.now();
|
||||
lockUntil = now + 1150;
|
||||
currentTarget = section.id;
|
||||
const baseY = sectionStartY ?? section.getBoundingClientRect().top + readY();
|
||||
const targetY = baseY + typingOffset(round);
|
||||
|
||||
if (lenis) {
|
||||
lenis.scrollTo(targetY, {
|
||||
duration: 0.85,
|
||||
easing: (t) => 1 - Math.pow(1 - t, 3),
|
||||
});
|
||||
} else {
|
||||
window.scrollTo({ top: targetY, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const canFocus = ({ section }) => {
|
||||
const now = performance.now();
|
||||
if (now < lockUntil) return false;
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (section.id === currentTarget && rect.top > -12 && rect.top < 12) return false;
|
||||
return rect.top > 24;
|
||||
};
|
||||
|
||||
const findHalfEnteredRound = () => {
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
const minTop = Math.max(36, vh * 0.08);
|
||||
const maxTop = vh * 0.62;
|
||||
|
||||
return targets.find((target) => {
|
||||
const { section } = target;
|
||||
const rect = section.getBoundingClientRect();
|
||||
if (!canFocus(target)) return false;
|
||||
return rect.top > minTop && rect.top < maxTop && rect.bottom > vh * 0.55;
|
||||
});
|
||||
};
|
||||
|
||||
const checkCurrent = () => {
|
||||
const target = findHalfEnteredRound();
|
||||
if (target) focusRound(target);
|
||||
};
|
||||
|
||||
const triggers = targets.map((section) =>
|
||||
ScrollTrigger.create({
|
||||
trigger: section.section,
|
||||
start: "top 62%",
|
||||
end: "top 8%",
|
||||
onEnter: (self) => {
|
||||
if (canFocus(section)) {
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
focusRound(section, self.start + vh * 0.62);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
window.addEventListener("resize", checkCurrent);
|
||||
window.setTimeout(checkCurrent, 250);
|
||||
|
||||
return () => {
|
||||
triggers.forEach((trigger) => trigger.kill());
|
||||
window.removeEventListener("resize", checkCurrent);
|
||||
};
|
||||
}
|
||||
|
||||
/* 降级:reduced motion 直接展示全部内容 */
|
||||
export function applyReducedMotion() {
|
||||
rounds
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyReducedMotion,
|
||||
initChoreography,
|
||||
initRail,
|
||||
initRoundAutoFocus,
|
||||
initSmoothScroll,
|
||||
playBoot,
|
||||
} from "./choreography";
|
||||
@@ -28,6 +29,7 @@ renderRail(document.getElementById("session-rail"), rail.jump);
|
||||
|
||||
if (!reduced) {
|
||||
initChoreography(mind, rail.setActive);
|
||||
initRoundAutoFocus(lenis);
|
||||
playBoot(mind);
|
||||
} else {
|
||||
applyReducedMotion();
|
||||
|
||||
Reference in New Issue
Block a user