为首页 V1 和 V2 增加高级滚动动画
This commit is contained in:
11
design-qa.md
11
design-qa.md
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user