Skip to content

Commit 74ea099

Browse files
committed
fix: improve WebGL effect rendering in preview
- Separate CSS and WebGL rendering logic into different useEffects - Render first frame with WebGL effect immediately on video load - Continuous WebGL rendering only during hover - Add logging to debug WebGL effect application - Remove duplicate first frame rendering code - Fixes issue where WebGL effects weren't showing on preview tiles
1 parent b0d4b1b commit 74ea099

File tree

1 file changed

+145
-83
lines changed

1 file changed

+145
-83
lines changed

src/features/effects/components/effect-preview.tsx

Lines changed: 145 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,22 @@ export function EffectPreview({
159159
* Определяем использует ли эффект WebGL
160160
*/
161161
const useWebGL = useMemo(() => {
162-
return !!(processedEffect?.processors?.webgl?.fragmentShader)
162+
const hasWebGL = !!(processedEffect?.processors?.webgl?.fragmentShader)
163+
if (processedEffect) {
164+
void logger.info("Effect rendering mode", {
165+
effectId: processedEffect.id,
166+
useWebGL: hasWebGL,
167+
hasFragmentShader: !!processedEffect?.processors?.webgl?.fragmentShader,
168+
})
169+
}
170+
return hasWebGL
163171
}, [processedEffect])
164172

165173
/**
166-
* Применение эффекта к видео (WebGL или CSS)
174+
* Применение CSS-эффекта к видео (только для эффектов без WebGL)
167175
*/
168176
useEffect(() => {
169-
if (!effect) return
177+
if (!effect || useWebGL) return // Пропускаем если используем WebGL
170178
if (!videoSrc || !videoRef.current) return
171179
const videoElement = videoRef.current
172180

@@ -184,72 +192,148 @@ export function EffectPreview({
184192
videoElement.playbackRate = playbackRate
185193
videoElement.loop = true
186194

187-
// WebGL рендеринг
188-
if (useWebGL && canvasRef.current) {
189-
const canvas = canvasRef.current
190-
canvas.width = width
191-
canvas.height = height
192-
193-
// Функция рендеринга кадра
194-
const renderFrame = async () => {
195-
if (!videoElement || !canvas || videoElement.paused || videoElement.ended) {
196-
return
197-
}
198-
199-
try {
200-
await getEffectsPreviewService().applyEffect(
201-
videoElement,
202-
processedEffect!.id,
203-
effectParams,
204-
canvas,
205-
)
206-
} catch (error) {
207-
void logger.error("WebGL effect rendering failed", { error })
208-
}
209-
210-
// Следующий кадр
211-
animationFrameRef.current = requestAnimationFrame(renderFrame)
212-
}
195+
// Применяем CSS фильтры
196+
const cssFilter = generateCSSFilterForEffect(processedEffect || effect, effectParams)
197+
if (cssFilter) {
198+
videoElement.style.filter = cssFilter
199+
}
213200

214-
// Начинаем рендеринг при hover
215-
if (isHovering && !videoElement.paused) {
216-
animationFrameRef.current = requestAnimationFrame(renderFrame)
217-
}
201+
// Специальные эффекты через box-shadow
202+
if (
203+
processedEffect?.id === "vignette" ||
204+
(processedEffect?.category === "lighting" && processedEffect?.id.includes("vignette"))
205+
) {
206+
const intensity = customParams?.intensity ?? effectParams?.intensity ?? 0.3
207+
const radius = customParams?.radius ?? effectParams?.radius ?? 0.8
208+
const shadowSize = Math.round(Math.min(width, height) * (1 - radius) * 0.5)
209+
const shadowBlur = Math.round(shadowSize * intensity * 2)
210+
videoElement.style.boxShadow = `inset 0 0 ${shadowBlur}px ${shadowSize}px rgba(0,0,0,${intensity})`
211+
} else {
212+
videoElement.style.boxShadow = ""
213+
}
218214

219-
return () => {
220-
if (animationFrameRef.current) {
221-
cancelAnimationFrame(animationFrameRef.current)
222-
animationFrameRef.current = null
223-
}
224-
}
215+
return () => {
216+
videoElement.style.filter = ""
217+
videoElement.style.boxShadow = ""
225218
}
226-
// CSS фильтры (fallback)
227-
else {
228-
const cssFilter = generateCSSFilterForEffect(processedEffect || effect, effectParams)
229-
if (cssFilter) {
230-
videoElement.style.filter = cssFilter
219+
}, [processedEffect, width, height, customParams, videoSrc, effect, useWebGL])
220+
221+
/**
222+
* Рендеринг первого кадра с WebGL эффектом
223+
*/
224+
useEffect(() => {
225+
if (!useWebGL || !videoRef.current || !canvasRef.current || !processedEffect) return
226+
227+
const videoElement = videoRef.current
228+
const canvas = canvasRef.current
229+
230+
// Устанавливаем размеры canvas
231+
canvas.width = width
232+
canvas.height = height
233+
234+
// Устанавливаем скорость воспроизведения
235+
const playbackRate = getPlaybackRate(processedEffect)
236+
videoElement.playbackRate = playbackRate
237+
videoElement.loop = true
238+
239+
// Функция рендеринга одного кадра
240+
const renderSingleFrame = async () => {
241+
if (videoElement.readyState < 2) return // Видео ещё не готово
242+
243+
const effectParams: Record<string, any> = {}
244+
if (processedEffect.parameters) {
245+
processedEffect.parameters.forEach((param) => {
246+
effectParams[param.id] = param.currentValue ?? param.defaultValue
247+
})
231248
}
249+
Object.assign(effectParams, customParams)
250+
251+
try {
252+
await getEffectsPreviewService().applyEffect(
253+
videoElement,
254+
processedEffect.id,
255+
effectParams,
256+
canvas,
257+
)
258+
} catch (error) {
259+
void logger.error("WebGL effect rendering failed", { error })
260+
}
261+
}
262+
263+
// Рендерим первый кадр когда видео готово
264+
const handleLoadedData = () => {
265+
void logger.info("Rendering first frame with WebGL", {
266+
effectId: processedEffect.id,
267+
readyState: videoElement.readyState,
268+
})
269+
void renderSingleFrame()
270+
}
271+
272+
if (videoElement.readyState >= 2) {
273+
void logger.info("Video ready, rendering first frame immediately", {
274+
effectId: processedEffect.id,
275+
readyState: videoElement.readyState,
276+
})
277+
void renderSingleFrame()
278+
} else {
279+
void logger.info("Waiting for video to load", {
280+
effectId: processedEffect.id,
281+
readyState: videoElement.readyState,
282+
})
283+
videoElement.addEventListener("loadeddata", handleLoadedData)
284+
}
285+
286+
return () => {
287+
videoElement.removeEventListener("loadeddata", handleLoadedData)
288+
}
289+
}, [useWebGL, processedEffect, width, height, customParams, videoSrc])
290+
291+
/**
292+
* Непрерывный WebGL рендеринг при hover
293+
*/
294+
useEffect(() => {
295+
if (!useWebGL || !isHovering || !videoRef.current || !canvasRef.current || !processedEffect) return
296+
297+
const videoElement = videoRef.current
298+
const canvas = canvasRef.current
299+
300+
if (videoElement.paused || videoElement.ended) return
301+
302+
const effectParams: Record<string, any> = {}
303+
if (processedEffect.parameters) {
304+
processedEffect.parameters.forEach((param) => {
305+
effectParams[param.id] = param.currentValue ?? param.defaultValue
306+
})
307+
}
308+
Object.assign(effectParams, customParams)
232309

233-
// Специальные эффекты через box-shadow
234-
if (
235-
processedEffect?.id === "vignette" ||
236-
(processedEffect?.category === "lighting" && processedEffect?.id.includes("vignette"))
237-
) {
238-
const intensity = customParams?.intensity ?? effectParams?.intensity ?? 0.3
239-
const radius = customParams?.radius ?? effectParams?.radius ?? 0.8
240-
const shadowSize = Math.round(Math.min(width, height) * (1 - radius) * 0.5)
241-
const shadowBlur = Math.round(shadowSize * intensity * 2)
242-
videoElement.style.boxShadow = `inset 0 0 ${shadowBlur}px ${shadowSize}px rgba(0,0,0,${intensity})`
243-
} else {
244-
videoElement.style.boxShadow = ""
310+
// Функция рендеринга кадра в цикле
311+
const renderFrame = async () => {
312+
if (videoElement.paused || videoElement.ended) return
313+
314+
try {
315+
await getEffectsPreviewService().applyEffect(
316+
videoElement,
317+
processedEffect.id,
318+
effectParams,
319+
canvas,
320+
)
321+
} catch (error) {
322+
void logger.error("WebGL continuous rendering failed", { error })
245323
}
246324

247-
return () => {
248-
videoElement.style.filter = ""
249-
videoElement.style.boxShadow = ""
325+
animationFrameRef.current = requestAnimationFrame(renderFrame)
326+
}
327+
328+
animationFrameRef.current = requestAnimationFrame(renderFrame)
329+
330+
return () => {
331+
if (animationFrameRef.current) {
332+
cancelAnimationFrame(animationFrameRef.current)
333+
animationFrameRef.current = null
250334
}
251335
}
252-
}, [processedEffect, width, height, customParams, videoSrc, effect, useWebGL, isHovering])
336+
}, [useWebGL, isHovering, processedEffect, customParams, videoSrc])
253337

254338
/**
255339
* Управление воспроизведением при наведении
@@ -268,30 +352,8 @@ export function EffectPreview({
268352
// Без наведения - останавливаем и возвращаем на первый кадр
269353
videoElement.pause()
270354
videoElement.currentTime = 0
271-
272-
// Для WebGL эффектов - рендерим первый кадр
273-
if (useWebGL && canvasRef.current && processedEffect) {
274-
const effectParams: Record<string, any> = {}
275-
if (processedEffect.parameters) {
276-
processedEffect.parameters.forEach((param) => {
277-
effectParams[param.id] = param.currentValue ?? param.defaultValue
278-
})
279-
}
280-
Object.assign(effectParams, customParams)
281-
282-
// Рендерим один кадр
283-
setTimeout(() => {
284-
if (canvasRef.current && videoElement.readyState >= 2) {
285-
getEffectsPreviewService()
286-
.applyEffect(videoElement, processedEffect.id, effectParams, canvasRef.current)
287-
.catch((err) => {
288-
void logger.error("Failed to render first frame", { error: err })
289-
})
290-
}
291-
}, 50)
292-
}
293355
}
294-
}, [isHovering, videoSrc, useWebGL, processedEffect, customParams])
356+
}, [isHovering, videoSrc])
295357

296358
return (
297359
<div className="flex flex-col items-center">

0 commit comments

Comments
 (0)