Files
vfxreview/hooks/use-review-player.ts
T
twotalesanimation 75f9258b69
Deploy / deploy (push) Successful in 5m39s
feat(player): aspect-ratio blanking overlay in the review player
A shot is reviewed at the framing it was rendered at but usually delivered
cropped, so add a dropdown beside the playback-speed control that lays
delivery blanking over the video: 2.39 through 9:16, widest to tallest, with
"No blanking" as the default. Purely a viewing aid — nothing is written to
the review, and the video element is untouched.

The geometry is the awkward part. The video is object-contain, so the picture
is letterboxed inside the element and bars drawn against the element's edges
would be wrong on any panel whose shape does not match the source.
letterboxRect works out where the picture is actually painted and the bars are
positioned against that, kept current by a ResizeObserver and a loadedmetadata
listener so resizes and fullscreen stay correct. Ratios within half a pixel of
the source draw nothing rather than a hairline of rounding error.

Two modes, since blanking and crop bars are different jobs: solid black hides
the crop the way the master will, guide dims it so the excluded area is still
readable. The overlay sits under the annotation canvas — deliberately no
z-index, since the canvas sets none either and DOM order alone gives marks
drawn over a blanked area the right precedence.

Verified against real browser layout, not just unit tests: the computed
picture rect lands exactly on the image edges across three panel shapes, and
the visible area measures the requested ratio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 07:21:58 +02:00

93 lines
2.9 KiB
TypeScript

import { create } from "zustand";
import type { AnnotationTool } from "@/types";
import { ASPECT_OFF, type BlankingMode } from "@/lib/aspect-overlays";
interface ReviewPlayerStore {
// Playback
isPlaying: boolean;
currentFrame: number;
currentTime: number;
duration: number;
playbackRate: number;
volume: number;
isMuted: boolean;
isFullscreen: boolean;
// Annotation
isAnnotating: boolean;
selectedTool: AnnotationTool;
selectedColor: string;
strokeWidth: number;
showAnnotations: boolean;
// Delivery blanking — a viewing aid only, never part of the saved review
aspectPresetId: string;
blankingMode: BlankingMode;
// Config (set from version data)
fps: number;
totalFrames: number;
// Actions
setPlaying: (playing: boolean) => void;
setCurrentFrame: (frame: number) => void;
setCurrentTime: (time: number) => void;
setDuration: (duration: number) => void;
setPlaybackRate: (rate: number) => void;
setVolume: (volume: number) => void;
setMuted: (muted: boolean) => void;
setFullscreen: (fullscreen: boolean) => void;
setAnnotating: (annotating: boolean) => void;
setSelectedTool: (tool: AnnotationTool) => void;
setSelectedColor: (color: string) => void;
setStrokeWidth: (width: number) => void;
setShowAnnotations: (show: boolean) => void;
setAspectPresetId: (id: string) => void;
setBlankingMode: (mode: BlankingMode) => void;
setFps: (fps: number) => void;
setTotalFrames: (frames: number) => void;
reset: () => void;
}
const initialState = {
isPlaying: false,
currentFrame: 0,
currentTime: 0,
duration: 0,
playbackRate: 1,
volume: 1,
isMuted: false,
isFullscreen: false,
isAnnotating: false,
selectedTool: "freehand" as AnnotationTool,
selectedColor: "#ef4444",
strokeWidth: 2,
showAnnotations: true,
aspectPresetId: ASPECT_OFF,
blankingMode: "solid" as BlankingMode,
fps: 24,
totalFrames: 0,
};
export const useReviewStore = create<ReviewPlayerStore>((set) => ({
...initialState,
setPlaying: (isPlaying) => set({ isPlaying }),
setCurrentFrame: (currentFrame) => set({ currentFrame }),
setCurrentTime: (currentTime) => set({ currentTime }),
setDuration: (duration) => set({ duration }),
setPlaybackRate: (playbackRate) => set({ playbackRate }),
setVolume: (volume) => set({ volume }),
setMuted: (isMuted) => set({ isMuted }),
setFullscreen: (isFullscreen) => set({ isFullscreen }),
setAnnotating: (isAnnotating) => set({ isAnnotating }),
setSelectedTool: (selectedTool) => set({ selectedTool }),
setSelectedColor: (selectedColor) => set({ selectedColor }),
setStrokeWidth: (strokeWidth) => set({ strokeWidth }),
setShowAnnotations: (showAnnotations) => set({ showAnnotations }),
setAspectPresetId: (aspectPresetId) => set({ aspectPresetId }),
setBlankingMode: (blankingMode) => set({ blankingMode }),
setFps: (fps) => set({ fps }),
setTotalFrames: (totalFrames) => set({ totalFrames }),
reset: () => set(initialState),
}));