fix crop and layout pos+scale on change

This commit is contained in:
Siddharth
2025-11-09 16:34:01 -07:00
parent d404ead557
commit fa780786c0
3 changed files with 111 additions and 11 deletions
+98 -3
View File
@@ -80,6 +80,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
const isPlayingRef = useRef(isPlaying);
const isSeekingRef = useRef(false);
const allowPlaybackRef = useRef(false);
const lockedVideoDimensionsRef = useRef<{ width: number; height: number } | null>(null);
const layoutVideoContentRef = useRef<(() => void) | null>(null);
const clampFocusToStage = useCallback((focus: ZoomFocus, depth: ZoomDepth) => {
return clampFocusToStageUtil(focus, depth, stageSizeRef.current);
@@ -123,6 +125,14 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
return;
}
// Lock video dimensions on first layout to prevent resize issues
if (!lockedVideoDimensionsRef.current && videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
lockedVideoDimensionsRef.current = {
width: videoElement.videoWidth,
height: videoElement.videoHeight,
};
}
const result = layoutVideoContentUtil({
container,
app,
@@ -130,6 +140,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
maskGraphics,
videoElement,
cropRegion,
lockedVideoDimensions: lockedVideoDimensionsRef.current,
});
if (result) {
@@ -151,7 +162,14 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
updateOverlayForRegion(activeRegion);
}
}, [updateOverlayForRegion, cropRegion]); const selectedZoom = useMemo(() => {
}, [updateOverlayForRegion, cropRegion]);
// Keep layoutVideoContent ref updated
useEffect(() => {
layoutVideoContentRef.current = layoutVideoContent;
}, [layoutVideoContent]);
const selectedZoom = useMemo(() => {
if (!selectedZoomId) return null;
return zoomRegions.find((region) => region.id === selectedZoomId) ?? null;
}, [zoomRegions, selectedZoomId]);
@@ -267,9 +285,84 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
isPlayingRef.current = isPlaying;
}, [isPlaying]);
// Reset animation state and transforms when crop changes
useEffect(() => {
if (!pixiReady || !videoReady) return;
layoutVideoContent();
const app = appRef.current;
const cameraContainer = cameraContainerRef.current;
const video = videoRef.current;
if (!app || !cameraContainer || !video) return;
const tickerWasStarted = app.ticker?.started || false;
if (tickerWasStarted && app.ticker) {
app.ticker.stop();
}
const wasPlaying = !video.paused;
if (wasPlaying) {
video.pause();
}
// Reset animation state so the ticker starts from identity once it resumes
animationStateRef.current = {
scale: 1,
focusX: DEFAULT_FOCUS.cx,
focusY: DEFAULT_FOCUS.cy,
};
if (blurFilterRef.current) {
blurFilterRef.current.blur = 0;
}
// Defer layout to the next frame so DOM measurements include the new crop UI state
requestAnimationFrame(() => {
const container = cameraContainerRef.current;
const videoStage = videoContainerRef.current;
const sprite = videoSpriteRef.current;
const currentApp = appRef.current;
if (!container || !videoStage || !sprite || !currentApp) {
return;
}
// Reset all transform hierarchies to identity
container.scale.set(1);
container.position.set(0, 0);
videoStage.scale.set(1);
videoStage.position.set(0, 0);
sprite.scale.set(1);
sprite.position.set(0, 0);
// Now layoutVideoContent will apply the correct transforms for the new crop
layoutVideoContent();
// Apply an explicit identity transform to ensure no residual camera offset
applyZoomTransform({
cameraContainer: container,
blurFilter: blurFilterRef.current,
stageSize: stageSizeRef.current,
baseMask: baseMaskRef.current,
zoomScale: 1,
focusX: DEFAULT_FOCUS.cx,
focusY: DEFAULT_FOCUS.cy,
motionIntensity: 0,
isPlaying: false,
});
// Restart ticker on a second frame to avoid running mid-layout
requestAnimationFrame(() => {
const finalApp = appRef.current;
if (wasPlaying && video) {
video.play().catch(() => {
/* ignore */
});
}
if (tickerWasStarted && finalApp?.ticker) {
finalApp.ticker.start();
}
});
});
}, [pixiReady, videoReady, layoutVideoContent, cropRegion]);
useEffect(() => {
@@ -559,7 +652,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
app.ticker.add(ticker);
return () => {
app.ticker.remove(ticker);
if (app && app.ticker) {
app.ticker.remove(ticker);
}
};
}, [pixiReady, videoReady, clampFocusToStage]);
@@ -9,6 +9,7 @@ interface LayoutParams {
maskGraphics: PIXI.Graphics;
videoElement: HTMLVideoElement;
cropRegion?: CropRegion;
lockedVideoDimensions?: { width: number; height: number } | null;
}
interface LayoutResult {
@@ -21,10 +22,11 @@ interface LayoutResult {
}
export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion } = params;
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions } = params;
const videoWidth = videoElement.videoWidth;
const videoHeight = videoElement.videoHeight;
// Use locked dimensions if available, otherwise use current video dimensions
const videoWidth = lockedVideoDimensions?.width || videoElement.videoWidth;
const videoHeight = lockedVideoDimensions?.height || videoElement.videoHeight;
if (!videoWidth || !videoHeight) {
return null;
@@ -1,3 +1,5 @@
import type React from 'react';
interface VideoEventHandlersParams {
video: HTMLVideoElement;
isSeekingRef: React.MutableRefObject<boolean>;
@@ -46,21 +48,22 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
return;
}
allowPlaybackRef.current = false;
isPlayingRef.current = true;
onPlayStateChange(true);
updateTime();
if (timeUpdateAnimationRef.current) {
cancelAnimationFrame(timeUpdateAnimationRef.current);
}
timeUpdateAnimationRef.current = requestAnimationFrame(updateTime);
};
const handlePause = () => {
allowPlaybackRef.current = false;
const handlePause = () => {
isPlayingRef.current = false;
onPlayStateChange(false);
if (timeUpdateAnimationRef.current) {
cancelAnimationFrame(timeUpdateAnimationRef.current);
timeUpdateAnimationRef.current = null;
}
emitTime(video.currentTime);
onPlayStateChange(false);
};
const handleSeeked = () => {