fix: snap scroll to dialogue typing

This commit is contained in:
湛兮
2026-06-11 15:38:25 +08:00
parent 4c63c00a18
commit 4ae452336c
2 changed files with 93 additions and 0 deletions
+91
View File
@@ -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
+2
View File
@@ -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();