Skip to content

Commit bd03d4b

Browse files
authored
Merge pull request #412 from promer94/fix/progressive-image-flickering
fix: progressive image flickering issue
2 parents 83528e4 + 6f83217 commit bd03d4b

File tree

3 files changed

+70
-79
lines changed

3 files changed

+70
-79
lines changed

components/album/progressive-image.tsx

Lines changed: 51 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import type { ProgressiveImageProps } from '~/types/props.ts'
4-
import { useEffect, useState, useRef } from 'react'
4+
import { useEffect, useState, useRef, Activity } from 'react'
55
import { useTranslations } from 'next-intl'
66
import { MotionImage } from '~/components/album/motion-image'
77
import { useBlurImageDataUrl } from '~/hooks/use-blurhash'
@@ -20,25 +20,25 @@ export default function ProgressiveImage(
2020
props: Readonly<ProgressiveImageProps>,
2121
) {
2222
const t = useTranslations()
23-
23+
2424
const [loadingProgress, setLoadingProgress] = useState(0)
2525
const [isLoading, setIsLoading] = useState(true)
2626
const [error, setError] = useState<string | null>(null)
2727
const [highResImageUrl, setHighResImageUrl] = useState<string | null>(null)
28-
const [showWebGLViewer, setShowWebGLViewer] = useState(Boolean(props.showLightbox))
29-
const [webGLAvailable, setWebGLAvailable] = useState(true)
30-
31-
const webglViewerRef = useRef<WebGLImageViewerRef | null>(null)
28+
const [highResImageLoaded, setHighResImageLoaded] = useState(false)
29+
const [showFullScreenViewer, setShowFullScreenViewer] = useState(Boolean(props.showLightbox))
30+
const [webGLAvailable] = useState(() => isWebGLSupported())
3231

32+
const webglViewerRef = useRef<WebGLImageViewerRef | null>(null)
3333
useEffect(() => {
34-
setShowWebGLViewer(Boolean(props.showLightbox))
34+
return () => {
35+
webglViewerRef.current?.destroy()
36+
}
37+
}, [])
38+
useEffect(() => {
39+
setShowFullScreenViewer(Boolean(props.showLightbox))
3540
}, [props.showLightbox])
3641

37-
// 检测 WebGL 支持
38-
useEffect(() => {
39-
setWebGLAvailable(isWebGLSupported())
40-
}, [])
41-
4242
useEffect(() => {
4343
loadHighResolutionImage()
4444
return () => {
@@ -86,7 +86,7 @@ export default function ProgressiveImage(
8686
const dataURL = useBlurImageDataUrl(props.blurhash)
8787

8888
const handleCloseViewer = () => {
89-
setShowWebGLViewer(false)
89+
setShowFullScreenViewer(false)
9090
if (props.onShowLightboxChange) {
9191
props.onShowLightboxChange(false)
9292
}
@@ -95,7 +95,7 @@ export default function ProgressiveImage(
9595
return (
9696
<div className="relative">
9797
{/* 预览图 - 在高清图未加载完成时显示 */}
98-
{!highResImageUrl ? (
98+
<Activity mode={highResImageLoaded ? 'hidden' : 'visible'}>
9999
<MotionImage
100100
initial={{ opacity: 0 }}
101101
animate={{ opacity: 1 }}
@@ -110,29 +110,49 @@ export default function ProgressiveImage(
110110
height={props.height}
111111
alt={props.alt || 'image'}
112112
/>
113-
) : (
114-
/* 高清图已加载 - 根据状态选择渲染方式 */
113+
{/* 加载进度条 */}
114+
{isLoading && (
115+
<div className="absolute bottom-0 left-0 w-full">
116+
<div
117+
className="h-1 bg-blue-500"
118+
style={{ width: `${loadingProgress}%` }}
119+
></div>
120+
<div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs px-2 py-1 rounded">
121+
{loadingProgress}%
122+
</div>
123+
</div>
124+
)}
125+
{/* 错误提示 */}
126+
{error && (
127+
<div className="absolute bottom-0 left-0 w-full">
128+
<div className="absolute bottom-2 right-2 text-white bg-black/60 text-xs px-2 py-1 rounded">
129+
{error}
130+
</div>
131+
</div>
132+
)}
133+
</Activity>
134+
{highResImageUrl ? (
115135
<>
116-
{/* 普通预览模式 - 点击可打开全屏查看 */}
117-
{!showWebGLViewer && (
136+
<Activity mode={highResImageLoaded && !showFullScreenViewer ? 'visible' : 'hidden'}>
118137
<img
119138
className="object-contain md:max-h-[90vh] cursor-pointer"
120139
src={highResImageUrl}
121140
width={props.width}
122141
height={props.height}
123142
alt={props.alt || 'image'}
124143
onClick={() => {
125-
setShowWebGLViewer(true)
144+
setShowFullScreenViewer(true)
126145
if (props.onShowLightboxChange) {
127146
props.onShowLightboxChange(true)
128147
}
129148
}}
149+
onLoad={() => {
150+
setHighResImageLoaded(true)
151+
}}
130152
/>
131-
)}
132-
133-
{/* WebGL 全屏查看模式 */}
134-
{showWebGLViewer && webGLAvailable && (
135-
<div
153+
</Activity>
154+
<Activity mode={showFullScreenViewer ? 'visible' : 'hidden'}>
155+
{webGLAvailable ? <div
136156
className="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center"
137157
onClick={(e) => {
138158
// 点击背景关闭
@@ -163,12 +183,12 @@ export default function ProgressiveImage(
163183
<line x1="6" y1="6" x2="18" y2="18"></line>
164184
</svg>
165185
</button>
166-
186+
167187
{/* 操作提示 */}
168188
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/50 text-sm pointer-events-none">
169189
{t('Tips.zoomHint')}
170190
</div>
171-
191+
172192
{/* WebGL 图片查看器 */}
173193
<div className="w-full h-full">
174194
<WebGLImageViewer
@@ -181,17 +201,11 @@ export default function ProgressiveImage(
181201
minScale={0.5}
182202
maxScale={10}
183203
limitToBounds={true}
184-
centerOnInit={true}
185204
smooth={true}
186205
debug={process.env.NODE_ENV === 'development'}
187206
/>
188207
</div>
189-
</div>
190-
)}
191-
192-
{/* WebGL 不可用时的 Fallback - 使用普通全屏图片 */}
193-
{showWebGLViewer && !webGLAvailable && (
194-
<div
208+
</div> : <div
195209
className="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center overflow-auto"
196210
onClick={(e) => {
197211
if (e.target === e.currentTarget) {
@@ -220,43 +234,20 @@ export default function ProgressiveImage(
220234
<line x1="6" y1="6" x2="18" y2="18"></line>
221235
</svg>
222236
</button>
223-
224237
{/* WebGL 不可用提示 */}
225238
<div className="absolute top-4 left-4 text-white/70 text-sm bg-black/50 px-3 py-1 rounded">
226239
{t('Tips.webglUnavailable')}
227240
</div>
228-
229241
<img
230242
className="max-w-full max-h-full object-contain"
231243
src={highResImageUrl}
232244
alt={props.alt || 'image'}
233245
/>
234-
</div>
235-
)}
246+
</div>}
247+
</Activity>
236248
</>
237-
)}
249+
) : null}
238250

239-
{/* 加载进度条 */}
240-
{isLoading && (
241-
<div className="absolute bottom-0 left-0 w-full">
242-
<div
243-
className="h-1 bg-blue-500"
244-
style={{ width: `${loadingProgress}%` }}
245-
></div>
246-
<div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs px-2 py-1 rounded">
247-
{loadingProgress}%
248-
</div>
249-
</div>
250-
)}
251-
252-
{/* 错误提示 */}
253-
{error && (
254-
<div className="absolute bottom-0 left-0 w-full">
255-
<div className="absolute bottom-2 right-2 text-white bg-black/60 text-xs px-2 py-1 rounded">
256-
{error}
257-
</div>
258-
</div>
259-
)}
260251
</div>
261252
)
262-
}
253+
}

components/album/webgl-viewer/WebGLImageViewer.tsx

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import * as React from 'react'
17-
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
17+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useEffectEvent } from 'react'
1818

1919
import {
2020
defaultAlignmentAnimation,
@@ -61,9 +61,10 @@ export const WebGLImageViewer = ({
6161
Omit<React.HTMLAttributes<HTMLDivElement>, 'className'>) => {
6262
const canvasRef = useRef<HTMLCanvasElement>(null)
6363
const viewerRef = useRef<WebGLImageViewerEngine | null>(null)
64+
const [isInitialized, setIsInitialized] = useState(false)
6465
const [tileOutlineEnabled, setTileOutlineEnabled] = useState(false)
6566

66-
const setDebugInfo = useRef((() => {}) as (debugInfo: DebugInfo) => void)
67+
const setDebugInfo = useRef((() => { }) as (debugInfo: DebugInfo) => void)
6768

6869
const config: Required<WebGLImageViewerProps> = useMemo(
6970
() => ({
@@ -89,9 +90,9 @@ export const WebGLImageViewer = ({
8990
...alignmentAnimation,
9091
},
9192
velocityAnimation: { ...defaultVelocityAnimation, ...velocityAnimation },
92-
onZoomChange: onZoomChange || (() => {}),
93-
onImageCopied: onImageCopied || (() => {}),
94-
onLoadingStateChange: onLoadingStateChange || (() => {}),
93+
onZoomChange: onZoomChange || (() => { }),
94+
onImageCopied: onImageCopied || (() => { }),
95+
onLoadingStateChange: onLoadingStateChange || (() => { }),
9596
debug: debug || false,
9697
}),
9798
[
@@ -123,32 +124,30 @@ export const WebGLImageViewer = ({
123124
zoomOut: (animated?: boolean) => viewerRef.current?.zoomOut(animated),
124125
resetView: () => viewerRef.current?.resetView(),
125126
getScale: () => viewerRef.current?.getScale() || 1,
127+
destroy: () => viewerRef.current?.destroy(),
126128
}))
127-
128-
useEffect(() => {
129+
const setUpWebGLEngine = useEffectEvent(() => {
130+
if (isInitialized) return viewerRef.current
129131
if (!canvasRef.current) return
130-
131132
const webGLImageViewerEngine = new WebGLImageViewerEngine(
132133
canvasRef.current,
133134
config,
134135
debug ? { current: setDebugInfo.current } : undefined,
135136
)
136-
137+
viewerRef.current = webGLImageViewerEngine
137138
try {
138139
const preknownWidth = config.width > 0 ? config.width : undefined
139140
const preknownHeight = config.height > 0 ? config.height : undefined
140-
webGLImageViewerEngine.loadImage(src, preknownWidth, preknownHeight).catch(console.error)
141-
viewerRef.current = webGLImageViewerEngine
142-
setTileOutlineEnabled(webGLImageViewerEngine.isTileOutlineEnabled())
141+
viewerRef.current.loadImage(src, preknownWidth, preknownHeight).catch(console.error)
143142
} catch (error) {
144-
console.error('Failed to initialize WebGL Image Viewer:', error)
143+
console.error('Failed to load image in WebGL Image Viewer:', error)
145144
}
146-
147-
return () => {
148-
webGLImageViewerEngine?.destroy()
149-
viewerRef.current = null
150-
}
151-
}, [src, config, debug])
145+
setIsInitialized(true)
146+
return viewerRef.current
147+
})
148+
useEffect(() => {
149+
setUpWebGLEngine()
150+
}, [])
152151

153152
const handleOutlineToggle = useCallback(
154153
(enabled: boolean) => {

components/album/webgl-viewer/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface WebGLImageViewerRef {
7575
zoomOut: (animated?: boolean) => void
7676
resetView: () => void
7777
getScale: () => number
78+
destroy: () => void
7879
}
7980

8081
export interface DebugInfo {

0 commit comments

Comments
 (0)