image-comparison.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. "use client"
  2. import type React from "react"
  3. import { useState, useRef, useCallback, useEffect } from "react"
  4. import Image from "next/image"
  5. import { useTranslations } from "next-intl"
  6. interface ImageComparisonProps {
  7. locale?: string
  8. }
  9. export default function ImageComparison({ locale }: ImageComparisonProps) {
  10. const [sliderPosition, setSliderPosition] = useState(50)
  11. const [isDragging, setIsDragging] = useState(false)
  12. const [isLoaded, setIsLoaded] = useState(false)
  13. const containerRef = useRef<HTMLDivElement>(null)
  14. const animationFrameRef = useRef<number | null>(null)
  15. const t = useTranslations("demo")
  16. const handleMouseDown = useCallback(() => {
  17. setIsDragging(true)
  18. }, [])
  19. const handleMouseUp = useCallback(() => {
  20. setIsDragging(false)
  21. }, [])
  22. const updateSliderPosition = useCallback((clientX: number) => {
  23. if (!containerRef.current) return
  24. const rect = containerRef.current.getBoundingClientRect()
  25. const x = clientX - rect.left
  26. const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
  27. // 使用 requestAnimationFrame 来优化性能
  28. if (animationFrameRef.current) {
  29. cancelAnimationFrame(animationFrameRef.current)
  30. }
  31. animationFrameRef.current = requestAnimationFrame(() => {
  32. setSliderPosition(percentage)
  33. })
  34. }, [])
  35. const handleMouseMove = useCallback(
  36. (e: React.MouseEvent) => {
  37. if (!isDragging) return
  38. e.preventDefault()
  39. updateSliderPosition(e.clientX)
  40. },
  41. [isDragging, updateSliderPosition],
  42. )
  43. const handleTouchMove = useCallback(
  44. (e: React.TouchEvent) => {
  45. if (!isDragging) return
  46. e.preventDefault()
  47. updateSliderPosition(e.touches[0].clientX)
  48. },
  49. [isDragging, updateSliderPosition],
  50. )
  51. // 清理动画帧
  52. useEffect(() => {
  53. return () => {
  54. if (animationFrameRef.current) {
  55. cancelAnimationFrame(animationFrameRef.current)
  56. }
  57. }
  58. }, [])
  59. // 图片加载完成后设置状态
  60. useEffect(() => {
  61. const timer = setTimeout(() => {
  62. setIsLoaded(true)
  63. }, 100)
  64. return () => clearTimeout(timer)
  65. }, [])
  66. return (
  67. <div className="relative w-full max-w-2xl mx-auto">
  68. <div
  69. ref={containerRef}
  70. className={`relative aspect-[4/3] overflow-hidden rounded-2xl shadow-2xl cursor-col-resize transition-opacity duration-300 ${
  71. isLoaded ? 'opacity-100' : 'opacity-0'
  72. }`}
  73. onMouseMove={handleMouseMove}
  74. onMouseUp={handleMouseUp}
  75. onMouseLeave={handleMouseUp}
  76. onTouchMove={handleTouchMove}
  77. onTouchEnd={handleMouseUp}
  78. style={{ willChange: 'transform' }}
  79. >
  80. {/* Before Image */}
  81. <div className="absolute inset-0" style={{ transform: 'translateZ(0)' }}>
  82. <Image
  83. src="/Original.jpg"
  84. alt="Original cityscape during daytime"
  85. fill
  86. className="object-cover"
  87. priority
  88. onLoad={() => setIsLoaded(true)}
  89. sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  90. />
  91. <div className="absolute top-4 left-4 bg-black/70 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
  92. Original
  93. </div>
  94. </div>
  95. {/* After Image */}
  96. <div
  97. className="absolute inset-0 overflow-hidden"
  98. style={{
  99. clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`,
  100. transform: 'translateZ(0)',
  101. willChange: 'clip-path'
  102. }}
  103. >
  104. <Image
  105. src="/AI Enhanced.jpg"
  106. alt="AI-transformed cyberpunk cityscape at night"
  107. fill
  108. className="object-cover"
  109. priority
  110. sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  111. />
  112. <div className="absolute top-4 right-4 bg-gradient-to-r from-purple-600 to-blue-600 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">
  113. AI Enhanced
  114. </div>
  115. </div>
  116. {/* Slider */}
  117. <div
  118. className="absolute top-0 bottom-0 w-1 bg-white shadow-lg cursor-col-resize transition-all duration-75 ease-out"
  119. style={{
  120. left: `${sliderPosition}%`,
  121. transform: 'translateZ(0)',
  122. willChange: 'left'
  123. }}
  124. onMouseDown={handleMouseDown}
  125. onTouchStart={handleMouseDown}
  126. >
  127. <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center transition-transform duration-150 hover:scale-110">
  128. <div className="w-1 h-4 bg-gray-400 rounded-full mx-0.5"></div>
  129. <div className="w-1 h-4 bg-gray-400 rounded-full mx-0.5"></div>
  130. </div>
  131. </div>
  132. {/* Loading overlay */}
  133. {!isLoaded && (
  134. <div className="absolute inset-0 bg-gray-100 animate-pulse rounded-2xl flex items-center justify-center">
  135. <div className="text-gray-400">Loading...</div>
  136. </div>
  137. )}
  138. </div>
  139. {/* Instructions */}
  140. <div className="text-center mt-4 text-sm text-muted-foreground transition-opacity duration-300">
  141. {t("sliderInstruction")}
  142. </div>
  143. </div>
  144. )
  145. }