1+
12import { 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
46interface 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
9039export 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