feat: import fluid portfolio snapshot

This commit is contained in:
湛兮
2026-06-11 12:24:52 +08:00
parent f7b529b1bd
commit 22baa715fd
22 changed files with 2955 additions and 6813 deletions
+236
View File
@@ -0,0 +1,236 @@
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
import { rounds } from "./dialogue";
gsap.registerPlugin(ScrollTrigger);
/* Lenis 平滑滚动 + ScrollTrigger 集成 */
export function initSmoothScroll() {
const lenis = new Lenis({ lerp: 0.1 });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
return lenis;
}
/* 打字机:scrub 时间轴上的离散字符 reveal */
function addTypewriter(tl, textEl, caretEl, fullText, start, duration) {
const proxy = { p: 0 };
tl.to(
proxy,
{
p: 1,
duration,
ease: `steps(${fullText.length})`,
onUpdate() {
const n = Math.round(proxy.p * fullText.length);
textEl.textContent = fullText.slice(0, n);
if (caretEl) caretEl.style.opacity = proxy.p >= 1 ? "0" : "1";
},
},
start
);
}
/* 开场(时间驱动,非滚动)*/
export function playBoot(mind) {
const boot = document.querySelector("#r-boot");
const typedLine = boot.querySelector(".typed-line");
const items = boot.querySelectorAll(".gen");
gsap.set(boot.querySelector(".answer"), { opacity: 1 });
gsap.set(items, { y: 26, autoAlpha: 0 });
const text = "这是我的数字思维体,向它提问即可。";
const proxy = { p: 0 };
const tl = gsap.timeline({ delay: 0.3 });
tl.call(() => mind.setState("thinking"))
.to(proxy, {
p: 1,
duration: 1.4,
ease: `steps(${text.length})`,
onUpdate: () => {
typedLine.textContent = text.slice(0, Math.round(proxy.p * text.length));
},
}, 0.5)
.call(() => mind.setState("answering"), null, 1.9)
.to(items, {
y: 0,
autoAlpha: 1,
duration: 0.7,
ease: "expo.out",
stagger: { each: 0.09, from: "start" },
}, 2.0)
.call(() => mind.setState("idle"), null, 3.4);
return tl;
}
/* 轮次编排:短轮 pin + scrub;长轮(flow)流式 reveal */
export function initChoreography(mind, setActive = () => {}) {
rounds
.filter((r) => r.question)
.forEach((round) => {
const section = document.getElementById(`r-${round.id}`);
const inner = section.querySelector(".round-inner");
const qText = section.querySelector(".q-text");
const qCaret = section.querySelector(".q-caret");
const thinking = section.querySelector(".thinking");
const answer = section.querySelector(".answer");
const items = section.querySelectorAll(".gen");
gsap.set(items, { y: 34, autoAlpha: 0 });
if (round.flow) {
buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive });
return;
}
const { qEnd, thinkEnd, staggerEach } = round;
const burstEnd = 0.8;
const tl = gsap.timeline({
defaults: { ease: "none" },
scrollTrigger: {
trigger: section,
start: "top top",
end: round.pinLength,
scrub: 0.65,
pin: true,
anticipatePin: 1,
onToggle(self) {
if (self.isActive) setActive(round.id);
},
onEnter: () => {
mind.setSide(round.side);
mind.setPalette(round.palette);
},
onEnterBack: () => {
mind.setSide(round.side);
mind.setPalette(round.palette);
},
onLeave: () => mind.setState("idle"),
onLeaveBack: () => mind.setState("idle"),
onUpdate(self) {
const p = self.progress;
if (p < qEnd) mind.setState("idle");
else if (p < thinkEnd) mind.setState("thinking");
else if (p < burstEnd) mind.setState("answering");
else mind.setState("idle");
},
},
});
// 1. 提问打字
addTypewriter(tl, qText, qCaret, round.question, 0, qEnd);
// 2. 思考 shimmer(刻意停顿 = 非线性蓄力)
tl.to(thinking, { opacity: 1, duration: 0.03 }, qEnd)
.to(thinking, { opacity: 0, duration: 0.04 }, thinkEnd - 0.04);
// 3. 爆发生成:不等间隔 stagger 模拟 token 到达
tl.to(answer, { opacity: 1, duration: 0.02 }, thinkEnd);
tl.to(
items,
{
y: 0,
autoAlpha: 1,
duration: 0.1,
ease: "expo.out",
stagger: { each: staggerEach, from: "start" },
},
thinkEnd + 0.02
);
// 4. 余韵:内容微视差上浮
tl.to(inner, { y: -26, duration: 0.2, ease: "power1.in" }, burstEnd);
// 时间轴总长归一
tl.to({}, { duration: 0.001 }, 1);
});
}
/* 长内容轮:提问/思考用入场 scrub,内容块各自进入视口时「生成」 */
function buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive }) {
gsap.set(answer, { opacity: 1 });
const qTl = gsap.timeline({
defaults: { ease: "none" },
scrollTrigger: {
trigger: section,
start: "top 78%",
end: "top 18%",
scrub: 0.5,
},
});
addTypewriter(qTl, qText, qCaret, round.question, 0, 0.55);
qTl.to(thinking, { opacity: 1, duration: 0.08 }, 0.6)
.to(thinking, { opacity: 0, duration: 0.1 }, 0.9);
items.forEach((item, i) => {
gsap.to(item, {
y: 0,
autoAlpha: 1,
duration: 0.8,
delay: (i % 3) * 0.09, // 不等间隔,模拟 token 到达
ease: "expo.out",
scrollTrigger: { trigger: item, start: "top 88%" },
});
});
ScrollTrigger.create({
trigger: section,
start: "top 60%",
end: "bottom 55%",
onToggle(self) {
if (self.isActive) {
setActive(round.id);
mind.setSide(round.side);
mind.setPalette(round.palette);
} else {
mind.setState("idle");
}
},
onUpdate(self) {
mind.setState(self.progress < 0.1 ? "thinking" : "answering");
},
});
}
/* 进度轨:高亮由编排回调驱动,这里只负责节点与跳转 */
export function initRail(lenis) {
const setActive = (id) => {
document.querySelectorAll(".rail-node").forEach((n) => {
n.classList.toggle("active", n.dataset.round === id);
});
};
ScrollTrigger.create({
trigger: "#r-boot",
start: "top top",
end: "bottom 40%",
onToggle(self) {
if (self.isActive) setActive("boot");
},
});
const jump = (id) => {
const target = document.getElementById(`r-${id}`);
if (!target) return;
if (lenis) lenis.scrollTo(target, { offset: 2, duration: 1.2 });
else target.scrollIntoView({ behavior: "smooth" });
};
return { jump, setActive };
}
/* 降级:reduced motion 直接展示全部内容 */
export function applyReducedMotion() {
rounds
.filter((r) => r.question)
.forEach((round) => {
const section = document.getElementById(`r-${round.id}`);
section.querySelector(".q-text").textContent = round.question;
const answer = section.querySelector(".answer");
answer.style.opacity = "1";
});
const boot = document.querySelector("#r-boot");
boot.querySelector(".typed-line").textContent = "这是我的数字思维体。";
document.querySelectorAll(".gen").forEach((n) => (n.style.opacity = "1"));
}