Merge pull request #373 from Moncef-Mhz/adjust-zoom-speed
feat: implement zoom speed
This commit is contained in:
@@ -225,6 +225,9 @@ interface SettingsPanelProps {
|
|||||||
onWebcamLayoutPresetChange?: (preset: WebcamLayoutPreset) => void;
|
onWebcamLayoutPresetChange?: (preset: WebcamLayoutPreset) => void;
|
||||||
webcamMaskShape?: import("./types").WebcamMaskShape;
|
webcamMaskShape?: import("./types").WebcamMaskShape;
|
||||||
onWebcamMaskShapeChange?: (shape: import("./types").WebcamMaskShape) => void;
|
onWebcamMaskShapeChange?: (shape: import("./types").WebcamMaskShape) => void;
|
||||||
|
selectedZoomInDuration?: number;
|
||||||
|
selectedZoomOutDuration?: number;
|
||||||
|
onZoomDurationChange?: (zoomIn: number, zoomOut: number) => void;
|
||||||
webcamSizePreset?: WebcamSizePreset;
|
webcamSizePreset?: WebcamSizePreset;
|
||||||
onWebcamSizePresetChange?: (size: WebcamSizePreset) => void;
|
onWebcamSizePresetChange?: (size: WebcamSizePreset) => void;
|
||||||
onWebcamSizePresetCommit?: () => void;
|
onWebcamSizePresetCommit?: () => void;
|
||||||
@@ -241,6 +244,13 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
|
|||||||
{ depth: 6, label: "5×" },
|
{ depth: 6, label: "5×" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ZOOM_SPEED_OPTIONS = [
|
||||||
|
{ label: "Instant", zoomIn: 0, zoomOut: 0 },
|
||||||
|
{ label: "Fast", zoomIn: 500, zoomOut: 350 },
|
||||||
|
{ label: "Smooth", zoomIn: 1522, zoomOut: 1015 },
|
||||||
|
{ label: "Lazy", zoomIn: 3000, zoomOut: 2000 },
|
||||||
|
];
|
||||||
|
|
||||||
export function SettingsPanel({
|
export function SettingsPanel({
|
||||||
selected,
|
selected,
|
||||||
onWallpaperChange,
|
onWallpaperChange,
|
||||||
@@ -306,6 +316,9 @@ export function SettingsPanel({
|
|||||||
onWebcamLayoutPresetChange,
|
onWebcamLayoutPresetChange,
|
||||||
webcamMaskShape = "rectangle",
|
webcamMaskShape = "rectangle",
|
||||||
onWebcamMaskShapeChange,
|
onWebcamMaskShapeChange,
|
||||||
|
selectedZoomInDuration,
|
||||||
|
selectedZoomOutDuration,
|
||||||
|
onZoomDurationChange,
|
||||||
webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET,
|
webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET,
|
||||||
onWebcamSizePresetChange,
|
onWebcamSizePresetChange,
|
||||||
onWebcamSizePresetCommit,
|
onWebcamSizePresetCommit,
|
||||||
@@ -648,6 +661,39 @@ export function SettingsPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{zoomEnabled && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="text-sm font-medium text-slate-200 mb-2 block">
|
||||||
|
{t("zoom.speed.title") || "Zoom Speed"}
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-4 gap-1.5">
|
||||||
|
{ZOOM_SPEED_OPTIONS.map((opt) => {
|
||||||
|
const isActive =
|
||||||
|
selectedZoomInDuration !== undefined &&
|
||||||
|
selectedZoomOutDuration !== undefined &&
|
||||||
|
Math.round(selectedZoomInDuration) === Math.round(opt.zoomIn) &&
|
||||||
|
Math.round(selectedZoomOutDuration) === Math.round(opt.zoomOut);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={opt.label}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onZoomDurationChange?.(opt.zoomIn, opt.zoomOut)}
|
||||||
|
className={cn(
|
||||||
|
"h-auto w-full rounded-lg border px-1 py-2 text-center shadow-sm transition-all",
|
||||||
|
"duration-200 ease-out cursor-pointer",
|
||||||
|
isActive
|
||||||
|
? "border-[#34B27B] bg-[#34B27B] text-white shadow-[#34B27B]/20"
|
||||||
|
: "border-white/5 bg-white/5 text-slate-400 hover:bg-white/10 hover:border-white/10 hover:text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-[10px] font-semibold">{opt.label}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{zoomEnabled && (
|
{zoomEnabled && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
@@ -1026,7 +1072,7 @@ export function SettingsPanel({
|
|||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="pb-3">
|
<AccordionContent className="pb-3">
|
||||||
<Tabs defaultValue="image" className="w-full">
|
<Tabs defaultValue="image" className="w-full">
|
||||||
<TabsList className="mb-2 bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-3 h-7 rounded-lg">
|
<TabsList className="mb-2 bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-3 rounded-lg">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="image"
|
value="image"
|
||||||
className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 text-[10px] py-1 rounded-md transition-all"
|
className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 text-[10px] py-1 rounded-md transition-all"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import {
|
|||||||
type ZoomRegion,
|
type ZoomRegion,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback";
|
import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback";
|
||||||
|
import { TRANSITION_WINDOW_MS, ZOOM_IN_TRANSITION_WINDOW_MS } from "./videoPlayback/constants";
|
||||||
|
|
||||||
export default function VideoEditor() {
|
export default function VideoEditor() {
|
||||||
const {
|
const {
|
||||||
@@ -955,6 +956,19 @@ export default function VideoEditor() {
|
|||||||
[pushState],
|
[pushState],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleZoomDurationChange = useCallback(
|
||||||
|
(id: string, zoomIn: number, zoomOut: number) => {
|
||||||
|
pushState((prev) => ({
|
||||||
|
zoomRegions: prev.zoomRegions.map((region) =>
|
||||||
|
region.id === id
|
||||||
|
? { ...region, zoomInDurationMs: zoomIn, zoomOutDurationMs: zoomOut }
|
||||||
|
: region,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[pushState],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAnnotationSpanChange = useCallback(
|
const handleAnnotationSpanChange = useCallback(
|
||||||
(id: string, span: Span) => {
|
(id: string, span: Span) => {
|
||||||
pushState((prev) => ({
|
pushState((prev) => ({
|
||||||
@@ -1852,6 +1866,7 @@ export default function VideoEditor() {
|
|||||||
onZoomAdded={handleZoomAdded}
|
onZoomAdded={handleZoomAdded}
|
||||||
onZoomSuggested={handleZoomSuggested}
|
onZoomSuggested={handleZoomSuggested}
|
||||||
onZoomSpanChange={handleZoomSpanChange}
|
onZoomSpanChange={handleZoomSpanChange}
|
||||||
|
onZoomDurationChange={handleZoomDurationChange}
|
||||||
onZoomDelete={handleZoomDelete}
|
onZoomDelete={handleZoomDelete}
|
||||||
selectedZoomId={selectedZoomId}
|
selectedZoomId={selectedZoomId}
|
||||||
onSelectZoom={handleSelectZoom}
|
onSelectZoom={handleSelectZoom}
|
||||||
@@ -1993,6 +2008,21 @@ export default function VideoEditor() {
|
|||||||
onSpeedDelete={handleSpeedDelete}
|
onSpeedDelete={handleSpeedDelete}
|
||||||
unsavedExport={unsavedExport}
|
unsavedExport={unsavedExport}
|
||||||
onSaveUnsavedExport={handleSaveUnsavedExport}
|
onSaveUnsavedExport={handleSaveUnsavedExport}
|
||||||
|
selectedZoomInDuration={
|
||||||
|
selectedZoomId
|
||||||
|
? (zoomRegions.find((z) => z.id === selectedZoomId)?.zoomInDurationMs ??
|
||||||
|
Math.round(ZOOM_IN_TRANSITION_WINDOW_MS))
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
selectedZoomOutDuration={
|
||||||
|
selectedZoomId
|
||||||
|
? (zoomRegions.find((z) => z.id === selectedZoomId)?.zoomOutDurationMs ??
|
||||||
|
Math.round(TRANSITION_WINDOW_MS))
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onZoomDurationChange={(zoomIn, zoomOut) =>
|
||||||
|
selectedZoomId && handleZoomDurationChange(selectedZoomId, zoomIn, zoomOut)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import type { Span } from "dnd-timeline";
|
import type { Span } from "dnd-timeline";
|
||||||
import { useItem } from "dnd-timeline";
|
import { useItem, useTimelineContext } from "dnd-timeline";
|
||||||
import { Gauge, MessageSquare, Scissors, ZoomIn } from "lucide-react";
|
import { Gauge, MessageSquare, Scissors, ZoomIn } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
DEFAULT_ZOOM_IN_MS,
|
||||||
|
DEFAULT_ZOOM_OUT_MS,
|
||||||
|
getDurations,
|
||||||
|
} from "../videoPlayback/zoomRegionUtils";
|
||||||
import glassStyles from "./ItemGlass.module.css";
|
import glassStyles from "./ItemGlass.module.css";
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
@@ -13,7 +18,10 @@ interface ItemProps {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelect?: () => void;
|
onSelect?: () => void;
|
||||||
zoomDepth?: number;
|
zoomDepth?: number;
|
||||||
|
zoomInDurationMs?: number;
|
||||||
|
zoomOutDurationMs?: number;
|
||||||
speedValue?: number;
|
speedValue?: number;
|
||||||
|
onZoomDurationChange?: (id: string, zoomIn: number, zoomOut: number) => void;
|
||||||
variant?: "zoom" | "trim" | "annotation" | "speed" | "blur";
|
variant?: "zoom" | "trim" | "annotation" | "speed" | "blur";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +52,14 @@ export default function Item({
|
|||||||
isSelected = false,
|
isSelected = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
zoomDepth = 1,
|
zoomDepth = 1,
|
||||||
|
zoomInDurationMs,
|
||||||
|
zoomOutDurationMs,
|
||||||
speedValue,
|
speedValue,
|
||||||
variant = "zoom",
|
variant = "zoom",
|
||||||
children,
|
children,
|
||||||
|
onZoomDurationChange,
|
||||||
}: ItemProps) {
|
}: ItemProps) {
|
||||||
|
const { pixelsToValue } = useTimelineContext();
|
||||||
const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem({
|
const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem({
|
||||||
id,
|
id,
|
||||||
span,
|
span,
|
||||||
@@ -79,6 +91,16 @@ export default function Item({
|
|||||||
const MIN_ITEM_PX = 6;
|
const MIN_ITEM_PX = 6;
|
||||||
const safeItemStyle = { ...itemStyle, minWidth: MIN_ITEM_PX };
|
const safeItemStyle = { ...itemStyle, minWidth: MIN_ITEM_PX };
|
||||||
|
|
||||||
|
const { zoomIn, zoomOut } = useMemo(() => {
|
||||||
|
if (!isZoom) return { zoomIn: 0, zoomOut: 0 };
|
||||||
|
return getDurations({
|
||||||
|
startMs: span.start,
|
||||||
|
endMs: span.end,
|
||||||
|
zoomInDurationMs,
|
||||||
|
zoomOutDurationMs,
|
||||||
|
});
|
||||||
|
}, [isZoom, span.start, span.end, zoomInDurationMs, zoomOutDurationMs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
@@ -101,6 +123,98 @@ export default function Item({
|
|||||||
onSelect?.();
|
onSelect?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isZoom && (
|
||||||
|
<>
|
||||||
|
{/* Transition In Marker */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 bottom-0 left-0 bg-white/10 border-r border-white/20 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
width: `${(zoomIn / (span.end - span.start)) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Draggable handle for Transition In */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 bottom-0 w-2 cursor-col-resize z-20 group-hover:bg-white/5 transition-colors"
|
||||||
|
style={{
|
||||||
|
left: `${(zoomIn / (span.end - span.start)) * 100}%`,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.currentTarget;
|
||||||
|
target.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
const startX = e.clientX;
|
||||||
|
const initialZoomIn = zoomInDurationMs ?? DEFAULT_ZOOM_IN_MS;
|
||||||
|
const initialZoomOut = zoomOutDurationMs ?? DEFAULT_ZOOM_OUT_MS;
|
||||||
|
|
||||||
|
const onPointerMove = (moveEvent: PointerEvent) => {
|
||||||
|
const deltaPx = moveEvent.clientX - startX;
|
||||||
|
const deltaMs = pixelsToValue(deltaPx);
|
||||||
|
const newDuration = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(initialZoomIn + deltaMs, span.end - span.start - initialZoomOut),
|
||||||
|
);
|
||||||
|
onZoomDurationChange?.(id, newDuration, initialZoomOut);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
target.releasePointerCapture(e.pointerId);
|
||||||
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Transition Out Marker */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 bottom-0 right-0 bg-white/10 border-l border-white/20 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
width: `${(zoomOut / (span.end - span.start)) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Draggable handle for Transition Out */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 bottom-0 w-2 cursor-col-resize z-20 group-hover:bg-white/5 transition-colors"
|
||||||
|
style={{
|
||||||
|
right: `${(zoomOut / (span.end - span.start)) * 100}%`,
|
||||||
|
transform: "translateX(50%)",
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.currentTarget;
|
||||||
|
target.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
const startX = e.clientX;
|
||||||
|
const initialZoomIn = zoomInDurationMs ?? DEFAULT_ZOOM_IN_MS;
|
||||||
|
const initialZoomOut = zoomOutDurationMs ?? DEFAULT_ZOOM_OUT_MS;
|
||||||
|
|
||||||
|
const onPointerMove = (moveEvent: PointerEvent) => {
|
||||||
|
const deltaPx = startX - moveEvent.clientX; // Inverted because right-anchored
|
||||||
|
const deltaMs = pixelsToValue(deltaPx);
|
||||||
|
const newDuration = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(initialZoomOut + deltaMs, span.end - span.start - initialZoomIn),
|
||||||
|
);
|
||||||
|
onZoomDurationChange?.(id, initialZoomIn, newDuration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
target.releasePointerCapture(e.pointerId);
|
||||||
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(glassStyles.zoomEndCap, glassStyles.left)}
|
className={cn(glassStyles.zoomEndCap, glassStyles.left)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ interface TimelineEditorProps {
|
|||||||
onZoomAdded: (span: Span) => void;
|
onZoomAdded: (span: Span) => void;
|
||||||
onZoomSuggested?: (span: Span, focus: ZoomFocus) => void;
|
onZoomSuggested?: (span: Span, focus: ZoomFocus) => void;
|
||||||
onZoomSpanChange: (id: string, span: Span) => void;
|
onZoomSpanChange: (id: string, span: Span) => void;
|
||||||
|
onZoomDurationChange: (id: string, zoomIn: number, zoomOut: number) => void;
|
||||||
onZoomDelete: (id: string) => void;
|
onZoomDelete: (id: string) => void;
|
||||||
selectedZoomId: string | null;
|
selectedZoomId: string | null;
|
||||||
onSelectZoom: (id: string | null) => void;
|
onSelectZoom: (id: string | null) => void;
|
||||||
@@ -103,6 +104,8 @@ interface TimelineRenderItem {
|
|||||||
label: string;
|
label: string;
|
||||||
zoomDepth?: number;
|
zoomDepth?: number;
|
||||||
speedValue?: number;
|
speedValue?: number;
|
||||||
|
zoomInDurationMs?: number;
|
||||||
|
zoomOutDurationMs?: number;
|
||||||
variant: "zoom" | "trim" | "annotation" | "speed" | "blur";
|
variant: "zoom" | "trim" | "annotation" | "speed" | "blur";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +542,7 @@ function Timeline({
|
|||||||
selectedAnnotationId,
|
selectedAnnotationId,
|
||||||
selectedBlurId,
|
selectedBlurId,
|
||||||
selectedSpeedId,
|
selectedSpeedId,
|
||||||
|
onZoomDurationChange,
|
||||||
keyframes = [],
|
keyframes = [],
|
||||||
}: {
|
}: {
|
||||||
items: TimelineRenderItem[];
|
items: TimelineRenderItem[];
|
||||||
@@ -556,6 +560,7 @@ function Timeline({
|
|||||||
selectedAnnotationId?: string | null;
|
selectedAnnotationId?: string | null;
|
||||||
selectedBlurId?: string | null;
|
selectedBlurId?: string | null;
|
||||||
selectedSpeedId?: string | null;
|
selectedSpeedId?: string | null;
|
||||||
|
onZoomDurationChange: (id: string, zoomIn: number, zoomOut: number) => void;
|
||||||
keyframes?: { id: string; time: number }[];
|
keyframes?: { id: string; time: number }[];
|
||||||
}) {
|
}) {
|
||||||
const t = useScopedT("timeline");
|
const t = useScopedT("timeline");
|
||||||
@@ -682,6 +687,9 @@ function Timeline({
|
|||||||
isSelected={item.id === selectedZoomId}
|
isSelected={item.id === selectedZoomId}
|
||||||
onSelect={() => onSelectZoom?.(item.id)}
|
onSelect={() => onSelectZoom?.(item.id)}
|
||||||
zoomDepth={item.zoomDepth}
|
zoomDepth={item.zoomDepth}
|
||||||
|
zoomInDurationMs={item.zoomInDurationMs}
|
||||||
|
zoomOutDurationMs={item.zoomOutDurationMs}
|
||||||
|
onZoomDurationChange={onZoomDurationChange}
|
||||||
variant="zoom"
|
variant="zoom"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -770,6 +778,7 @@ export default function TimelineEditor({
|
|||||||
onZoomAdded,
|
onZoomAdded,
|
||||||
onZoomSuggested,
|
onZoomSuggested,
|
||||||
onZoomSpanChange,
|
onZoomSpanChange,
|
||||||
|
onZoomDurationChange,
|
||||||
onZoomDelete,
|
onZoomDelete,
|
||||||
selectedZoomId,
|
selectedZoomId,
|
||||||
onSelectZoom,
|
onSelectZoom,
|
||||||
@@ -1338,6 +1347,8 @@ export default function TimelineEditor({
|
|||||||
span: { start: region.startMs, end: region.endMs },
|
span: { start: region.startMs, end: region.endMs },
|
||||||
label: t("labels.zoomItem", { index: String(index + 1) }),
|
label: t("labels.zoomItem", { index: String(index + 1) }),
|
||||||
zoomDepth: region.depth,
|
zoomDepth: region.depth,
|
||||||
|
zoomInDurationMs: region.zoomInDurationMs,
|
||||||
|
zoomOutDurationMs: region.zoomOutDurationMs,
|
||||||
variant: "zoom",
|
variant: "zoom",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1594,6 +1605,7 @@ export default function TimelineEditor({
|
|||||||
selectedAnnotationId={selectedAnnotationId}
|
selectedAnnotationId={selectedAnnotationId}
|
||||||
selectedBlurId={selectedBlurId}
|
selectedBlurId={selectedBlurId}
|
||||||
selectedSpeedId={selectedSpeedId}
|
selectedSpeedId={selectedSpeedId}
|
||||||
|
onZoomDurationChange={onZoomDurationChange}
|
||||||
keyframes={keyframes}
|
keyframes={keyframes}
|
||||||
/>
|
/>
|
||||||
</TimelineWrapper>
|
</TimelineWrapper>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export interface ZoomRegion {
|
|||||||
depth: ZoomDepth;
|
depth: ZoomDepth;
|
||||||
focus: ZoomFocus;
|
focus: ZoomFocus;
|
||||||
focusMode?: ZoomFocusMode;
|
focusMode?: ZoomFocusMode;
|
||||||
|
zoomInDurationMs?: number;
|
||||||
|
zoomOutDurationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CursorTelemetryPoint {
|
export interface CursorTelemetryPoint {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { clamp01, cubicBezier, easeOutScreenStudio } from "./mathUtils";
|
|||||||
|
|
||||||
const CHAINED_ZOOM_PAN_GAP_MS = 1500;
|
const CHAINED_ZOOM_PAN_GAP_MS = 1500;
|
||||||
const CONNECTED_ZOOM_PAN_DURATION_MS = 1000;
|
const CONNECTED_ZOOM_PAN_DURATION_MS = 1000;
|
||||||
const ZOOM_IN_OVERLAP_MS = 500;
|
|
||||||
|
|
||||||
type DominantRegionOptions = {
|
type DominantRegionOptions = {
|
||||||
connectZooms?: boolean;
|
connectZooms?: boolean;
|
||||||
@@ -38,26 +37,49 @@ function easeConnectedPan(value: number) {
|
|||||||
return cubicBezier(0.1, 0.0, 0.2, 1.0, value);
|
return cubicBezier(0.1, 0.0, 0.2, 1.0, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeRegionStrength(region: ZoomRegion, timeMs: number) {
|
export const DEFAULT_ZOOM_OUT_MS = TRANSITION_WINDOW_MS;
|
||||||
const zoomInEnd = region.startMs + ZOOM_IN_OVERLAP_MS;
|
export const DEFAULT_ZOOM_IN_MS = ZOOM_IN_TRANSITION_WINDOW_MS;
|
||||||
const leadInStart = zoomInEnd - ZOOM_IN_TRANSITION_WINDOW_MS;
|
|
||||||
const leadOutEnd = region.endMs + TRANSITION_WINDOW_MS;
|
|
||||||
|
|
||||||
if (timeMs < leadInStart || timeMs > leadOutEnd) {
|
export function getDurations(region: {
|
||||||
|
startMs: number;
|
||||||
|
endMs: number;
|
||||||
|
zoomInDurationMs?: number;
|
||||||
|
zoomOutDurationMs?: number;
|
||||||
|
}) {
|
||||||
|
let zoomIn = region.zoomInDurationMs ?? DEFAULT_ZOOM_IN_MS;
|
||||||
|
let zoomOut = region.zoomOutDurationMs ?? DEFAULT_ZOOM_OUT_MS;
|
||||||
|
|
||||||
|
const duration = region.endMs - region.startMs;
|
||||||
|
if (zoomIn + zoomOut > duration) {
|
||||||
|
const scale = duration / (zoomIn + zoomOut);
|
||||||
|
zoomIn *= scale;
|
||||||
|
zoomOut *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { zoomIn, zoomOut };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeRegionStrength(region: ZoomRegion, timeMs: number) {
|
||||||
|
const { zoomIn, zoomOut } = getDurations(region);
|
||||||
|
|
||||||
|
if (timeMs < region.startMs || timeMs > region.endMs) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeMs < zoomInEnd) {
|
// Zooming in
|
||||||
const progress = (timeMs - leadInStart) / ZOOM_IN_TRANSITION_WINDOW_MS;
|
if (timeMs < region.startMs + zoomIn) {
|
||||||
|
const progress = Math.max(0, Math.min(1, (timeMs - region.startMs) / zoomIn));
|
||||||
return easeOutScreenStudio(progress);
|
return easeOutScreenStudio(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeMs <= region.endMs) {
|
// Zooming out
|
||||||
return 1;
|
if (timeMs > region.endMs - zoomOut) {
|
||||||
|
const progress = Math.max(0, Math.min(1, (region.endMs - timeMs) / zoomOut));
|
||||||
|
return easeOutScreenStudio(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress = clamp01((timeMs - region.endMs) / TRANSITION_WINDOW_MS);
|
// Full zoom
|
||||||
return 1 - easeOutScreenStudio(progress);
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLinearFocus(start: ZoomFocus, end: ZoomFocus, amount: number): ZoomFocus {
|
function getLinearFocus(start: ZoomFocus, end: ZoomFocus, amount: number): ZoomFocus {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"autoDescription": "Camera follows the recorded cursor position"
|
"autoDescription": "Camera follows the recorded cursor position"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"title": "Zoom Speed",
|
||||||
|
"instant": "Instant",
|
||||||
|
"fast": "Fast",
|
||||||
|
"smooth": "Smooth",
|
||||||
|
"lazy": "Lazy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speed": {
|
"speed": {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"autoDescription": "La cámara sigue la posición del cursor grabado"
|
"autoDescription": "La cámara sigue la posición del cursor grabado"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"title": "Velocidad de zoom",
|
||||||
|
"instant": "Instantáneo",
|
||||||
|
"fast": "Rápido",
|
||||||
|
"smooth": "Suave",
|
||||||
|
"lazy": "Lento"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speed": {
|
"speed": {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
"manual": "手动",
|
"manual": "手动",
|
||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"autoDescription": "摄像头跟随录制时的光标位置"
|
"autoDescription": "摄像头跟随录制时的光标位置"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"title": "缩放速度",
|
||||||
|
"instant": "即时",
|
||||||
|
"fast": "快速",
|
||||||
|
"smooth": "平滑",
|
||||||
|
"lazy": "缓慢"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"speed": {
|
"speed": {
|
||||||
|
|||||||
Reference in New Issue
Block a user