import { FC, useEffect, useId, useRef, useState } from 'react'; import JSXStyle from 'styled-jsx/style'; import { getLayoutStyles, VideoItem as TVideoItem } from '@cntrl-site/sdk'; import { ItemProps } from '../Item'; import { LinkWrapper } from '../LinkWrapper'; import { useFileItem } from './useFileItem'; import { useItemAngle } from '../useItemAngle'; import { useCntrlContext } from '../../../provider/useCntrlContext'; import { useRegisterResize } from '../../../common/useRegisterResize'; import { useLayoutContext } from '../../useLayoutContext'; import { ScrollPlaybackVideo } from '../../ScrollPlaybackVideo'; import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams'; import { useVideoFx } from '../../../utils/effects/useVideoFx'; import { useElementRect } from '../../../utils/useElementRect'; import { useItemFXData } from '../../../common/useItemFXData'; import { getFill } from '../../../utils/getFill'; import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry'; export const VideoItem: FC> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => { const reactId = useId(); const id = `${reactId}-video-${item.id}`; const { layouts } = useCntrlContext(); const layoutId = useLayoutContext(); const { radius: itemRadius, strokeWidth: itemStrokeWidth, strokeFill: itemStrokeFill, opacity: itemOpacity, blur: itemBlur } = useFileItem(item, sectionId); const [isVideoPlaying, setIsVideoPlaying] = useState(false); const isScrollPausedRef = useRef(false); const [userPaused, setUserPaused] = useState(false); const [isVideoInteracted, setIsVideoInteracted] = useState(false); const itemAngle = useItemAngle(item, sectionId); const [ref, setRef] = useState(null); const [videoRef, setVideoRef] = useState(null); const fxCanvas = useRef(null); const { url, hasGLEffect } = item.commonParams; const isInitialRef = useRef(true); const area = layoutId ? item.area[layoutId] : null; const layoutParams = layoutId ? item.layoutParams[layoutId] : null; const exemplary = layouts?.find(l => l.id === layoutId)?.exemplary; const width = area && exemplary ? area.width * exemplary : 0; const height = area && exemplary ? area.height * exemplary : 0; const { controlsValues, fragmentShader, texturesUrls } = useItemFXData(item, sectionId); const rect = useElementRect(ref); const rectWidth = Math.floor(rect?.width ?? 0); const rectHeight = Math.floor(rect?.height ?? 0); const scrollPlayback = layoutParams ? layoutParams.scrollPlayback : null; const layoutValues: Record[] = [item.area, item.layoutParams]; const isHiddenOnLayout = layoutId ? (item.hidden[layoutId] ?? false) : false; const hasScrollPlayback = scrollPlayback !== null && !isHiddenOnLayout; const wrapperStateParams = interactionCtrl?.getState(['angle', 'opacity', 'blur']); const videoStateParams = interactionCtrl?.getState(['strokeWidth', 'radius', 'strokeFill']); const angle = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.angle, itemAngle); const opacity = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.opacity, itemOpacity); const blur = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.blur, itemBlur); const strokeWidth = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.strokeWidth, itemStrokeWidth); const radius = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.radius, itemRadius); const strokeFill = getStyleFromItemStateAndParams(videoStateParams?.styles?.strokeFill?.[0], itemStrokeFill?.[0]) ?? itemStrokeFill?.[0]; const stroke = strokeFill ? getFill(strokeFill) ?? 'transparent' : 'transparent'; useEffect(() => { isInitialRef.current = false; }, []); const isFXAllowed = useVideoFx( fxCanvas.current, !!(hasGLEffect && !isInitialRef.current), { videoUrl: url, fragmentShader, controls: controlsValues, texturesUrls }, width, height ); useRegisterResize(ref, onResize); useItemGeometry(item.id, ref); const inlineStyles = { ...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}), ...(strokeWidth !== undefined ? { borderWidth: `${strokeWidth * 100}vw`, borderColor: stroke, borderRadius: radius !== undefined ? `${radius * 100}vw` : 'inherit', borderStyle: 'solid' } : {}), transition: videoStateParams?.transition ?? 'none' }; const isInteractive = opacity !== 0; useEffect(() => { onVisibilityChange?.(isInteractive); }, [isInteractive, onVisibilityChange]); useEffect(() => { if (!layoutParams || !videoRef || layoutParams.play !== 'on-click' || !ref) return; const observer = new IntersectionObserver( ([entry]) => { if (userPaused || !isVideoInteracted) return; if (entry.isIntersecting) { isScrollPausedRef.current = false; videoRef.play(); } else { isScrollPausedRef.current = true; videoRef.pause(); } } ); observer.observe(ref); return () => observer.disconnect(); }, [layoutParams, videoRef, ref, userPaused, isVideoInteracted]); return (
{hasScrollPlayback && ( )} {hasGLEffect && isFXAllowed && ( )} {!hasScrollPlayback && !hasGLEffect && layoutParams && ( <> {(layoutParams.play === 'on-click' || layoutParams.play === 'on-hover') && item.commonParams.coverUrl && !isVideoInteracted && ( { if (!videoRef || layoutParams.play !== 'on-hover') return; setIsVideoInteracted(true); videoRef.play(); }} src={item.commonParams.coverUrl ?? ''} className={`video-cover-${item.id}`} onClick={() => { if (!videoRef) return; setIsVideoInteracted(true); videoRef.play(); }} /> )} {(layoutParams.play === 'on-click' && !layoutParams.controls && (
{ if (!videoRef) return; setIsVideoInteracted(true); if (isVideoPlaying) { videoRef.pause(); } else { videoRef.play(); } }} /> ))} )}
{` .video-wrapper-${item.id} { position: absolute; overflow: hidden; width: 100%; height: 100%; box-sizing: border-box; opacity: ${opacity}; } .video-overlay-${item.id} { position: absolute; top: 0; left: 0; cursor: pointer; width: 100%; height: 100%; } .video-cover-${item.id} { position: absolute; top: 0; left: 0; cursor: pointer; width: 100%; height: 100%; object-fit: cover; } .video { width: 100%; height: 100%; box-sizing: border-box; opacity: 1; object-fit: cover; overflow: hidden; border-style: solid; } .video-${item.id} { border-radius: ${radius !== undefined ? `${radius * 100}vw` : '0'}; } .video-playback-wrapper { display: flex; justify-content: center; } .video-canvas { border: solid; width: 100%; height: 100%; pointer-events: none; border-width: 0; box-sizing: border-box; } ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams]) => { return (` .video-wrapper-${item.id} { opacity: ${layoutParams.opacity}; transform: rotate(${area.angle}deg); filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'}; ${layoutParams.blur !== 0 ? 'will-change: transform;' : ''} } .video-${item.id} { border: solid; border-color: ${stroke}; border-radius: ${layoutParams.radius * 100}vw; border-width: ${layoutParams.strokeWidth * 100}vw; } `); })} `} ); };