diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 0260582720..ad566014d5 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -1018,7 +1018,7 @@ fn project_config_from_recording( } else { Vec::new() }, - layout_segments: Vec::new(), + scene_segments: Vec::new(), }), ..default_config.unwrap_or_default() } diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index de8c35011d..623da2b38e 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -147,10 +147,12 @@ function Page() { if (rawOptions.captureTarget.variant === "display") { const screenId = rawOptions.captureTarget.id; - screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; + screen = + _screens()?.find((s: any) => s.id === screenId) ?? _screens()?.[0]; } else if (rawOptions.captureTarget.variant === "area") { const screenId = rawOptions.captureTarget.screen; - screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; + screen = + _screens()?.find((s: any) => s.id === screenId) ?? _screens()?.[0]; } return screen; @@ -160,7 +162,8 @@ function Page() { if (rawOptions.captureTarget.variant === "window") { const windowId = rawOptions.captureTarget.id; - win = _windows()?.find((s) => s.id === windowId) ?? _windows()?.[0]; + win = + _windows()?.find((s: any) => s.id === windowId) ?? _windows()?.[0]; } return win; @@ -173,7 +176,7 @@ function Page() { if ("DeviceID" in cameraID && c.device_id === cameraID.DeviceID) return c; }), - micName: () => mics.data?.find((name) => name === rawOptions.micName), + micName: () => mics.data?.find((name: any) => name === rawOptions.micName), }; // if target is window and no windows are available, switch to screen capture diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 4ba780823e..1327530aa3 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -44,7 +44,7 @@ import { type BackgroundSource, type CameraShape, commands, - type LayoutSegment, + type SceneSegment, type StereoMode, type TimelineSegment, type ZoomSegment, @@ -671,18 +671,18 @@ export function ConfigSidebar() { { - const layoutSelection = selection(); - if (layoutSelection.type !== "layout") return; + const sceneSelection = selection(); + if (sceneSelection.type !== "scene") return; const segment = - project.timeline?.layoutSegments?.[layoutSelection.index]; + project.timeline?.sceneSegments?.[sceneSelection.index]; if (!segment) return; - return { selection: layoutSelection, segment }; + return { selection: sceneSelection, segment }; })()} > {(value) => ( - @@ -1839,7 +1839,9 @@ function ZoomSegmentPreview(props: { const video = document.createElement("video"); createEffect(() => { const path = convertFileSrc( - `${editorInstance.path}/content/segments/segment-${segmentIndex()}/display.mp4`, + `${ + editorInstance.path + }/content/segments/segment-${segmentIndex()}/display.mp4`, ); video.src = path; video.preload = "auto"; @@ -2282,9 +2284,9 @@ function ClipSegmentConfig(props: { ); } -function LayoutSegmentConfig(props: { +function SceneSegmentConfig(props: { segmentIndex: number; - segment: LayoutSegment; + segment: SceneSegment; }) { const { setProject, setEditorState, projectActions } = useEditorContext(); @@ -2302,7 +2304,7 @@ function LayoutSegmentConfig(props: { { - projectActions.deleteLayoutSegment(props.segmentIndex); + projectActions.deleteSceneSegment(props.segmentIndex); }} leftIcon={} > @@ -2316,7 +2318,7 @@ function LayoutSegmentConfig(props: { onChange={(v) => { setProject( "timeline", - "layoutSegments", + "sceneSegments", props.segmentIndex, "mode", v as "default" | "cameraOnly" | "hideCamera", diff --git a/apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx b/apps/desktop/src/routes/editor/Timeline/SceneTrack.tsx similarity index 85% rename from apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx rename to apps/desktop/src/routes/editor/Timeline/SceneTrack.tsx index 5e0e4ddaf6..4c12bc27f2 100644 --- a/apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/SceneTrack.tsx @@ -13,9 +13,7 @@ import { Show, } from "solid-js"; import { produce } from "solid-js/store"; -import IconLucideEyeOff from "~icons/lucide/eye-off"; -import IconLucideMonitor from "~icons/lucide/monitor"; -import IconLucideVideo from "~icons/lucide/video"; + import { useEditorContext } from "../context"; import { useSegmentContext, @@ -24,13 +22,13 @@ import { } from "./context"; import { SegmentContent, SegmentHandle, SegmentRoot, TrackRoot } from "./Track"; -export type LayoutSegmentDragState = +export type SceneSegmentDragState = | { type: "idle" } | { type: "movePending" } | { type: "moving" }; -export function LayoutTrack(props: { - onDragStateChanged: (v: LayoutSegmentDragState) => void; +export function SceneTrack(props: { + onDragStateChanged: (v: SceneSegmentDragState) => void; handleUpdatePlayhead: (e: MouseEvent) => void; }) { const { project, setProject, projectHistory, setEditorState, editorState } = @@ -49,13 +47,13 @@ export function LayoutTrack(props: { // users from creating new segments. This effect ensures we reset the hover state // when all segments are deleted. createEffect(() => { - const segments = project.timeline?.layoutSegments; + const segments = project.timeline?.sceneSegments; if (!segments || segments.length === 0) { setHoveringSegment(false); } }); - const getLayoutIcon = (mode: string | undefined) => { + const getSceneIcon = (mode: string | undefined) => { switch (mode) { case "cameraOnly": return ; @@ -66,7 +64,7 @@ export function LayoutTrack(props: { } }; - const getLayoutLabel = (mode: string | undefined) => { + const getSceneLabel = (mode: string | undefined) => { switch (mode) { case "cameraOnly": return "Camera Only"; @@ -91,7 +89,7 @@ export function LayoutTrack(props: { (e.clientX - bounds.left) * secsPerPixel() + editorState.timeline.transform.position; - const segments = project.timeline?.layoutSegments || []; + const segments = project.timeline?.sceneSegments || []; const nextSegmentIndex = segments.findIndex((s) => time < s.start); let maxDuration = 3; // Default duration @@ -158,23 +156,23 @@ export function LayoutTrack(props: { e.stopPropagation(); batch(() => { - setProject("timeline", "layoutSegments", (v) => v ?? []); + setProject("timeline", "sceneSegments", (v) => v ?? []); setProject( "timeline", - "layoutSegments", - produce((layoutSegments) => { - layoutSegments ??= []; + "sceneSegments", + produce((sceneSegments) => { + sceneSegments ??= []; - let index = layoutSegments.length; + let index = sceneSegments.length; - for (let i = layoutSegments.length - 1; i >= 0; i--) { - if (layoutSegments[i].start > time) { + for (let i = sceneSegments.length - 1; i >= 0; i--) { + if (sceneSegments[i].start > time) { index = i; break; } } - layoutSegments.splice(index, 0, { + sceneSegments.splice(index, 0, { start: time, end: time + maxDuration, mode: "cameraOnly", @@ -187,10 +185,10 @@ export function LayoutTrack(props: { }} > -
Click to add layout segment
+
Click to add scene segment
(Make the camera full screen, or hide it)
@@ -200,7 +198,7 @@ export function LayoutTrack(props: { {(segment, i) => { const { setTrackState } = useTrackContext(); - const layoutSegments = () => project.timeline!.layoutSegments!; + const sceneSegments = () => project.timeline!.sceneSegments!; function createMouseDownDrag( setup: () => T, @@ -225,13 +223,13 @@ export function LayoutTrack(props: { if (!moved) { e.stopPropagation(); setEditorState("timeline", "selection", { - type: "layout", + type: "scene", index: i(), }); props.handleUpdatePlayhead(e); } else { setEditorState("timeline", "selection", { - type: "layout", + type: "scene", index: i(), }); } @@ -272,9 +270,9 @@ export function LayoutTrack(props: { const isSelected = createMemo(() => { const selection = editorState.timeline.selection; - if (!selection || selection.type !== "layout") return false; + if (!selection || selection.type !== "scene") return false; - const segmentIndex = project.timeline?.layoutSegments?.findIndex( + const segmentIndex = project.timeline?.sceneSegments?.findIndex( (s) => s.start === segment.start && s.end === segment.end, ); @@ -309,8 +307,8 @@ export function LayoutTrack(props: { const maxValue = segment.end - 1; - for (let i = layoutSegments().length - 1; i >= 0; i--) { - const segment = layoutSegments()[i]!; + for (let i = sceneSegments().length - 1; i >= 0; i--) { + const segment = sceneSegments()[i]!; if (segment.end <= start) { minValue = segment.end; break; @@ -326,7 +324,7 @@ export function LayoutTrack(props: { setProject( "timeline", - "layoutSegments", + "sceneSegments", i(), "start", Math.min( @@ -337,7 +335,7 @@ export function LayoutTrack(props: { setProject( "timeline", - "layoutSegments", + "sceneSegments", produce((s) => { if (s) { s.sort((a, b) => a.start - b.start); @@ -353,8 +351,8 @@ export function LayoutTrack(props: { () => { const original = { ...segment }; - const prevSegment = layoutSegments()[i() - 1]; - const nextSegment = layoutSegments()[i() + 1]; + const prevSegment = sceneSegments()[i() - 1]; + const nextSegment = sceneSegments()[i() + 1]; const minStart = prevSegment?.end ?? 0; const maxEnd = nextSegment?.start ?? duration(); @@ -379,7 +377,7 @@ export function LayoutTrack(props: { else if (newEnd > value.maxEnd) delta = value.maxEnd - value.original.end; - setProject("timeline", "layoutSegments", i(), { + setProject("timeline", "sceneSegments", i(), { start: value.original.start + delta, end: value.original.end + delta, }); @@ -392,12 +390,12 @@ export function LayoutTrack(props: { return ( 80}>
- Layout + Scene
- {getLayoutIcon(segment.mode)} + {getSceneIcon(segment.mode)} {ctx.width() > 120 && ( - {getLayoutLabel(segment.mode)} + {getSceneLabel(segment.mode)} )}
@@ -416,8 +414,8 @@ export function LayoutTrack(props: { let maxValue = duration(); - for (let i = 0; i < layoutSegments().length; i++) { - const segment = layoutSegments()[i]!; + for (let i = 0; i < sceneSegments().length; i++) { + const segment = sceneSegments()[i]!; if (segment.start > end) { maxValue = segment.start; break; @@ -432,7 +430,7 @@ export function LayoutTrack(props: { setProject( "timeline", - "layoutSegments", + "sceneSegments", i(), "end", Math.min( @@ -443,7 +441,7 @@ export function LayoutTrack(props: { setProject( "timeline", - "layoutSegments", + "sceneSegments", produce((s) => { if (s) { s.sort((a, b) => a.start - b.start); diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 6c466d5dc9..133b24a11c 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -11,7 +11,7 @@ import { useEditorContext } from "../context"; import { formatTime } from "../utils"; import { ClipTrack } from "./ClipTrack"; import { TimelineContextProvider, useTimelineContext } from "./context"; -import { type LayoutSegmentDragState, LayoutTrack } from "./LayoutTrack"; +import { type SceneSegmentDragState, SceneTrack } from "./SceneTrack"; import { type ZoomSegmentDragState, ZoomTrack } from "./ZoomTrack"; const TIMELINE_PADDING = 16; @@ -74,13 +74,13 @@ export function Timeline() { } let zoomSegmentDragState = { type: "idle" } as ZoomSegmentDragState; - let layoutSegmentDragState = { type: "idle" } as LayoutSegmentDragState; + let sceneSegmentDragState = { type: "idle" } as SceneSegmentDragState; async function handleUpdatePlayhead(e: MouseEvent) { const { left } = timelineBounds; if ( zoomSegmentDragState.type !== "moving" && - layoutSegmentDragState.type !== "moving" + sceneSegmentDragState.type !== "moving" ) { setEditorState( "playbackTime", @@ -103,8 +103,8 @@ export function Timeline() { projectActions.deleteZoomSegments(selection.indices); } else if (selection.type === "clip") { projectActions.deleteClipSegment(selection.index); - } else if (selection.type === "layout") { - projectActions.deleteLayoutSegment(selection.index); + } else if (selection.type === "scene") { + projectActions.deleteSceneSegment(selection.index); } } else if (e.code === "KeyC" && hasNoModifiers) { if (!editorState.previewTime) return; @@ -242,9 +242,9 @@ export function Timeline() { handleUpdatePlayhead={handleUpdatePlayhead} /> - { - layoutSegmentDragState = v; + sceneSegmentDragState = v; }} handleUpdatePlayhead={handleUpdatePlayhead} /> diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 7a2c10dac7..a504fc7a67 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -146,11 +146,11 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( setEditorState("timeline", "selection", null); }); }, - deleteLayoutSegment: (segmentIndex: number) => { + deleteSceneSegment: (segmentIndex: number) => { batch(() => { setProject( "timeline", - "layoutSegments", + "sceneSegments", produce((s) => { if (!s) return; s.splice(segmentIndex, 1); @@ -257,7 +257,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( | null | { type: "zoom"; indices: number[] } | { type: "clip"; index: number } - | { type: "layout"; index: number }, + | { type: "scene"; index: number }, transform: { // visible seconds zoom: zoomOutLimit(), diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index bc97c7a8f4..c02d36db54 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -388,8 +388,6 @@ export type HotkeysConfiguration = { show: boolean } export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } export type InstantRecordingMeta = { fps: number; sample_rate: number | null } export type JsonValue = [T] -export type LayoutMode = "default" | "cameraOnly" | "hideCamera" -export type LayoutSegment = { start: number; end: number; mode?: LayoutMode } export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } export type LogicalPosition = { x: number; y: number } export type LogicalSize = { width: number; height: number } @@ -427,6 +425,8 @@ export type RequestNewScreenshot = null export type RequestOpenSettings = { page: string } export type RequestStartRecording = null export type S3UploadMeta = { id: string } +export type SceneMode = "default" | "cameraOnly" | "hideCamera" +export type SceneSegment = { start: number; end: number; mode?: SceneMode } export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "display"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } @@ -438,7 +438,7 @@ export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; captur export type StereoMode = "stereo" | "monoL" | "monoR" export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } -export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; layoutSegments?: LayoutSegment[] } +export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[] } export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" export type UploadProgress = { progress: number } diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs index 148f3631dc..432be71bf5 100644 --- a/crates/project/src/configuration.rs +++ b/crates/project/src/configuration.rs @@ -459,7 +459,7 @@ pub enum ZoomMode { #[derive(Type, Serialize, Deserialize, Clone, Copy, Debug, Default)] #[serde(rename_all = "camelCase")] -pub enum LayoutMode { +pub enum SceneMode { #[default] Default, CameraOnly, @@ -468,11 +468,11 @@ pub enum LayoutMode { #[derive(Type, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] -pub struct LayoutSegment { +pub struct SceneSegment { pub start: f64, pub end: f64, #[serde(default)] - pub mode: LayoutMode, + pub mode: SceneMode, } #[derive(Type, Serialize, Deserialize, Clone, Debug)] @@ -481,7 +481,7 @@ pub struct TimelineConfiguration { pub segments: Vec, pub zoom_segments: Vec, #[serde(default)] - pub layout_segments: Vec, + pub scene_segments: Vec, } impl TimelineConfiguration { diff --git a/crates/rendering/src/layout.rs b/crates/rendering/src/layout.rs deleted file mode 100644 index 1e6619dcaa..0000000000 --- a/crates/rendering/src/layout.rs +++ /dev/null @@ -1,270 +0,0 @@ -use cap_project::{LayoutMode, LayoutSegment}; - -pub const LAYOUT_TRANSITION_DURATION: f64 = 0.3; - -#[derive(Debug, Clone, Copy)] -pub struct LayoutSegmentsCursor<'a> { - time: f64, - segment: Option<&'a LayoutSegment>, - prev_segment: Option<&'a LayoutSegment>, - segments: &'a [LayoutSegment], -} - -impl<'a> LayoutSegmentsCursor<'a> { - pub fn new(time: f64, segments: &'a [LayoutSegment]) -> Self { - match segments - .iter() - .position(|s| time >= s.start && time < s.end) - { - Some(segment_index) => LayoutSegmentsCursor { - time, - segment: Some(&segments[segment_index]), - prev_segment: if segment_index > 0 { - Some(&segments[segment_index - 1]) - } else { - None - }, - segments, - }, - None => { - let prev = segments - .iter() - .enumerate() - .rev() - .find(|(_, s)| s.end <= time); - LayoutSegmentsCursor { - time, - segment: None, - prev_segment: prev.map(|(_, s)| s), - segments, - } - } - } - } - - pub fn next_segment(&self) -> Option<&'a LayoutSegment> { - let current_time = self.time; - self.segments.iter().find(|s| s.start > current_time) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct InterpolatedLayout { - pub camera_opacity: f64, - pub screen_opacity: f64, - pub camera_scale: f64, - pub layout_mode: LayoutMode, - pub transition_progress: f64, - pub from_mode: LayoutMode, - pub to_mode: LayoutMode, - pub screen_blur: f64, - pub camera_only_zoom: f64, - pub camera_only_blur: f64, -} - -impl InterpolatedLayout { - pub fn new(cursor: LayoutSegmentsCursor) -> Self { - let ease_in_out = bezier_easing::bezier_easing(0.42, 0.0, 0.58, 1.0).unwrap(); - - let (current_mode, next_mode, transition_progress) = if let Some(segment) = cursor.segment { - let transition_start = segment.start - LAYOUT_TRANSITION_DURATION; - let transition_end = segment.end - LAYOUT_TRANSITION_DURATION; - - if cursor.time < segment.start && cursor.time >= transition_start { - let prev_mode = cursor - .prev_segment - .map(|s| s.mode) - .unwrap_or(LayoutMode::Default); - let progress = (cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION; - (prev_mode, segment.mode, ease_in_out(progress as f32) as f64) - } else if cursor.time >= transition_end && cursor.time < segment.end { - if let Some(next_seg) = cursor.next_segment() { - let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION; - ( - segment.mode, - next_seg.mode, - ease_in_out(progress as f32) as f64, - ) - } else { - let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION; - ( - segment.mode, - LayoutMode::Default, - ease_in_out(progress as f32) as f64, - ) - } - } else { - (segment.mode, segment.mode, 1.0) - } - } else if let Some(next_segment) = cursor.next_segment() { - let transition_start = next_segment.start - LAYOUT_TRANSITION_DURATION; - if cursor.time >= transition_start { - let prev_mode = cursor - .prev_segment - .map(|s| s.mode) - .unwrap_or(LayoutMode::Default); - let progress = (cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION; - ( - prev_mode, - next_segment.mode, - ease_in_out(progress as f32) as f64, - ) - } else if let Some(prev_segment) = cursor.prev_segment { - if cursor.time < prev_segment.end + 0.05 { - (prev_segment.mode, LayoutMode::Default, 1.0) - } else { - (LayoutMode::Default, LayoutMode::Default, 1.0) - } - } else { - (LayoutMode::Default, LayoutMode::Default, 1.0) - } - } else if let Some(prev_segment) = cursor.prev_segment { - if cursor.time < prev_segment.end + 0.05 { - (prev_segment.mode, LayoutMode::Default, 1.0) - } else { - (LayoutMode::Default, LayoutMode::Default, 1.0) - } - } else { - (LayoutMode::Default, LayoutMode::Default, 1.0) - }; - - let (start_camera_opacity, start_screen_opacity, start_camera_scale) = - Self::get_layout_values(¤t_mode); - let (end_camera_opacity, end_screen_opacity, end_camera_scale) = - Self::get_layout_values(&next_mode); - - let camera_opacity = Self::lerp( - start_camera_opacity, - end_camera_opacity, - transition_progress, - ); - let screen_opacity = Self::lerp( - start_screen_opacity, - end_screen_opacity, - transition_progress, - ); - let camera_scale = Self::lerp(start_camera_scale, end_camera_scale, transition_progress); - - let screen_blur = if matches!(current_mode, LayoutMode::CameraOnly) - || matches!(next_mode, LayoutMode::CameraOnly) - { - if matches!(current_mode, LayoutMode::CameraOnly) - && !matches!(next_mode, LayoutMode::CameraOnly) - { - Self::lerp(1.0, 0.0, transition_progress) - } else if !matches!(current_mode, LayoutMode::CameraOnly) - && matches!(next_mode, LayoutMode::CameraOnly) - { - transition_progress - } else { - 0.0 - } - } else { - 0.0 - }; - - let camera_only_zoom = if matches!(next_mode, LayoutMode::CameraOnly) - && !matches!(current_mode, LayoutMode::CameraOnly) - { - Self::lerp(1.1, 1.0, transition_progress) - } else if matches!(current_mode, LayoutMode::CameraOnly) - && !matches!(next_mode, LayoutMode::CameraOnly) - { - Self::lerp(1.0, 1.1, transition_progress) - } else { - 1.0 - }; - - let camera_only_blur = if matches!(next_mode, LayoutMode::CameraOnly) - && !matches!(current_mode, LayoutMode::CameraOnly) - { - Self::lerp(1.0, 0.0, transition_progress) - } else if matches!(current_mode, LayoutMode::CameraOnly) - && !matches!(next_mode, LayoutMode::CameraOnly) - { - transition_progress - } else { - 0.0 - }; - - InterpolatedLayout { - camera_opacity, - screen_opacity, - camera_scale, - layout_mode: if transition_progress > 0.5 { - next_mode - } else { - current_mode - }, - transition_progress, - from_mode: current_mode, - to_mode: next_mode, - screen_blur, - camera_only_zoom, - camera_only_blur, - } - } - - fn get_layout_values(mode: &LayoutMode) -> (f64, f64, f64) { - match mode { - LayoutMode::Default => (1.0, 1.0, 1.0), - LayoutMode::CameraOnly => (1.0, 1.0, 1.0), - LayoutMode::HideCamera => (0.0, 1.0, 1.0), - } - } - - fn lerp(start: f64, end: f64, t: f64) -> f64 { - start + (end - start) * t - } - - pub fn should_render_camera(&self) -> bool { - self.camera_opacity > 0.01 - } - - pub fn should_render_screen(&self) -> bool { - true - } - - pub fn is_transitioning_camera_only(&self) -> bool { - matches!(self.from_mode, LayoutMode::CameraOnly) - || matches!(self.to_mode, LayoutMode::CameraOnly) - } - - pub fn camera_only_transition_opacity(&self) -> f64 { - if matches!(self.from_mode, LayoutMode::CameraOnly) - && !matches!(self.to_mode, LayoutMode::CameraOnly) - { - 1.0 - self.transition_progress - } else if !matches!(self.from_mode, LayoutMode::CameraOnly) - && matches!(self.to_mode, LayoutMode::CameraOnly) - { - self.transition_progress - } else if matches!(self.from_mode, LayoutMode::CameraOnly) - && matches!(self.to_mode, LayoutMode::CameraOnly) - { - 1.0 - } else { - 0.0 - } - } - - pub fn regular_camera_transition_opacity(&self) -> f64 { - if matches!(self.to_mode, LayoutMode::CameraOnly) - && !matches!(self.from_mode, LayoutMode::CameraOnly) - { - let fast_fade = (1.0 - self.transition_progress * 1.5).max(0.0); - fast_fade * self.camera_opacity - } else if matches!(self.from_mode, LayoutMode::CameraOnly) - && !matches!(self.to_mode, LayoutMode::CameraOnly) - { - let fast_fade = (self.transition_progress * 1.5).min(1.0); - fast_fade * self.camera_opacity - } else if matches!(self.from_mode, LayoutMode::CameraOnly) - && matches!(self.to_mode, LayoutMode::CameraOnly) - { - 0.0 - } else { - self.camera_opacity - } - } -} diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 2e539a319d..52ac314afd 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -26,8 +26,8 @@ mod cursor_interpolation; pub mod decoder; mod frame_pipeline; mod layers; -mod layout; mod project_recordings; +mod scene; mod spring_mass_damper; mod zoom; @@ -36,7 +36,7 @@ pub use decoder::DecodedFrame; pub use frame_pipeline::RenderedFrame; pub use project_recordings::{ProjectRecordingsMeta, SegmentRecordings}; -use layout::*; +use scene::*; use zoom::*; const STANDARD_CURSOR_HEIGHT: f32 = 75.0; @@ -345,7 +345,7 @@ pub struct ProjectUniforms { interpolated_cursor: Option, pub project: ProjectConfiguration, pub zoom: InterpolatedZoom, - pub layout: InterpolatedLayout, + pub scene: InterpolatedScene, pub resolution_base: XY, } @@ -565,12 +565,12 @@ impl ProjectUniforms { .unwrap_or_else(|| Coord::new(XY::new(0.5, 0.5))), ); - let layout = InterpolatedLayout::new(LayoutSegmentsCursor::new( + let scene = InterpolatedScene::new(SceneSegmentsCursor::new( frame_time as f64, project .timeline .as_ref() - .map(|t| t.layout_segments.as_slice()) + .map(|t| t.scene_segments.as_slice()) .unwrap_or(&[]), )); @@ -617,7 +617,7 @@ impl ProjectUniforms { rounding_px: (project.background.rounding / 100.0 * 0.5 * min_target_axis) as f32, mirror_x: 0.0, velocity_uv: velocity, - motion_blur_amount: (motion_blur_amount + layout.screen_blur as f32 * 0.8).min(1.0), + motion_blur_amount: (motion_blur_amount + scene.screen_blur as f32 * 0.8).min(1.0), camera_motion_blur_amount: 0.0, shadow: project.background.shadow, shadow_size: project @@ -635,14 +635,14 @@ impl ProjectUniforms { .advanced_shadow .as_ref() .map_or(50.0, |s| s.blur), - opacity: layout.screen_opacity as f32, + opacity: scene.screen_opacity as f32, _padding: [0.0; 3], } }; let camera = options .camera_size - .filter(|_| !project.camera.hide && layout.should_render_camera()) + .filter(|_| !project.camera.hide && scene.should_render_camera()) .map(|camera_size| { let output_size = [output_size.0 as f32, output_size.1 as f32]; let frame_size = [camera_size.x as f32, camera_size.y as f32]; @@ -658,7 +658,7 @@ impl ProjectUniforms { let zoomed_size = (zoom.t as f32) * zoom_size * base_size + (1.0 - zoom.t as f32) * base_size; - let zoomed_size = zoomed_size * layout.camera_scale as f32; + let zoomed_size = zoomed_size * scene.camera_scale as f32; let aspect = frame_size[0] / frame_size[1]; let size = match project.camera.shape { @@ -744,14 +744,14 @@ impl ProjectUniforms { .advanced_shadow .as_ref() .map_or(50.0, |s| s.blur), - opacity: layout.regular_camera_transition_opacity() as f32, + opacity: scene.regular_camera_transition_opacity() as f32, _padding: [0.0; 3], } }); let camera_only = options .camera_size - .filter(|_| !project.camera.hide && layout.is_transitioning_camera_only()) + .filter(|_| !project.camera.hide && scene.is_transitioning_camera_only()) .map(|camera_size| { let output_size = [output_size.0 as f32, output_size.1 as f32]; let frame_size = [camera_size.x as f32, camera_size.y as f32]; @@ -759,7 +759,7 @@ impl ProjectUniforms { let aspect = frame_size[0] / frame_size[1]; let output_aspect = output_size[0] / output_size[1]; - let zoom_factor = layout.camera_only_zoom as f32; + let zoom_factor = scene.camera_only_zoom as f32; let size = [output_size[0] * zoom_factor, output_size[1] * zoom_factor]; let position = [ @@ -802,12 +802,12 @@ impl ProjectUniforms { mirror_x: if project.camera.mirror { 1.0 } else { 0.0 }, velocity_uv: [0.0, 0.0], motion_blur_amount: 0.0, - camera_motion_blur_amount: layout.camera_only_blur as f32 * 0.5, + camera_motion_blur_amount: scene.camera_only_blur as f32 * 0.5, shadow: 0.0, shadow_size: 0.0, shadow_opacity: 0.0, shadow_blur: 0.0, - opacity: layout.camera_only_transition_opacity() as f32, + opacity: scene.camera_only_transition_opacity() as f32, _padding: [0.0; 3], } }); @@ -821,7 +821,7 @@ impl ProjectUniforms { camera_only, project: project.clone(), zoom, - layout, + scene, interpolated_cursor, } } @@ -1009,25 +1009,25 @@ impl RendererLayers { session.swap_textures(); } - if uniforms.layout.should_render_screen() { + if uniforms.scene.should_render_screen() { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.display.render(&mut pass); } - if uniforms.layout.should_render_screen() { + if uniforms.scene.should_render_screen() { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.cursor.render(&mut pass); } // Render camera-only layer when transitioning with CameraOnly mode - if uniforms.layout.is_transitioning_camera_only() { + if uniforms.scene.is_transitioning_camera_only() { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.camera_only.render(&mut pass); } // Also render regular camera overlay during transitions when its opacity > 0 - if uniforms.layout.should_render_camera() - && uniforms.layout.regular_camera_transition_opacity() > 0.01 + if uniforms.scene.should_render_camera() + && uniforms.scene.regular_camera_transition_opacity() > 0.01 { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.camera.render(&mut pass); diff --git a/crates/rendering/src/scene.rs b/crates/rendering/src/scene.rs new file mode 100644 index 0000000000..94500b07d9 --- /dev/null +++ b/crates/rendering/src/scene.rs @@ -0,0 +1,352 @@ +use cap_project::{SceneMode, SceneSegment}; + +pub const SCENE_TRANSITION_DURATION: f64 = 0.3; +pub const MIN_GAP_FOR_TRANSITION: f64 = 0.5; + +#[derive(Debug, Clone, Copy)] +pub struct SceneSegmentsCursor<'a> { + time: f64, + segment: Option<&'a SceneSegment>, + prev_segment: Option<&'a SceneSegment>, + segments: &'a [SceneSegment], +} + +impl<'a> SceneSegmentsCursor<'a> { + pub fn new(time: f64, segments: &'a [SceneSegment]) -> Self { + match segments + .iter() + .position(|s| time >= s.start && time < s.end) + { + Some(segment_index) => SceneSegmentsCursor { + time, + segment: Some(&segments[segment_index]), + prev_segment: if segment_index > 0 { + Some(&segments[segment_index - 1]) + } else { + None + }, + segments, + }, + None => { + let prev = segments + .iter() + .enumerate() + .rev() + .find(|(_, s)| s.end <= time); + SceneSegmentsCursor { + time, + segment: None, + prev_segment: prev.map(|(_, s)| s), + segments, + } + } + } + } + + pub fn next_segment(&self) -> Option<&'a SceneSegment> { + let current_time = self.time; + self.segments.iter().find(|s| s.start > current_time) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct InterpolatedScene { + pub camera_opacity: f64, + pub screen_opacity: f64, + pub camera_scale: f64, + pub scene_mode: SceneMode, + pub transition_progress: f64, + pub from_mode: SceneMode, + pub to_mode: SceneMode, + pub screen_blur: f64, + pub camera_only_zoom: f64, + pub camera_only_blur: f64, +} + +impl InterpolatedScene { + fn from_single_mode(mode: SceneMode) -> Self { + let (camera_opacity, screen_opacity, camera_scale) = Self::get_scene_values(&mode); + + InterpolatedScene { + camera_opacity, + screen_opacity, + camera_scale, + scene_mode: mode.clone(), + transition_progress: 1.0, + from_mode: mode.clone(), + to_mode: mode, + screen_blur: 0.0, + camera_only_zoom: 1.0, + camera_only_blur: 0.0, + } + } + + pub fn new(cursor: SceneSegmentsCursor) -> Self { + let ease_in_out = bezier_easing::bezier_easing(0.42, 0.0, 0.58, 1.0).unwrap(); + + let (current_mode, next_mode, transition_progress) = if let Some(segment) = cursor.segment { + let transition_start = segment.start - SCENE_TRANSITION_DURATION; + let transition_end = segment.end - SCENE_TRANSITION_DURATION; + + if cursor.time < segment.start && cursor.time >= transition_start { + // Check if we should skip transition for small gaps + let prev_mode = if let Some(prev_seg) = cursor.prev_segment { + let gap = segment.start - prev_seg.end; + let same_mode = matches!( + (&prev_seg.mode, &segment.mode), + (SceneMode::CameraOnly, SceneMode::CameraOnly) + | (SceneMode::Default, SceneMode::Default) + | (SceneMode::HideCamera, SceneMode::HideCamera) + ); + if gap < MIN_GAP_FOR_TRANSITION && same_mode { + // Small gap between same modes, no transition needed + return InterpolatedScene::from_single_mode(segment.mode.clone()); + } else if gap > 0.01 { + SceneMode::Default + } else { + prev_seg.mode.clone() + } + } else { + SceneMode::Default + }; + let progress = (cursor.time - transition_start) / SCENE_TRANSITION_DURATION; + ( + prev_mode, + segment.mode.clone(), + ease_in_out(progress as f32) as f64, + ) + } else if cursor.time >= transition_end && cursor.time < segment.end { + if let Some(next_seg) = cursor.next_segment() { + let gap = next_seg.start - segment.end; + + // For small gaps between same-mode segments, don't transition + let same_mode = matches!( + (&segment.mode, &next_seg.mode), + (SceneMode::CameraOnly, SceneMode::CameraOnly) + | (SceneMode::Default, SceneMode::Default) + | (SceneMode::HideCamera, SceneMode::HideCamera) + ); + if gap < MIN_GAP_FOR_TRANSITION && same_mode { + // Keep the current mode without transitioning + (segment.mode.clone(), segment.mode.clone(), 1.0) + } else if gap > 0.01 { + // There's a significant gap, so transition to default scene + let progress = + ((cursor.time - transition_end) / SCENE_TRANSITION_DURATION).min(1.0); + ( + segment.mode.clone(), + SceneMode::Default, + ease_in_out(progress as f32) as f64, + ) + } else { + // No gap, segments are back-to-back, transition directly if modes differ + let progress = + ((cursor.time - transition_end) / SCENE_TRANSITION_DURATION).min(1.0); + ( + segment.mode.clone(), + next_seg.mode.clone(), + ease_in_out(progress as f32) as f64, + ) + } + } else { + // No next segment, transition to default + let progress = + ((cursor.time - transition_end) / SCENE_TRANSITION_DURATION).min(1.0); + ( + segment.mode.clone(), + SceneMode::Default, + ease_in_out(progress as f32) as f64, + ) + } + } else { + (segment.mode.clone(), segment.mode.clone(), 1.0) + } + } else if let Some(next_segment) = cursor.next_segment() { + let transition_start = next_segment.start - SCENE_TRANSITION_DURATION; + + if let Some(prev_seg) = cursor.prev_segment { + let gap = next_segment.start - prev_seg.end; + + // For small gaps between same-mode segments, stay in that mode + let same_mode = matches!( + (&prev_seg.mode, &next_segment.mode), + (SceneMode::CameraOnly, SceneMode::CameraOnly) + | (SceneMode::Default, SceneMode::Default) + | (SceneMode::HideCamera, SceneMode::HideCamera) + ); + if gap < MIN_GAP_FOR_TRANSITION && same_mode { + (prev_seg.mode.clone(), prev_seg.mode.clone(), 1.0) + } else if cursor.time >= transition_start { + // Start transitioning into the next segment + let prev_mode = if gap > 0.01 { + SceneMode::Default + } else { + prev_seg.mode.clone() + }; + let progress = (cursor.time - transition_start) / SCENE_TRANSITION_DURATION; + ( + prev_mode, + next_segment.mode.clone(), + ease_in_out(progress as f32) as f64, + ) + } else { + // We're in a gap that requires transition - should be at default + (SceneMode::Default, SceneMode::Default, 1.0) + } + } else if cursor.time >= transition_start { + // No previous segment, transitioning into the first segment + let progress = (cursor.time - transition_start) / SCENE_TRANSITION_DURATION; + ( + SceneMode::Default, + next_segment.mode.clone(), + ease_in_out(progress as f32) as f64, + ) + } else { + (SceneMode::Default, SceneMode::Default, 1.0) + } + } else { + // No next segment (at the end of timeline) + // The transition should have already completed inside the last segment + (SceneMode::Default, SceneMode::Default, 1.0) + }; + + let (start_camera_opacity, start_screen_opacity, start_camera_scale) = + Self::get_scene_values(¤t_mode); + let (end_camera_opacity, end_screen_opacity, end_camera_scale) = + Self::get_scene_values(&next_mode); + + let camera_opacity = Self::lerp( + start_camera_opacity, + end_camera_opacity, + transition_progress, + ); + let screen_opacity = Self::lerp( + start_screen_opacity, + end_screen_opacity, + transition_progress, + ); + let camera_scale = Self::lerp(start_camera_scale, end_camera_scale, transition_progress); + + let screen_blur = if matches!(current_mode, SceneMode::CameraOnly) + || matches!(next_mode, SceneMode::CameraOnly) + { + if matches!(current_mode, SceneMode::CameraOnly) + && !matches!(next_mode, SceneMode::CameraOnly) + { + Self::lerp(1.0, 0.0, transition_progress) + } else if !matches!(current_mode, SceneMode::CameraOnly) + && matches!(next_mode, SceneMode::CameraOnly) + { + transition_progress + } else { + 0.0 + } + } else { + 0.0 + }; + + let camera_only_zoom = if matches!(next_mode, SceneMode::CameraOnly) + && !matches!(current_mode, SceneMode::CameraOnly) + { + Self::lerp(1.1, 1.0, transition_progress) + } else if matches!(current_mode, SceneMode::CameraOnly) + && !matches!(next_mode, SceneMode::CameraOnly) + { + Self::lerp(1.0, 1.1, transition_progress) + } else { + 1.0 + }; + + let camera_only_blur = if matches!(next_mode, SceneMode::CameraOnly) + && !matches!(current_mode, SceneMode::CameraOnly) + { + Self::lerp(1.0, 0.0, transition_progress) + } else if matches!(current_mode, SceneMode::CameraOnly) + && !matches!(next_mode, SceneMode::CameraOnly) + { + transition_progress + } else { + 0.0 + }; + + InterpolatedScene { + camera_opacity, + screen_opacity, + camera_scale, + scene_mode: if transition_progress > 0.5 { + next_mode.clone() + } else { + current_mode.clone() + }, + transition_progress, + from_mode: current_mode, + to_mode: next_mode, + screen_blur, + camera_only_zoom, + camera_only_blur, + } + } + + fn get_scene_values(mode: &SceneMode) -> (f64, f64, f64) { + match mode { + SceneMode::Default => (1.0, 1.0, 1.0), + SceneMode::CameraOnly => (1.0, 1.0, 1.0), + SceneMode::HideCamera => (0.0, 1.0, 1.0), + } + } + + fn lerp(start: f64, end: f64, t: f64) -> f64 { + start + (end - start) * t + } + + pub fn should_render_camera(&self) -> bool { + self.camera_opacity > 0.01 + } + + pub fn should_render_screen(&self) -> bool { + self.screen_opacity > 0.01 || self.screen_blur > 0.01 + } + + pub fn is_transitioning_camera_only(&self) -> bool { + matches!(self.from_mode, SceneMode::CameraOnly) + || matches!(self.to_mode, SceneMode::CameraOnly) + } + + pub fn camera_only_transition_opacity(&self) -> f64 { + if matches!(self.from_mode, SceneMode::CameraOnly) + && !matches!(self.to_mode, SceneMode::CameraOnly) + { + 1.0 - self.transition_progress + } else if !matches!(self.from_mode, SceneMode::CameraOnly) + && matches!(self.to_mode, SceneMode::CameraOnly) + { + self.transition_progress + } else if matches!(self.from_mode, SceneMode::CameraOnly) + && matches!(self.to_mode, SceneMode::CameraOnly) + { + 1.0 + } else { + 0.0 + } + } + + pub fn regular_camera_transition_opacity(&self) -> f64 { + if matches!(self.to_mode, SceneMode::CameraOnly) + && !matches!(self.from_mode, SceneMode::CameraOnly) + { + let fast_fade = (1.0 - self.transition_progress * 1.5).max(0.0); + fast_fade * self.camera_opacity + } else if matches!(self.from_mode, SceneMode::CameraOnly) + && !matches!(self.to_mode, SceneMode::CameraOnly) + { + let fast_fade = (self.transition_progress * 1.5).min(1.0); + fast_fade * self.camera_opacity + } else if matches!(self.from_mode, SceneMode::CameraOnly) + && matches!(self.to_mode, SceneMode::CameraOnly) + { + 0.0 + } else { + self.camera_opacity + } + } +} diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index d904ed4438..5062bce9af 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -7,83 +7,84 @@ export {} declare global { const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"] - const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"] - const IconCapBgBlur: typeof import("~icons/cap/bg-blur.jsx")["default"] + const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] + const IconCapBgBlur: typeof import('~icons/cap/bg-blur.jsx')['default'] const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] - const IconCapCaptions: typeof import("~icons/cap/captions.jsx")["default"] + const IconCapCaptions: typeof import('~icons/cap/captions.jsx')['default'] const IconCapCaretDown: typeof import("~icons/cap/caret-down.jsx")["default"] - const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"] + const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"] - const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"] - const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"] - const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"] - const IconCapCopy: typeof import("~icons/cap/copy.jsx")["default"] - const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"] - const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"] - const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"] + const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] + const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] + const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] + const IconCapCopy: typeof import('~icons/cap/copy.jsx')['default'] + const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] + const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] + const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"] - const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"] - const IconCapFile: typeof import("~icons/cap/file.jsx")["default"] - const IconCapFilmCut: typeof import("~icons/cap/film-cut.jsx")["default"] - const IconCapGauge: typeof import("~icons/cap/gauge.jsx")["default"] + const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] + const IconCapFile: typeof import('~icons/cap/file.jsx')['default'] + const IconCapFilmCut: typeof import('~icons/cap/film-cut.jsx')['default'] + const IconCapGauge: typeof import('~icons/cap/gauge.jsx')['default'] const IconCapGear: typeof import("~icons/cap/gear.jsx")["default"] - const IconCapHotkeys: typeof import("~icons/cap/hotkeys.jsx")["default"] - const IconCapImage: typeof import("~icons/cap/image.jsx")["default"] + const IconCapHotkeys: typeof import('~icons/cap/hotkeys.jsx')['default'] + const IconCapImage: typeof import('~icons/cap/image.jsx')['default'] const IconCapInfo: typeof import("~icons/cap/info.jsx")["default"] - const IconCapInstant: typeof import("~icons/cap/instant.jsx")["default"] - const IconCapLayout: typeof import("~icons/cap/layout.jsx")["default"] - const IconCapLink: typeof import("~icons/cap/link.jsx")["default"] + const IconCapInstant: typeof import('~icons/cap/instant.jsx')['default'] + const IconCapLayout: typeof import('~icons/cap/layout.jsx')['default'] + const IconCapLink: typeof import('~icons/cap/link.jsx')['default'] const IconCapLogo: typeof import('~icons/cap/logo.jsx')['default'] const IconCapLogoFull: typeof import("~icons/cap/logo-full.jsx")["default"] const IconCapLogoFullDark: typeof import("~icons/cap/logo-full-dark.jsx")["default"] - const IconCapMessageBubble: typeof import("~icons/cap/message-bubble.jsx")["default"] + const IconCapMessageBubble: typeof import('~icons/cap/message-bubble.jsx')['default'] const IconCapMicrophone: typeof import('~icons/cap/microphone.jsx')['default'] const IconCapMoreVertical: typeof import("~icons/cap/more-vertical.jsx")["default"] - const IconCapNext: typeof import("~icons/cap/next.jsx")["default"] - const IconCapPadding: typeof import("~icons/cap/padding.jsx")["default"] - const IconCapPause: typeof import("~icons/cap/pause.jsx")["default"] + const IconCapNext: typeof import('~icons/cap/next.jsx')['default'] + const IconCapPadding: typeof import('~icons/cap/padding.jsx')['default'] + const IconCapPause: typeof import('~icons/cap/pause.jsx')['default'] const IconCapPauseCircle: typeof import("~icons/cap/pause-circle.jsx")["default"] - const IconCapPlay: typeof import("~icons/cap/play.jsx")["default"] + const IconCapPlay: typeof import('~icons/cap/play.jsx')['default'] const IconCapPlayCircle: typeof import("~icons/cap/play-circle.jsx")["default"] - const IconCapPresets: typeof import("~icons/cap/presets.jsx")["default"] - const IconCapPrev: typeof import("~icons/cap/prev.jsx")["default"] - const IconCapRedo: typeof import("~icons/cap/redo.jsx")["default"] + const IconCapPresets: typeof import('~icons/cap/presets.jsx')['default'] + const IconCapPrev: typeof import('~icons/cap/prev.jsx')['default'] + const IconCapRedo: typeof import('~icons/cap/redo.jsx')['default'] const IconCapRestart: typeof import("~icons/cap/restart.jsx")["default"] - const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"] + const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] - const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"] + const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"] const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"] - const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"] - const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"] + const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] + const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] const IconCapX: typeof import("~icons/cap/x.jsx")["default"] - const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"] - const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"] - const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"] + const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] + const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] + const IconHugeiconsEaseCurveControlPoints: typeof import('~icons/hugeicons/ease-curve-control-points.jsx')['default'] const IconLucideAppWindowMac: typeof import('~icons/lucide/app-window-mac.jsx')['default'] const IconLucideBell: typeof import('~icons/lucide/bell.jsx')['default'] const IconLucideBug: typeof import('~icons/lucide/bug.jsx')['default'] - const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"] - const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"] + const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] + const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"] - const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"] + const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"] - const IconLucideEyeOff: typeof import("~icons/lucide/eye-off.jsx")["default"] - const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"] - const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"] - const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"] - const IconLucideLayout: typeof import("~icons/lucide/layout.jsx")["default"] - const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"] - const IconLucideMessageSquarePlus: typeof import("~icons/lucide/message-square-plus.jsx")["default"] + const IconLucideEyeOff: typeof import('~icons/lucide/eye-off.jsx')['default'] + const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] + const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] + const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default'] + const IconLucideLayout: typeof import('~icons/lucide/layout.jsx')['default'] + const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] + const IconLucideMessageSquarePlus: typeof import('~icons/lucide/message-square-plus.jsx')['default'] const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"] - const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"] + const IconLucideMonitor: typeof import('~icons/lucide/monitor.jsx')['default'] const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"] - const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"] - const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"] + const IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw.jsx')['default'] + const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] - const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"] - const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"] + const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] + const IconLucideVideo: typeof import('~icons/lucide/video.jsx')['default'] + const IconLucideVolume2: typeof import('~icons/lucide/volume2.jsx')['default'] const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"] const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import('~icons/material-symbols/screenshot-frame2-rounded.jsx')['default'] const IconMdiLoading: typeof import("~icons/mdi/loading.jsx")["default"]