Skip to content

Commit 9f175a3

Browse files
committed
feat: 为ProjectShowcase列实施基于CSS的滚动动画,重构图像数据
1 parent a4f6f95 commit 9f175a3

File tree

2 files changed

+80
-111
lines changed

2 files changed

+80
-111
lines changed

app/app.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,23 @@ body {
185185

186186
.animate-scroll-down {
187187
animation: scroll-down 64s linear infinite;
188+
}
189+
@keyframes scroll-up-half {
190+
0% {
191+
transform: translateY(0);
192+
}
193+
194+
100% {
195+
transform: translateY(-50%);
196+
}
197+
}
198+
199+
@keyframes scroll-down-half {
200+
0% {
201+
transform: translateY(-50%);
202+
}
203+
204+
100% {
205+
transform: translateY(0);
206+
}
188207
}

app/components/ProjectShowcase.tsx

Lines changed: 61 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
12
import { Button } from 'antd'
2-
import { useEffect, useRef, useState, useMemo } from 'react'
3+
import { ArrowRightOutlined, GithubOutlined } from '@ant-design/icons';
4+
import { useMemo } from 'react'
35

46
interface ProjectShowcaseProps {
57
title?: string
@@ -13,116 +15,54 @@ interface ProjectShowcaseProps {
1315
}
1416

1517
// JS-based Scrolling Column Component
16-
function ScrollingColumn({ images, direction = 'up', duration = 40 }: { images: string[], direction?: 'up' | 'down', duration?: number }) {
17-
const containerRef = useRef<HTMLDivElement>(null);
18-
const [contentHeight, setContentHeight] = useState(0);
19-
20-
// Measure content height once images are loaded/rendered
21-
useEffect(() => {
22-
if (containerRef.current) {
23-
// The height of one set of images is half the total scrollable height (since we duplicate)
24-
// But simpler: we just measure the first child (the wrapper of the first set)
25-
const firstSet = containerRef.current.firstElementChild as HTMLElement;
26-
if (firstSet) {
27-
const resizeObserver = new ResizeObserver(() => {
28-
setContentHeight(firstSet.offsetHeight);
29-
});
30-
resizeObserver.observe(firstSet);
31-
return () => resizeObserver.disconnect();
32-
}
33-
}
34-
}, []);
35-
36-
useEffect(() => {
37-
const container = containerRef.current;
38-
if (!container || contentHeight === 0) return;
39-
40-
let startTime: number | null = null;
41-
let animationFrameId: number;
42-
43-
// Speed calculation: pixels per millisecond
44-
const speed = contentHeight / (duration * 1000);
45-
46-
const animate = (timestamp: number) => {
47-
if (!startTime) startTime = timestamp;
48-
const elapsed = timestamp - startTime;
49-
50-
// Calculate offset based on direction
51-
let offset = (elapsed * speed) % contentHeight;
52-
53-
if (direction === 'up') {
54-
// Move up: 0 -> -contentHeight
55-
container.style.transform = `translateY(-${offset}px)`;
56-
} else {
57-
// Move down: -contentHeight -> 0
58-
// We start at -contentHeight and add offset
59-
container.style.transform = `translateY(-${contentHeight - offset}px)`;
60-
}
6118

62-
animationFrameId = requestAnimationFrame(animate);
63-
};
6419

65-
animationFrameId = requestAnimationFrame(animate);
66-
67-
return () => {
68-
cancelAnimationFrame(animationFrameId);
69-
};
70-
}, [contentHeight, direction, duration]);
71-
72-
return (
73-
<div className="h-full overflow-hidden relative w-1/3 min-w-[150px]">
74-
<div ref={containerRef} className="will-change-transform">
75-
{/* Render two copies for seamless looping */}
76-
{[0, 1].map((copyIndex) => (
77-
<div key={copyIndex} className="flex flex-col gap-8 pb-8">
78-
{images.map((src, j) => (
79-
<div key={`${copyIndex}-${j}`} className="w-full h-fit rounded-xl shadow-sm">
80-
<img src={src} alt="" className="w-full h-auto object-cover transition-all duration-500" />
81-
</div>
82-
))}
83-
</div>
84-
))}
85-
</div>
86-
</div>
87-
);
88-
}
20+
// Background images for scrolling columns
21+
const IMAGES = [
22+
'https://p.weizwz.com/cover/ThisCover_20250817_113149_50f33c9237daf1c6.webp',
23+
'https://p.weizwz.com/cover/ThisCover_20250817_170842_0f88bd188cabcecc.webp',
24+
'https://p.weizwz.com/cover/thiscover_example_1_2c9d37d69e1800f6.webp',
25+
'https://p.weizwz.com/cover/thiscover_example_2_1c118ab0f9fc93e0.webp',
26+
'https://p.weizwz.com/cover/thiscover_example_3_f41f7c9eb1e527a8.webp',
27+
'https://p.weizwz.com/cover/thiscover_example_4_8e2644481e476e28.webp',
28+
'https://p.weizwz.com/cover/ThisCover_20251122_200945_9817439a52309ebe.webp',
29+
'https://p.weizwz.com/cover/ThisCover_20251122_200033_d6ef3e567113ef9c.webp',
30+
'https://p.weizwz.com/cover/thiscover_example_5_1e1feb39361e31ca.webp',
31+
'https://p.weizwz.com/cover/thiscover_example_6_92b78a7283d015eb.webp',
32+
'https://p.weizwz.com/cover/thiscover_example_7_fdbfc2f7903cbd18.webp',
33+
'https://p.weizwz.com/cover/thiscover_example_8_db9eec43bc97cbd4.webp',
34+
'https://p.weizwz.com/cover/thiscover1_4x3_1c30c0287378464e.webp',
35+
'https://p.weizwz.com/cover/thiscover3_2x3_56c6e944063ea327.webp',
36+
'https://p.weizwz.com/cover/ThisCover_20251124_104354_aaf33287ee6a7ddd.webp',
37+
];
8938

9039
export function ProjectShowcase({
9140
title = 'ThisCover',
9241
subTitle = '封面生成器',
9342
logo = 'https://p.weizwz.com/cover/cover_full_441653186ab35580.webp',
9443
description = '一个免费、漂亮的封面生成器,提供丰富的素材和众多模板。支持多种格式导出,让每个人都能轻松制作出专业级的封面设计。无需设计经验,点点点即可完成精美封面制作。',
95-
primaryButtonText = '在线体验',
44+
primaryButtonText = '立即体验',
9645
secondaryButtonText = '了解更多',
9746
primaryButtonLink = 'https://cover.weizwz.com/editor/',
9847
secondaryButtonLink = 'https://cover.weizwz.com/'
9948
}: ProjectShowcaseProps) {
100-
// Background images for scrolling columns
101-
const images = [
102-
'https://p.weizwz.com/cover/ThisCover_20250817_113149_50f33c9237daf1c6.webp',
103-
'https://p.weizwz.com/cover/ThisCover_20250817_162105_931f0a568023c6ef.webp',
104-
'https://p.weizwz.com/cover/ThisCover_20250817_170842_0f88bd188cabcecc.webp',
105-
'https://p.weizwz.com/cover/thiscover_example_1_2c9d37d69e1800f6.webp',
106-
'https://p.weizwz.com/cover/thiscover_example_2_1c118ab0f9fc93e0.webp',
107-
'https://p.weizwz.com/cover/thiscover_example_3_f41f7c9eb1e527a8.webp',
108-
'https://p.weizwz.com/cover/thiscover_example_4_8e2644481e476e28.webp',
109-
'https://p.weizwz.com/cover/thiscover_example_5_1e1feb39361e31ca.webp',
110-
'https://p.weizwz.com/cover/thiscover_example_6_92b78a7283d015eb.webp',
111-
'https://p.weizwz.com/cover/thiscover_example_7_fdbfc2f7903cbd18.webp',
112-
'https://p.weizwz.com/cover/thiscover_example_8_db9eec43bc97cbd4.webp',
113-
'https://p.weizwz.com/cover/thiscover1_4x3_1c30c0287378464e.webp',
114-
'https://p.weizwz.com/cover/thiscover2_3x2_4a4e2de8d6047cd0.webp',
115-
'https://p.weizwz.com/cover/thiscover3_2x3_56c6e944063ea327.webp'
116-
];
117-
118-
// Create 5 columns with randomized images and durations
119-
const columns = useMemo(() => Array.from({ length: 3 }).map((_, i) => {
120-
// Shuffle images and pick 10 random ones
121-
const shuffled = [...images].sort(() => Math.random() - 0.5).slice(0, 10);
122-
// Random duration between 40s and 80s
123-
const duration = 40 + Math.random() * 40;
124-
return { images: shuffled, duration };
125-
}), []);
49+
50+
// Create 3 columns with sequential images and random durations
51+
const columns = useMemo(() => {
52+
const columnCount = 3;
53+
const itemsPerColumn = Math.ceil(IMAGES.length / columnCount);
54+
55+
return Array.from({ length: columnCount }).map((_, i) => {
56+
// Slice images for this column to ensure uniqueness
57+
const start = i * itemsPerColumn;
58+
const end = start + itemsPerColumn;
59+
const columnImages = IMAGES.slice(start, end);
60+
61+
// Random duration between 32s and 42s
62+
const duration = 22 + Math.random() * 10;
63+
return { images: columnImages, duration };
64+
});
65+
}, []);
12666

12767
const techStack = ['Next.js', 'TailwindCSS', 'Iconify API', 'Unsplash API'];
12868

@@ -196,21 +136,31 @@ export function ProjectShowcase({
196136
<div className='ml-2 pl-2 flex-1 bg-white h-6 rounded-xl border border-gray-200/50 text-gray-400 text-sm'>{secondaryButtonLink}</div>
197137
</div>
198138

199-
{/* Browser Content (Scrolling Animation) */}
139+
{/* Browser Content (Carousel) */}
200140
<div className='h-[400px] md:h-[500px] p-4 overflow-hidden bg-gray-50'>
201-
<div className='h-full relative overflow-hidden rounded-xl'>
202-
<div className="absolute inset-0 flex gap-4 justify-center overflow-hidden opacity-90 h-[150%] -top-[25%]">
141+
<div className='h-full relative overflow-hidden rounded-xl flex gap-4'>
203142
{columns.map((col, i) => (
204-
<ScrollingColumn
205-
key={i}
206-
images={col.images}
207-
direction={i % 2 === 0 ? 'up' : 'down'}
208-
duration={col.duration}
209-
/>
143+
<div key={i} className="w-1/3 h-full relative overflow-hidden">
144+
<div
145+
key={col.duration}
146+
className="w-full flex flex-col gap-4 pb-4 will-change-transform"
147+
style={{
148+
animation: `scroll-${i % 2 === 0 ? 'up' : 'down'}-half ${col.duration}s linear infinite`
149+
}}
150+
>
151+
{/* Render two copies for seamless looping */}
152+
{[0, 1].map((copyIndex) => (
153+
<div key={copyIndex} className="flex flex-col gap-4">
154+
{col.images.map((src, j) => (
155+
<div key={`${copyIndex}-${j}`} className="w-full h-fit rounded-xl shadow-sm overflow-hidden">
156+
<img src={src} alt="" className="w-full h-auto object-cover" />
157+
</div>
158+
))}
159+
</div>
160+
))}
161+
</div>
162+
</div>
210163
))}
211-
</div>
212-
{/* Inner Shadow/Overlay for depth */}
213-
<div className="absolute inset-0 pointer-events-none shadow-[inset_0_0_40px_rgba(0,0,0,0.05)]"></div>
214164
</div>
215165
</div>
216166
</div>

0 commit comments

Comments
 (0)