feat: import fluid portfolio snapshot
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
Reference in New Issue
Block a user