为首页 V1 和 V2 增加高级滚动动画

This commit is contained in:
1
2026-08-13 12:55:40 +08:00
parent 85b41ecb2a
commit 31761f2f78
3 changed files with 336 additions and 2 deletions

View File

@@ -32,4 +32,15 @@ Date: 2026-08-13
- P3: customer may prefer a different hero photograph after comparing all three versions; this is a content choice, not a layout defect.
- P3: final English line breaks can be tuned after the customer confirms which direction to pursue.
## Motion pass — 2026-08-13
- V1 now has a high-impact entrance sequence, hero parallax, staggered business panels, alternating experience reveals, route-card wipes, team/photo entrances, gallery choreography, comparison-table reveal, and a closing CTA sequence.
- V2 now has a slower cinematic entrance, hero push-in and parallax, masked editorial-card reveals, feature-story image wipe, route and field-collage choreography, layered team-photo entrance, and a closing landscape push-in.
- Motion is scoped independently to each version; Classic remains unchanged.
- Browser timing inspection confirmed hero elements move from hidden/transformed states to their completed state in both versions.
- Scroll-trigger inspection confirmed V1 content bands and V2 editorial cards reveal and settle correctly.
- Browser console errors and warnings: 0 on both V1 and V2.
- Broken images: 0. Horizontal overflow at desktop: 0.
- Reduced-motion behavior is preserved: scripted movement is skipped and CSS animation duration is constrained.
final result: passed

View File

@@ -54,6 +54,16 @@
.oc-v2-hero-scroll { position: absolute; right: 0; bottom: 0; width: 76px; height: 76px; display: grid; place-items: center; background: var(--v2-cyan); color: var(--v2-navy); }
.oc-v2-hero-scroll svg { transform: rotate(90deg); }
.oc-home-v2 [style*="clip-path"],
.oc-home-v1 [style*="clip-path"] { will-change: clip-path, transform, opacity; }
.oc-v2-hero > img,
.oc-v2-experience-card > img,
.oc-v2-route-card img,
.oc-v2-field-collage img,
.oc-v2-final > img,
.oc-home-v1 .oc-experience-band img,
.oc-home-v1 .oc-home-safari-image { will-change: transform; }
.oc-v2-discover,
.oc-v2-routes,
.oc-v2-field { padding: clamp(100px, 11vw, 175px) clamp(24px, 5.5vw, 88px); }
@@ -150,6 +160,22 @@
.oc-home-v2 :is(a, button):focus-visible { outline: 3px solid var(--v2-coral); outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) {
.oc-home-v2 *,
.oc-home-v1 * {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
.oc-home-v2 [style],
.oc-home-v1 [style] {
translate: none !important;
rotate: none !important;
scale: none !important;
}
}
@media (max-width: 960px) {
.oc-v2-hero-copy { width: calc(100% - 48px); }
.oc-v2-section-intro,

View File

@@ -2041,6 +2041,7 @@ function selectPage(path: string) {
function EditorialHomePage() {
const { content, language } = useTravelSite();
const editorialRef = useRef<HTMLElement | null>(null);
const home = content.home;
const editorialExperiences = [
{ number: '01', title: pickText('山野', 'Wild Karst', language), eyebrow: pickText('徒步 · 骑行 · 洞穴', 'Hike · Cycle · Caves', language), image: '/client-assets/karst-hike-ridge.jpg', href: '/experiences/karst-hiking/' },
@@ -2057,8 +2058,157 @@ function EditorialHomePage() {
'/client-assets/culinary-long-table-banquet.jpg',
];
useGSAP(
() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const heroTimeline = gsap.timeline({ defaults: { ease: 'power3.out' } });
heroTimeline
.from('.oc-v2-hero > img', { scale: 1.13, autoAlpha: 0.4, duration: 1.8, ease: 'power2.out' })
.from('.oc-v2-hero-copy > p', { y: 20, autoAlpha: 0, duration: 0.6 }, '-=1.18')
.from('.oc-v2-hero h1', { y: 90, autoAlpha: 0, duration: 1.05 }, '-=.85')
.from('.oc-v2-hero-bottom > *', { y: 32, autoAlpha: 0, duration: 0.7, stagger: 0.12 }, '-=.58')
.from('.oc-v2-hero-scroll', { scale: 0.65, autoAlpha: 0, duration: 0.55 }, '-=.45');
gsap.to('.oc-v2-hero > img', {
yPercent: 10,
scale: 1.08,
ease: 'none',
scrollTrigger: { trigger: '.oc-v2-hero', start: 'top top+=90', end: 'bottom top', scrub: 0.7 },
});
gsap.to('.oc-v2-hero-scroll svg', {
y: 8,
duration: 1.15,
repeat: -1,
yoyo: true,
ease: 'sine.inOut',
});
gsap.utils.toArray<HTMLElement>('.oc-v2-section-intro, .oc-v2-field-heading').forEach((heading) => {
gsap.from(heading.children, {
y: 58,
autoAlpha: 0,
duration: 0.9,
stagger: 0.12,
ease: 'power3.out',
scrollTrigger: { trigger: heading, start: 'top 78%', once: true },
});
});
gsap.from('.oc-v2-experience-card', {
y: 86,
clipPath: 'inset(0 0 100% 0)',
duration: 1.05,
stagger: 0.11,
ease: 'power4.out',
scrollTrigger: { trigger: '.oc-v2-experience-grid', start: 'top 76%', once: true },
});
gsap.from('.oc-v2-experience-card > img', {
scale: 1.18,
duration: 1.5,
stagger: 0.11,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-experience-grid', start: 'top 76%', once: true },
});
gsap.from('.oc-v2-feature-image', {
clipPath: 'inset(0 100% 0 0)',
duration: 1.25,
ease: 'power4.inOut',
scrollTrigger: { trigger: '.oc-v2-feature', start: 'top 72%', once: true },
});
gsap.from('.oc-v2-feature-image img', {
scale: 1.16,
duration: 1.6,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-feature', start: 'top 72%', once: true },
});
gsap.from('.oc-v2-feature-copy > *', {
y: 58,
autoAlpha: 0,
duration: 0.82,
stagger: 0.1,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-feature-copy', start: 'top 74%', once: true },
});
gsap.from('.oc-v2-route-card', {
y: 90,
clipPath: 'inset(100% 0 0 0)',
duration: 1.08,
stagger: 0.13,
ease: 'power4.out',
scrollTrigger: { trigger: '.oc-v2-route-grid', start: 'top 76%', once: true },
});
gsap.from('.oc-v2-route-card img', {
scale: 1.16,
duration: 1.55,
stagger: 0.13,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-route-grid', start: 'top 76%', once: true },
});
gsap.from('.oc-v2-field-collage figure, .oc-v2-field-note', {
y: 76,
scale: 0.96,
autoAlpha: 0,
duration: 0.9,
stagger: 0.1,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-field-collage', start: 'top 76%', once: true },
});
gsap.to('.oc-v2-field-collage figure.is-1 img', {
yPercent: 10,
scale: 1.08,
ease: 'none',
scrollTrigger: { trigger: '.oc-v2-field-collage', start: 'top bottom', end: 'bottom top', scrub: 0.8 },
});
gsap.from('.oc-v2-local-media img:first-child', {
clipPath: 'inset(0 0 100% 0)',
scale: 1.1,
duration: 1.25,
ease: 'power4.out',
scrollTrigger: { trigger: '.oc-v2-local', start: 'top 72%', once: true },
});
gsap.from('.oc-v2-local-media img:last-child', {
x: 70,
y: 60,
rotate: 2,
autoAlpha: 0,
duration: 0.95,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-local', start: 'top 65%', once: true },
});
gsap.from('.oc-v2-local-copy > *', {
y: 52,
autoAlpha: 0,
duration: 0.78,
stagger: 0.09,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-local-copy', start: 'top 74%', once: true },
});
gsap.from('.oc-v2-final > div > *', {
y: 66,
autoAlpha: 0,
duration: 0.9,
stagger: 0.13,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-v2-final', start: 'top 66%', once: true },
});
gsap.fromTo('.oc-v2-final > img', { scale: 1.06 }, {
scale: 1.16,
ease: 'none',
scrollTrigger: { trigger: '.oc-v2-final', start: 'top bottom', end: 'bottom top', scrub: 0.9 },
});
},
{ scope: editorialRef, dependencies: [language], revertOnUpdate: true },
);
return (
<main className="oc-home-v2" data-no-translate>
<main ref={editorialRef} className="oc-home-v2" data-no-translate>
<section className="oc-v2-hero" aria-labelledby="v2-hero-title">
<img src={asset(home.hero.imageUrl)} alt={pickText('贵州喀斯特山野中的万趣私家旅行', 'A private WanderQ journey in the karst landscape of Guizhou', language)} />
<div className="oc-v2-hero-shade" aria-hidden="true" />
@@ -2794,8 +2944,155 @@ function HomePage({ variant = 'v1' }: { variant?: 'classic' | 'v1' }) {
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-prototype-story-steps', start: 'top 72%' },
});
gsap.from('.oc-home-trust-item', {
y: 34,
autoAlpha: 0,
duration: 0.62,
stagger: 0.08,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-trust', start: 'top 84%', once: true },
});
gsap.from('.oc-home-plan-row .oc-step', {
y: 72,
autoAlpha: 0,
duration: 0.82,
stagger: 0.13,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-plan-row', start: 'top 76%', once: true },
});
gsap.utils.toArray<HTMLElement>('.oc-home-v1 .oc-section-title').forEach((heading) => {
gsap.from(heading.children, {
y: 62,
autoAlpha: 0,
duration: 0.86,
stagger: 0.12,
ease: 'power3.out',
scrollTrigger: { trigger: heading, start: 'top 78%', once: true },
});
});
gsap.utils.toArray<HTMLElement>('.oc-home-v1 .oc-experience-band').forEach((band, index) => {
const image = band.querySelector('img');
const copy = band.querySelector('.oc-experience-band-copy');
gsap.from(band, {
xPercent: index % 2 === 0 ? -7 : 7,
autoAlpha: 0,
duration: 1,
ease: 'power3.out',
scrollTrigger: { trigger: band, start: 'top 82%', once: true },
});
if (image) {
gsap.from(image, {
scale: 1.15,
duration: 1.35,
ease: 'power3.out',
scrollTrigger: { trigger: band, start: 'top 82%', once: true },
});
}
if (copy) {
gsap.from(copy.children, {
y: 35,
autoAlpha: 0,
duration: 0.68,
stagger: 0.08,
ease: 'power3.out',
scrollTrigger: { trigger: band, start: 'top 78%', once: true },
});
}
});
gsap.from('.oc-home-v1 .oc-home-safari', {
y: 92,
clipPath: 'inset(0 0 100% 0)',
duration: 1.02,
stagger: 0.13,
ease: 'power4.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-home-safaris', start: 'top 76%', once: true },
});
gsap.from('.oc-home-v1 .oc-home-team-media > img', {
y: 72,
scale: 0.96,
autoAlpha: 0,
duration: 1,
stagger: 0.16,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-home-team', start: 'top 73%', once: true },
});
gsap.from('.oc-home-v1 .oc-home-team-copy > *', {
x: 58,
autoAlpha: 0,
duration: 0.78,
stagger: 0.08,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-home-team-copy', start: 'top 75%', once: true },
});
gsap.from('.oc-home-v1 .oc-quote > *', {
y: 68,
autoAlpha: 0,
duration: 0.95,
stagger: 0.14,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-quote', start: 'top 70%', once: true },
});
gsap.from('.oc-home-v1 .oc-social-channel', {
y: 38,
autoAlpha: 0,
duration: 0.62,
stagger: 0.09,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-social-channel-list', start: 'top 82%', once: true },
});
gsap.from('.oc-home-v1 .oc-live-heading > *', {
y: 58,
autoAlpha: 0,
duration: 0.82,
stagger: 0.11,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-live-heading', start: 'top 78%', once: true },
});
gsap.from('.oc-home-v1 .oc-live-photo', {
y: 84,
rotate: 1.5,
autoAlpha: 0,
duration: 0.9,
stagger: 0.09,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-live-grid', start: 'top 78%', once: true },
});
gsap.from('.oc-home-v1 .oc-comparison-heading > *', {
y: 56,
autoAlpha: 0,
duration: 0.82,
stagger: 0.1,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-comparison-heading', start: 'top 78%', once: true },
});
gsap.from('.oc-home-v1 .oc-comparison-scroll', {
y: 70,
autoAlpha: 0,
duration: 0.95,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-comparison-scroll', start: 'top 82%', once: true },
});
gsap.from('.oc-home-v1 .oc-home-final-cta-copy > *, .oc-home-v1 .oc-home-inquiry-prompts > *', {
y: 64,
autoAlpha: 0,
duration: 0.82,
stagger: 0.08,
ease: 'power3.out',
scrollTrigger: { trigger: '.oc-home-v1 .oc-home-final-cta', start: 'top 72%', once: true },
});
},
{ scope: prototypeRef, dependencies: [language, variant] },
{ scope: prototypeRef, dependencies: [language, variant], revertOnUpdate: true },
);
useEffect(() => {