From 56421f6e1f0dd240d76801221a3fc98499ea41f7 Mon Sep 17 00:00:00 2001 From: Vishal Date: Tue, 19 Aug 2025 13:07:10 +0530 Subject: [PATCH 1/3] feat:Add blur track with shaders file for it --- apps/desktop/src-tauri/src/recording.rs | 3 +- .../desktop/src/routes/editor/BlurOverlay.tsx | 192 +++ .../src/routes/editor/ConfigSidebar.tsx | 170 +++ apps/desktop/src/routes/editor/Player.tsx | 3 + .../src/routes/editor/Timeline/BlurTrack.tsx | 434 ++++++ .../src/routes/editor/Timeline/index.tsx | 10 + apps/desktop/src/routes/editor/context.ts | 16 +- apps/desktop/src/utils/tauri.ts | 1202 ++++++----------- crates/project/src/configuration.rs | 21 + crates/rendering/src/frame_pipeline.rs | 1 + crates/rendering/src/layers/mod.rs | 3 + crates/rendering/src/layers/selective_blur.rs | 114 ++ crates/rendering/src/lib.rs | 26 +- .../rendering/src/selective_blur_pipeline.rs | 160 +++ crates/rendering/src/shaders/blur.wgsl | 64 + packages/ui-solid/src/auto-imports.d.ts | 169 +-- 16 files changed, 1730 insertions(+), 858 deletions(-) create mode 100644 apps/desktop/src/routes/editor/BlurOverlay.tsx create mode 100644 apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx create mode 100644 crates/rendering/src/layers/selective_blur.rs create mode 100644 crates/rendering/src/selective_blur_pipeline.rs create mode 100644 crates/rendering/src/shaders/blur.wgsl diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index a6842300d..8dc744fda 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -25,7 +25,7 @@ use cap_media::{ use cap_project::{ CursorClickEvent, Platform, ProjectConfiguration, RecordingMeta, RecordingMetaInner, SharingMeta, StudioRecordingMeta, TimelineConfiguration, TimelineSegment, ZoomMode, - ZoomSegment, cursor::CursorEvents, + BlurSegment,ZoomSegment, cursor::CursorEvents, }; use cap_recording::{ CompletedStudioRecording, RecordingError, RecordingMode, StudioRecordingHandle, @@ -985,6 +985,7 @@ fn project_config_from_recording( } else { Vec::new() }, + blur_segments:Some(Vec::new()) }), ..default_config.unwrap_or_default() } diff --git a/apps/desktop/src/routes/editor/BlurOverlay.tsx b/apps/desktop/src/routes/editor/BlurOverlay.tsx new file mode 100644 index 000000000..93f2307a5 --- /dev/null +++ b/apps/desktop/src/routes/editor/BlurOverlay.tsx @@ -0,0 +1,192 @@ +import { createElementBounds } from "@solid-primitives/bounds"; +import { createEventListenerMap } from "@solid-primitives/event-listener"; +import { createRoot, createSignal, For, Show } from "solid-js"; +import { cx } from "cva"; +import { useEditorContext } from "./context"; + + +interface BlurRectangleProps { + rect: { x: number; y: number; width: number; height: number }; + style: { left: string; top: string; width: string; height: string; filter?: string }; + onUpdate: (rect: { x: number; y: number; width: number; height: number }) => void; + containerBounds: { width?: number | null; height?: number | null }; + blurAmount: number; + isEditing: boolean; + } + +export function BlurOverlay() { + const { project, setProject, editorState } = useEditorContext(); + + const [canvasContainerRef, setCanvasContainerRef] = createSignal(); + const containerBounds = createElementBounds(canvasContainerRef); + + const currentTime = () => editorState.previewTime ?? editorState.playbackTime ?? 0; + + const activeBlurSegmentsWithIndex = () => { + return (project.timeline?.blurSegments || []).map((segment, index) => ({ segment, index })).filter( + ({ segment }) => currentTime() >= segment.start && currentTime() <= segment.end + ); + }; + + const updateBlurRect = (index: number, rect: { x: number; y: number; width: number; height: number }) => { + setProject("timeline", "blurSegments", index, "rect", rect); + }; + + const isSelected = (index: number) => { + const selection = editorState.timeline.selection; + return selection?.type === "blur" && selection.index === index; + }; + + return ( +
+ + {({ segment, index }) => { + // Convert normalized coordinates to pixel coordinates + const rectStyle = () => { + const containerWidth = containerBounds.width ?? 1; + const containerHeight = containerBounds.height ?? 1; + + return { + left: `${segment.rect.x * containerWidth}px`, + top: `${segment.rect.y * containerHeight}px`, + width: `${segment.rect.width * containerWidth}px`, + height: `${segment.rect.height * containerHeight}px`, + }; + }; + + return ( + updateBlurRect(index, newRect)} + containerBounds={containerBounds} + isEditing={isSelected(index)} + /> + ); + }} + +
+ ); +} + + + +function BlurRectangle(props: BlurRectangleProps) { + const handleMouseDown = (e: MouseEvent, action: 'move' | 'resize', corner?: string) => { + e.preventDefault(); + e.stopPropagation(); + + const containerWidth = props.containerBounds.width ?? 1; + const containerHeight = props.containerBounds.height ?? 1; + + const startX = e.clientX; + const startY = e.clientY; + const startRect = { ...props.rect }; + + createRoot((dispose) => { + createEventListenerMap(window, { + mousemove: (moveEvent: MouseEvent) => { + const deltaX = (moveEvent.clientX - startX) / containerWidth; + const deltaY = (moveEvent.clientY - startY) / containerHeight; + + let newRect = { ...startRect }; + + if (action === 'move') { + newRect.x = Math.max(0, Math.min(1 - newRect.width, startRect.x + deltaX)); + newRect.y = Math.max(0, Math.min(1 - newRect.height, startRect.y + deltaY)); + } else if (action === 'resize') { + switch (corner) { + case 'nw': // Northwest corner + newRect.x = Math.max(0, startRect.x + deltaX); + newRect.y = Math.max(0, startRect.y + deltaY); + newRect.width = startRect.width - deltaX; + newRect.height = startRect.height - deltaY; + break; + case 'ne': // Northeast corner + newRect.y = Math.max(0, startRect.y + deltaY); + newRect.width = startRect.width + deltaX; + newRect.height = startRect.height - deltaY; + break; + case 'sw': // Southwest corner + newRect.x = Math.max(0, startRect.x + deltaX); + newRect.width = startRect.width - deltaX; + newRect.height = startRect.height + deltaY; + break; + case 'se': // Southeast corner + newRect.width = startRect.width + deltaX; + newRect.height = startRect.height + deltaY; + break; + } + + // Ensure minimum size + newRect.width = Math.max(0.05, newRect.width); + newRect.height = Math.max(0.05, newRect.height); + + // Ensure within bounds + newRect.x = Math.max(0, Math.min(1 - newRect.width, newRect.x)); + newRect.y = Math.max(0, Math.min(1 - newRect.height, newRect.y)); + newRect.width = Math.min(1 - newRect.x, newRect.width); + newRect.height = Math.min(1 - newRect.y, newRect.height); + } + + props.onUpdate(newRect); + }, + mouseup: () => { + dispose(); + }, + }); + }); + }; + + return ( +
+ + {/* Main draggable area */} +
handleMouseDown(e, 'move')} + /> + + {/* Resize handles */} +
handleMouseDown(e, 'resize', 'nw')} + /> +
handleMouseDown(e, 'resize', 'ne')} + /> +
handleMouseDown(e, 'resize', 'sw')} + /> +
handleMouseDown(e, 'resize', 'se')} + /> + + {/* Center label */} + {/*
+
+ + Blur Area +
+
*/} + +
+ ); +} \ No newline at end of file diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 7bb77e0d2..049bd1859 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -46,6 +46,7 @@ import { type StereoMode, type TimelineSegment, type ZoomSegment, + type BlurSegment, } from "~/utils/tauri"; import IconLucideSparkles from "~icons/lucide/sparkles"; import { CaptionsTab } from "./CaptionsTab"; @@ -585,6 +586,28 @@ export function ConfigSidebar() { /> )} + + { + const blurSelection = selection(); + if (blurSelection.type !== "blur") return; + + const segment = + project.timeline?.blurSegments?.[blurSelection.index]; + if (!segment) return; + + return { selection: blurSelection, segment }; + })()} + > + {(value) => ( + + )} + + + { const clipSegment = selection(); @@ -1975,6 +1998,153 @@ function ZoomSegmentConfig(props: { ); } +function BlurSegmentConfig(props: { + segmentIndex: number; + segment: BlurSegment; +}) { + const { + project, + setProject, + editorInstance, + setEditorState, + projectHistory, + projectActions, + } = useEditorContext(); + + return ( + <> +
+
+ setEditorState("timeline", "selection", null)} + leftIcon={} + > + Done + +
+ { + projectActions.deleteBlurSegment(props.segmentIndex); + }} + leftIcon={} + > + Delete + +
+ + }> + + setProject( + "timeline", + "blurSegments", + props.segmentIndex, + "blur_amount", + v[0], + ) + } + minValue={0} + maxValue={20} + step={0.1} + formatTooltip="px" + /> + + + }> +
+
+
+ + + setProject( + "timeline", + "blurSegments", + props.segmentIndex, + "rect", + "x", + v[0] / 100, + ) + } + minValue={0} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+
+ + + setProject( + "timeline", + "blurSegments", + props.segmentIndex, + "rect", + "y", + v[0] / 100, + ) + } + minValue={0} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+
+ +
+
+ + + setProject( + "timeline", + "blurSegments", + props.segmentIndex, + "rect", + "width", + v[0] / 100, + ) + } + minValue={1} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+
+ + + setProject( + "timeline", + "blurSegments", + props.segmentIndex, + "rect", + "height", + v[0] / 100, + ) + } + minValue={1} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+
+
+
+ + ); +} + function ClipSegmentConfig(props: { segmentIndex: number; segment: TimelineSegment; diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index d766b6460..5f0d36540 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -22,6 +22,7 @@ import AspectRatioSelect from "./AspectRatioSelect"; import { FPS, OUTPUT_SIZE, useEditorContext } from "./context"; import { ComingSoonTooltip, EditorButton, Slider } from "./ui"; import { formatTime } from "./utils"; +import { BlurOverlay } from "./BlurOverlay"; export function Player() { const { @@ -373,6 +374,7 @@ function PreviewCanvas() { width={currentFrame().width} height={currentFrame().data.height} /> +
); }} @@ -381,6 +383,7 @@ function PreviewCanvas() { ); } + function Time(props: { seconds: number; fps?: number; class?: string }) { return ( diff --git a/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx b/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx new file mode 100644 index 000000000..fd350d822 --- /dev/null +++ b/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx @@ -0,0 +1,434 @@ +import { + createEventListener, + createEventListenerMap, +} from "@solid-primitives/event-listener"; +import { Menu } from "@tauri-apps/api/menu"; +import { cx } from "cva"; +import { + batch, + createMemo, + createRoot, + createSignal, + For, + Show, +} from "solid-js"; +import { produce } from "solid-js/store"; +import { commands } from "~/utils/tauri"; +import { useEditorContext } from "../context"; +import { + useSegmentContext, + useTimelineContext, + useTrackContext, +} from "./context"; +import { SegmentContent, SegmentHandle, SegmentRoot, TrackRoot } from "./Track"; + +export type BlurSegmentDragState = + | { type: "idle" } + | { type: "movePending" } + | { type: "moving" }; + +export function BlurTrack(props: { + onDragStateChanged: (v: BlurSegmentDragState) => void; + handleUpdatePlayhead: (e: MouseEvent) => void; +}) { + const { project, setProject, projectHistory, setEditorState, editorState } = + useEditorContext(); + + const { duration, secsPerPixel } = useTimelineContext(); + + const [hoveringSegment, setHoveringSegment] = createSignal(false); + const [hoveredTime, setHoveredTime] = createSignal(); + + const handleGenerateBlurSegments = async () => { + try { + const blurSegments = await commands.generateBlurSegmentsFromClicks(); + setProject("timeline", "blurSegments", blurSegments); + } catch (error) { + console.error("Failed to generate blur segments:", error); + } + }; + + return ( + { + if (!import.meta.env.DEV) return; + + e.preventDefault(); + const menu = await Menu.new({ + id: "blur-track-options", + items: [ + { + id: "generateBlurSegments", + text: "Generate blur segments from clicks", + action: handleGenerateBlurSegments, + }, + ], + }); + menu.popup(); + }} + onMouseMove={(e) => { + if (hoveringSegment()) { + setHoveredTime(undefined); + return; + } + + const bounds = e.target.getBoundingClientRect()!; + + let time = + (e.clientX - bounds.left) * secsPerPixel() + + editorState.timeline.transform.position; + + const nextSegmentIndex = project.timeline?.blurSegments?.findIndex( + (s) => time < s.start, + ); + + if (nextSegmentIndex !== undefined) { + const prevSegmentIndex = nextSegmentIndex - 1; + + if (prevSegmentIndex === undefined) return; + + const nextSegment = + project.timeline?.blurSegments?.[nextSegmentIndex]; + + if (prevSegmentIndex !== undefined && nextSegment) { + const prevSegment = + project.timeline?.blurSegments?.[prevSegmentIndex]; + + if (prevSegment) { + const availableTime = nextSegment?.start - prevSegment?.end; + + if (availableTime < 1) return; + } + } + + if (nextSegment && nextSegment.start - time < 1) { + time = nextSegment.start - 1; + } + } + + setHoveredTime(Math.min(time, duration() - 1)); + }} + onMouseLeave={() => setHoveredTime()} + onMouseDown={(e) => { + createRoot((dispose) => { + createEventListener(e.currentTarget, "mouseup", (e) => { + dispose(); + + const time = hoveredTime(); + if (time === undefined) return; + + e.stopPropagation(); + batch(() => { + setProject("timeline", "blurSegments", (v) => v ?? []); + setProject( + "timeline", + "blurSegments", + produce((blurSegments) => { + blurSegments ??= []; + + let index = blurSegments.length; + + for (let i = blurSegments.length - 1; i >= 0; i--) { + if (blurSegments[i].start > time) { + index = i; + break; + } + } + + blurSegments.splice(index, 0, { + start: time, + end: time + 1, + blur_amount: 8, + rect: { x: 0.25, y: 0.25, width: 0.5, height: 0.5 }, + }); + }), + ); + }); + }); + }); + }} + > + + Click to add blur segment +
+ } + > + {(segment, i) => { + const { setTrackState } = useTrackContext(); + + const blurPercentage = () => { + const amount = segment.blur_amount; + return `${amount.toFixed(1)}x`; + }; + + const blurSegments = () => project.timeline!.blurSegments!; + + function createMouseDownDrag( + setup: () => T, + _update: (e: MouseEvent, v: T, initialMouseX: number) => void, + ) { + return (downEvent: MouseEvent) => { + downEvent.stopPropagation(); + + const initial = setup(); + + let moved = false; + let initialMouseX: null | number = null; + + setTrackState("draggingSegment", true); + + const resumeHistory = projectHistory.pause(); + + props.onDragStateChanged({ type: "movePending" }); + + function finish(e: MouseEvent) { + resumeHistory(); + if (!moved) { + e.stopPropagation(); + setEditorState("timeline", "selection", { + type: "blur", + index: i(), + }); + props.handleUpdatePlayhead(e); + } + props.onDragStateChanged({ type: "idle" }); + setTrackState("draggingSegment", false); + } + + function update(event: MouseEvent) { + if (Math.abs(event.clientX - downEvent.clientX) > 2) { + if (!moved) { + moved = true; + initialMouseX = event.clientX; + props.onDragStateChanged({ + type: "moving", + }); + } + } + + if (initialMouseX === null) return; + + _update(event, initial, initialMouseX); + } + + createRoot((dispose) => { + createEventListenerMap(window, { + mousemove: (e) => { + update(e); + }, + mouseup: (e) => { + update(e); + finish(e); + dispose(); + }, + }); + }); + }; + } + + const isSelected = createMemo(() => { + const selection = editorState.timeline.selection; + if (!selection || selection.type !== "blur") return false; + + const segmentIndex = project.timeline?.blurSegments?.findIndex( + (s) => s.start === segment.start && s.end === segment.end, + ); + + return segmentIndex === selection.index; + }); + + return ( + { + setHoveringSegment(true); + }} + onMouseLeave={() => { + setHoveringSegment(false); + }} + > + { + const start = segment.start; + + let minValue = 0; + + const maxValue = segment.end - 1; + + for (let i = blurSegments().length - 1; i >= 0; i--) { + const segment = blurSegments()[i]!; + if (segment.end <= start) { + minValue = segment.end; + break; + } + } + + return { start, minValue, maxValue }; + }, + (e, value, initialMouseX) => { + const newStart = + value.start + + (e.clientX - initialMouseX) * secsPerPixel(); + + setProject( + "timeline", + "blurSegments", + i(), + "start", + Math.min( + value.maxValue, + Math.max(value.minValue, newStart), + ), + ); + + setProject( + "timeline", + "blurSegments", + produce((s) => { + s.sort((a, b) => a.start - b.start); + }), + ); + }, + )} + /> + { + const original = { ...segment }; + + const prevSegment = blurSegments()[i() - 1]; + const nextSegment = blurSegments()[i() + 1]; + + const minStart = prevSegment?.end ?? 0; + const maxEnd = nextSegment?.start ?? duration(); + + return { + original, + minStart, + maxEnd, + }; + }, + (e, value, initialMouseX) => { + const rawDelta = + (e.clientX - initialMouseX) * secsPerPixel(); + + const newStart = value.original.start + rawDelta; + const newEnd = value.original.end + rawDelta; + + let delta = rawDelta; + + if (newStart < value.minStart) + delta = value.minStart - value.original.start; + else if (newEnd > value.maxEnd) + delta = value.maxEnd - value.original.end; + + setProject("timeline", "blurSegments", i(), { + start: value.original.start + delta, + end: value.original.end + delta, + }); + }, + )} + > + {(() => { + const ctx = useSegmentContext(); + + return ( + 100}> +
+ Blur +
+ {" "} + {blurPercentage()}{" "} +
+
+
+ ); + })()} +
+ { + const end = segment.end; + + const minValue = segment.start + 1; + + let maxValue = duration(); + + for (let i = 0; i < blurSegments().length; i++) { + const segment = blurSegments()[i]!; + if (segment.start > end) { + maxValue = segment.start; + break; + } + } + + return { end, minValue, maxValue }; + }, + (e, value, initialMouseX) => { + const newEnd = + value.end + (e.clientX - initialMouseX) * secsPerPixel(); + + setProject( + "timeline", + "blurSegments", + i(), + "end", + Math.min( + value.maxValue, + Math.max(value.minValue, newEnd), + ), + ); + + setProject( + "timeline", + "blurSegments", + produce((s) => { + s.sort((a, b) => a.start - b.start); + }), + ); + }, + )} + /> +
+ ); + }} + + + {(time) => ( + + +

+ + +

+
+
+ )} +
+ + ); +} diff --git a/apps/desktop/src/routes/editor/Timeline/index.tsx b/apps/desktop/src/routes/editor/Timeline/index.tsx index 1aa632110..ef3b1721f 100644 --- a/apps/desktop/src/routes/editor/Timeline/index.tsx +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx @@ -12,6 +12,7 @@ import { formatTime } from "../utils"; import { ClipTrack } from "./ClipTrack"; import { TimelineContextProvider, useTimelineContext } from "./context"; import { type ZoomSegmentDragState, ZoomTrack } from "./ZoomTrack"; +import { BlurTrack,type BlurSegmentDragState } from "./BlurTrack"; const TIMELINE_PADDING = 16; @@ -66,12 +67,14 @@ export function Timeline() { }, ], zoomSegments: [], + blurSegments: [], }; }), ); } let zoomSegmentDragState = { type: "idle" } as ZoomSegmentDragState; + let blurSegmentDragState = { type: "idle" } as BlurSegmentDragState; async function handleUpdatePlayhead(e: MouseEvent) { const { left } = timelineBounds; @@ -84,6 +87,7 @@ export function Timeline() { ), ); } + } createEventListener(window, "keydown", (e) => { @@ -229,6 +233,12 @@ export function Timeline() { }} handleUpdatePlayhead={handleUpdatePlayhead} /> + + { + blurSegmentDragState = v; + }} + handleUpdatePlayhead={handleUpdatePlayhead} /> +
); diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 56cb1493a..a58c6d7b5 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -141,6 +141,19 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( setEditorState("timeline", "selection", null); }); }, + deleteBlurSegment: (segmentIndex: number) => { + batch(() => { + setProject( + "timeline", + "blurSegments", + produce((s) => { + if (!s) return; + return s.splice(segmentIndex, 1); + }), + ); + setEditorState("timeline", "selection", null); + }); + }, }; createEffect( @@ -238,7 +251,8 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( selection: null as | null | { type: "zoom"; index: number } - | { type: "clip"; index: number }, + | { type: "clip"; index: number } + | { type: "blur"; index: number }, transform: { // visible seconds zoom: zoomOutLimit(), diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index e2cd307ab..4f8af640c 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,790 +1,457 @@ + // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ + export const commands = { - async setMicInput(label: string | null): Promise { - return await TAURI_INVOKE("set_mic_input", { label }); - }, - async setCameraInput(id: DeviceOrModelID | null): Promise { - return await TAURI_INVOKE("set_camera_input", { id }); - }, - async startRecording(inputs: StartRecordingInputs): Promise { - return await TAURI_INVOKE("start_recording", { inputs }); - }, - async stopRecording(): Promise { - return await TAURI_INVOKE("stop_recording"); - }, - async pauseRecording(): Promise { - return await TAURI_INVOKE("pause_recording"); - }, - async resumeRecording(): Promise { - return await TAURI_INVOKE("resume_recording"); - }, - async restartRecording(): Promise { - return await TAURI_INVOKE("restart_recording"); - }, - async deleteRecording(): Promise { - return await TAURI_INVOKE("delete_recording"); - }, - async listCameras(): Promise { - return await TAURI_INVOKE("list_cameras"); - }, - async listCaptureWindows(): Promise { - return await TAURI_INVOKE("list_capture_windows"); - }, - async listCaptureScreens(): Promise { - return await TAURI_INVOKE("list_capture_screens"); - }, - async takeScreenshot(): Promise { - return await TAURI_INVOKE("take_screenshot"); - }, - async listAudioDevices(): Promise { - return await TAURI_INVOKE("list_audio_devices"); - }, - async closeRecordingsOverlayWindow(): Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); - }, - async setFakeWindowBounds(name: string, bounds: Bounds): Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); - }, - async removeFakeWindow(name: string): Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); - }, - async focusCapturesPanel(): Promise { - await TAURI_INVOKE("focus_captures_panel"); - }, - async getCurrentRecording(): Promise> { - return await TAURI_INVOKE("get_current_recording"); - }, - async exportVideo( - projectPath: string, - progress: TAURI_CHANNEL, - settings: ExportSettings, - ): Promise { - return await TAURI_INVOKE("export_video", { - projectPath, - progress, - settings, - }); - }, - async getExportEstimates( - path: string, - resolution: XY, - fps: number, - ): Promise { - return await TAURI_INVOKE("get_export_estimates", { - path, - resolution, - fps, - }); - }, - async copyFileToPath(src: string, dst: string): Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); - }, - async copyVideoToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); - }, - async copyScreenshotToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); - }, - async openFilePath(path: string): Promise { - return await TAURI_INVOKE("open_file_path", { path }); - }, - async getVideoMetadata(path: string): Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); - }, - async createEditorInstance(): Promise { - return await TAURI_INVOKE("create_editor_instance"); - }, - async getMicWaveforms(): Promise { - return await TAURI_INVOKE("get_mic_waveforms"); - }, - async getSystemAudioWaveforms(): Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); - }, - async startPlayback(fps: number, resolutionBase: XY): Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); - }, - async stopPlayback(): Promise { - return await TAURI_INVOKE("stop_playback"); - }, - async setPlayheadPosition(frameNumber: number): Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); - }, - async setProjectConfig(config: ProjectConfiguration): Promise { - return await TAURI_INVOKE("set_project_config", { config }); - }, - async generateZoomSegmentsFromClicks(): Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); - }, - async openPermissionSettings(permission: OSPermission): Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); - }, - async doPermissionsCheck(initialCheck: boolean): Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); - }, - async requestPermission(permission: OSPermission): Promise { - await TAURI_INVOKE("request_permission", { permission }); - }, - async uploadExportedVideo( - path: string, - mode: UploadMode, - ): Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); - }, - async uploadScreenshot(screenshotPath: string): Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); - }, - async getRecordingMeta( - path: string, - fileType: FileType, - ): Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); - }, - async saveFileDialog( - fileName: string, - fileType: string, - ): Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); - }, - async listRecordings(): Promise<[string, RecordingMetaWithType][]> { - return await TAURI_INVOKE("list_recordings"); - }, - async listScreenshots(): Promise<[string, RecordingMeta][]> { - return await TAURI_INVOKE("list_screenshots"); - }, - async checkUpgradedAndUpdate(): Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); - }, - async openExternalLink(url: string): Promise { - return await TAURI_INVOKE("open_external_link", { url }); - }, - async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); - }, - async resetCameraPermissions(): Promise { - return await TAURI_INVOKE("reset_camera_permissions"); - }, - async resetMicrophonePermissions(): Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); - }, - async isCameraWindowOpen(): Promise { - return await TAURI_INVOKE("is_camera_window_open"); - }, - async seekTo(frameNumber: number): Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); - }, - async positionTrafficLights( - controlsInset: [number, number] | null, - ): Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); - }, - async setTheme(theme: AppTheme): Promise { - await TAURI_INVOKE("set_theme", { theme }); - }, - async globalMessageDialog(message: string): Promise { - await TAURI_INVOKE("global_message_dialog", { message }); - }, - async showWindow(window: ShowCapWindow): Promise { - return await TAURI_INVOKE("show_window", { window }); - }, - async writeClipboardString(text: string): Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); - }, - async performHapticFeedback( - pattern: HapticPattern | null, - time: HapticPerformanceTime | null, - ): Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); - }, - async listFails(): Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); - }, - async setFail(name: string, value: boolean): Promise { - await TAURI_INVOKE("set_fail", { name, value }); - }, - async updateAuthPlan(): Promise { - await TAURI_INVOKE("update_auth_plan"); - }, - async setWindowTransparent(value: boolean): Promise { - await TAURI_INVOKE("set_window_transparent", { value }); - }, - async getEditorMeta(): Promise { - return await TAURI_INVOKE("get_editor_meta"); - }, - async setPrettyName(prettyName: string): Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); - }, - async setServerUrl(serverUrl: string): Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); - }, - async setCameraPreviewState(state: CameraWindowState): Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); - }, - async awaitCameraPreviewReady(): Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); - }, - /** - * Function to handle creating directories for the model - */ - async createDir(path: string, recursive: boolean): Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); - }, - /** - * Function to save the model file - */ - async saveModelFile(path: string, data: number[]): Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); - }, - /** - * Function to transcribe audio from a video file using Whisper - */ - async transcribeAudio( - videoPath: string, - modelPath: string, - language: string, - ): Promise { - return await TAURI_INVOKE("transcribe_audio", { - videoPath, - modelPath, - language, - }); - }, - /** - * Function to save caption data to a file - */ - async saveCaptions(videoId: string, captions: CaptionData): Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); - }, - /** - * Function to load caption data from a file - */ - async loadCaptions(videoId: string): Promise { - return await TAURI_INVOKE("load_captions", { videoId }); - }, - /** - * Helper function to download a Whisper model from Hugging Face Hub - */ - async downloadWhisperModel( - modelName: string, - outputPath: string, - ): Promise { - return await TAURI_INVOKE("download_whisper_model", { - modelName, - outputPath, - }); - }, - /** - * Function to check if a model file exists - */ - async checkModelExists(modelPath: string): Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); - }, - /** - * Function to delete a downloaded model - */ - async deleteWhisperModel(modelPath: string): Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); - }, - /** - * Export captions to an SRT file - */ - async exportCaptionsSrt(videoId: string): Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); - }, - async openTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("open_target_select_overlays"); - }, - async closeTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("close_target_select_overlays"); - }, -}; +async setMicInput(label: string | null) : Promise { + return await TAURI_INVOKE("set_mic_input", { label }); +}, +async setCameraInput(id: DeviceOrModelID | null) : Promise { + return await TAURI_INVOKE("set_camera_input", { id }); +}, +async startRecording(inputs: StartRecordingInputs) : Promise { + return await TAURI_INVOKE("start_recording", { inputs }); +}, +async stopRecording() : Promise { + return await TAURI_INVOKE("stop_recording"); +}, +async pauseRecording() : Promise { + return await TAURI_INVOKE("pause_recording"); +}, +async resumeRecording() : Promise { + return await TAURI_INVOKE("resume_recording"); +}, +async restartRecording() : Promise { + return await TAURI_INVOKE("restart_recording"); +}, +async deleteRecording() : Promise { + return await TAURI_INVOKE("delete_recording"); +}, +async listCameras() : Promise { + return await TAURI_INVOKE("list_cameras"); +}, +async listCaptureWindows() : Promise { + return await TAURI_INVOKE("list_capture_windows"); +}, +async listCaptureScreens() : Promise { + return await TAURI_INVOKE("list_capture_screens"); +}, +async takeScreenshot() : Promise { + return await TAURI_INVOKE("take_screenshot"); +}, +async listAudioDevices() : Promise { + return await TAURI_INVOKE("list_audio_devices"); +}, +async closeRecordingsOverlayWindow() : Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); +}, +async setFakeWindowBounds(name: string, bounds: Bounds) : Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); +}, +async removeFakeWindow(name: string) : Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); +}, +async focusCapturesPanel() : Promise { + await TAURI_INVOKE("focus_captures_panel"); +}, +async getCurrentRecording() : Promise> { + return await TAURI_INVOKE("get_current_recording"); +}, +async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { + return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); +}, +async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { + return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); +}, +async copyFileToPath(src: string, dst: string) : Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); +}, +async copyVideoToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); +}, +async copyScreenshotToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); +}, +async openFilePath(path: string) : Promise { + return await TAURI_INVOKE("open_file_path", { path }); +}, +async getVideoMetadata(path: string) : Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); +}, +async createEditorInstance() : Promise { + return await TAURI_INVOKE("create_editor_instance"); +}, +async getMicWaveforms() : Promise { + return await TAURI_INVOKE("get_mic_waveforms"); +}, +async getSystemAudioWaveforms() : Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); +}, +async startPlayback(fps: number, resolutionBase: XY) : Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); +}, +async stopPlayback() : Promise { + return await TAURI_INVOKE("stop_playback"); +}, +async setPlayheadPosition(frameNumber: number) : Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); +}, +async setProjectConfig(config: ProjectConfiguration) : Promise { + return await TAURI_INVOKE("set_project_config", { config }); +}, +async generateZoomSegmentsFromClicks() : Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); +}, +async openPermissionSettings(permission: OSPermission) : Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); +}, +async doPermissionsCheck(initialCheck: boolean) : Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); +}, +async requestPermission(permission: OSPermission) : Promise { + await TAURI_INVOKE("request_permission", { permission }); +}, +async uploadExportedVideo(path: string, mode: UploadMode) : Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); +}, +async uploadScreenshot(screenshotPath: string) : Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); +}, +async getRecordingMeta(path: string, fileType: FileType) : Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); +}, +async saveFileDialog(fileName: string, fileType: string) : Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); +}, +async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { + return await TAURI_INVOKE("list_recordings"); +}, +async listScreenshots() : Promise<([string, RecordingMeta])[]> { + return await TAURI_INVOKE("list_screenshots"); +}, +async checkUpgradedAndUpdate() : Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); +}, +async openExternalLink(url: string) : Promise { + return await TAURI_INVOKE("open_external_link", { url }); +}, +async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); +}, +async resetCameraPermissions() : Promise { + return await TAURI_INVOKE("reset_camera_permissions"); +}, +async resetMicrophonePermissions() : Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); +}, +async isCameraWindowOpen() : Promise { + return await TAURI_INVOKE("is_camera_window_open"); +}, +async seekTo(frameNumber: number) : Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); +}, +async positionTrafficLights(controlsInset: [number, number] | null) : Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); +}, +async setTheme(theme: AppTheme) : Promise { + await TAURI_INVOKE("set_theme", { theme }); +}, +async globalMessageDialog(message: string) : Promise { + await TAURI_INVOKE("global_message_dialog", { message }); +}, +async showWindow(window: ShowCapWindow) : Promise { + return await TAURI_INVOKE("show_window", { window }); +}, +async writeClipboardString(text: string) : Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); +}, +async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); +}, +async listFails() : Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); +}, +async setFail(name: string, value: boolean) : Promise { + await TAURI_INVOKE("set_fail", { name, value }); +}, +async updateAuthPlan() : Promise { + await TAURI_INVOKE("update_auth_plan"); +}, +async setWindowTransparent(value: boolean) : Promise { + await TAURI_INVOKE("set_window_transparent", { value }); +}, +async getEditorMeta() : Promise { + return await TAURI_INVOKE("get_editor_meta"); +}, +async setPrettyName(prettyName: string) : Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); +}, +async setServerUrl(serverUrl: string) : Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); +}, +async setCameraPreviewState(state: CameraWindowState) : Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); +}, +async awaitCameraPreviewReady() : Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); +}, +/** + * Function to handle creating directories for the model + */ +async createDir(path: string, recursive: boolean) : Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); +}, +/** + * Function to save the model file + */ +async saveModelFile(path: string, data: number[]) : Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); +}, +/** + * Function to transcribe audio from a video file using Whisper + */ +async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { + return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); +}, +/** + * Function to save caption data to a file + */ +async saveCaptions(videoId: string, captions: CaptionData) : Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); +}, +/** + * Function to load caption data from a file + */ +async loadCaptions(videoId: string) : Promise { + return await TAURI_INVOKE("load_captions", { videoId }); +}, +/** + * Helper function to download a Whisper model from Hugging Face Hub + */ +async downloadWhisperModel(modelName: string, outputPath: string) : Promise { + return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); +}, +/** + * Function to check if a model file exists + */ +async checkModelExists(modelPath: string) : Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); +}, +/** + * Function to delete a downloaded model + */ +async deleteWhisperModel(modelPath: string) : Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); +}, +/** + * Export captions to an SRT file + */ +async exportCaptionsSrt(videoId: string) : Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); +}, +async openTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("open_target_select_overlays"); +}, +async closeTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("close_target_select_overlays"); +} +} /** user-defined events **/ + export const events = __makeEvents__<{ - audioInputLevelChange: AudioInputLevelChange; - authenticationInvalid: AuthenticationInvalid; - currentRecordingChanged: CurrentRecordingChanged; - downloadProgress: DownloadProgress; - editorStateChanged: EditorStateChanged; - newNotification: NewNotification; - newScreenshotAdded: NewScreenshotAdded; - newStudioRecordingAdded: NewStudioRecordingAdded; - onEscapePress: OnEscapePress; - recordingDeleted: RecordingDeleted; - recordingEvent: RecordingEvent; - recordingOptionsChanged: RecordingOptionsChanged; - recordingStarted: RecordingStarted; - recordingStopped: RecordingStopped; - renderFrameEvent: RenderFrameEvent; - requestNewScreenshot: RequestNewScreenshot; - requestOpenSettings: RequestOpenSettings; - requestStartRecording: RequestStartRecording; - targetUnderCursor: TargetUnderCursor; - uploadProgress: UploadProgress; +audioInputLevelChange: AudioInputLevelChange, +authenticationInvalid: AuthenticationInvalid, +currentRecordingChanged: CurrentRecordingChanged, +downloadProgress: DownloadProgress, +editorStateChanged: EditorStateChanged, +newNotification: NewNotification, +newScreenshotAdded: NewScreenshotAdded, +newStudioRecordingAdded: NewStudioRecordingAdded, +onEscapePress: OnEscapePress, +recordingDeleted: RecordingDeleted, +recordingEvent: RecordingEvent, +recordingOptionsChanged: RecordingOptionsChanged, +recordingStarted: RecordingStarted, +recordingStopped: RecordingStopped, +renderFrameEvent: RenderFrameEvent, +requestNewScreenshot: RequestNewScreenshot, +requestOpenSettings: RequestOpenSettings, +requestStartRecording: RequestStartRecording, +targetUnderCursor: TargetUnderCursor, +uploadProgress: UploadProgress }>({ - audioInputLevelChange: "audio-input-level-change", - authenticationInvalid: "authentication-invalid", - currentRecordingChanged: "current-recording-changed", - downloadProgress: "download-progress", - editorStateChanged: "editor-state-changed", - newNotification: "new-notification", - newScreenshotAdded: "new-screenshot-added", - newStudioRecordingAdded: "new-studio-recording-added", - onEscapePress: "on-escape-press", - recordingDeleted: "recording-deleted", - recordingEvent: "recording-event", - recordingOptionsChanged: "recording-options-changed", - recordingStarted: "recording-started", - recordingStopped: "recording-stopped", - renderFrameEvent: "render-frame-event", - requestNewScreenshot: "request-new-screenshot", - requestOpenSettings: "request-open-settings", - requestStartRecording: "request-start-recording", - targetUnderCursor: "target-under-cursor", - uploadProgress: "upload-progress", -}); +audioInputLevelChange: "audio-input-level-change", +authenticationInvalid: "authentication-invalid", +currentRecordingChanged: "current-recording-changed", +downloadProgress: "download-progress", +editorStateChanged: "editor-state-changed", +newNotification: "new-notification", +newScreenshotAdded: "new-screenshot-added", +newStudioRecordingAdded: "new-studio-recording-added", +onEscapePress: "on-escape-press", +recordingDeleted: "recording-deleted", +recordingEvent: "recording-event", +recordingOptionsChanged: "recording-options-changed", +recordingStarted: "recording-started", +recordingStopped: "recording-stopped", +renderFrameEvent: "render-frame-event", +requestNewScreenshot: "request-new-screenshot", +requestOpenSettings: "request-open-settings", +requestStartRecording: "request-start-recording", +targetUnderCursor: "target-under-cursor", +uploadProgress: "upload-progress" +}) /** user-defined constants **/ + + /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark"; -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; -export type Audio = { - duration: number; - sample_rate: number; - channels: number; - start_time: number; -}; -export type AudioConfiguration = { - mute: boolean; - improve: boolean; - micVolumeDb?: number; - micStereoMode?: StereoMode; - systemVolumeDb?: number; -}; -export type AudioInputLevelChange = number; -export type AudioMeta = { - path: string; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type AuthSecret = - | { api_key: string } - | { token: string; expires: number }; -export type AuthStore = { - secret: AuthSecret; - user_id: string | null; - plan: Plan | null; - intercom_hash: string | null; -}; -export type AuthenticationInvalid = null; -export type BackgroundConfiguration = { - source: BackgroundSource; - blur: number; - padding: number; - rounding: number; - inset: number; - crop: Crop | null; - shadow?: number; - advancedShadow?: ShadowConfiguration | null; -}; -export type BackgroundSource = - | { type: "wallpaper"; path: string | null } - | { type: "image"; path: string | null } - | { type: "color"; value: [number, number, number] } - | { - type: "gradient"; - from: [number, number, number]; - to: [number, number, number]; - angle?: number; - }; -export type Bounds = { x: number; y: number; width: number; height: number }; -export type Camera = { - hide: boolean; - mirror: boolean; - position: CameraPosition; - size: number; - zoom_size: number | null; - rounding?: number; - shadow?: number; - advanced_shadow?: ShadowConfiguration | null; - shape?: CameraShape; -}; -export type CameraInfo = { - device_id: string; - model_id: ModelIDType | null; - display_name: string; -}; -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; -export type CameraPreviewShape = "round" | "square" | "full"; -export type CameraPreviewSize = "sm" | "lg"; -export type CameraShape = "square" | "source"; -export type CameraWindowState = { - size: CameraPreviewSize; - shape: CameraPreviewShape; - mirrored: boolean; -}; -export type CameraXPosition = "left" | "center" | "right"; -export type CameraYPosition = "top" | "bottom"; -export type CaptionData = { - segments: CaptionSegment[]; - settings: CaptionSettings | null; -}; -export type CaptionSegment = { - id: string; - start: number; - end: number; - text: string; -}; -export type CaptionSettings = { - enabled: boolean; - font: string; - size: number; - color: string; - backgroundColor: string; - backgroundOpacity: number; - position: string; - bold: boolean; - italic: boolean; - outline: boolean; - outlineColor: string; - exportWithSubtitles: boolean; -}; -export type CaptionsData = { - segments: CaptionSegment[]; - settings: CaptionSettings; -}; -export type CaptureScreen = { id: number; name: string; refresh_rate: number }; -export type CaptureWindow = { - id: number; - owner_name: string; - name: string; - bounds: Bounds; - refresh_rate: number; -}; -export type CommercialLicense = { - licenseKey: string; - expiryDate: number | null; - refresh: number; - activatedOn: number; -}; -export type Crop = { position: XY; size: XY }; -export type CurrentRecording = { - target: CurrentRecordingTarget; - type: RecordingType; -}; -export type CurrentRecordingChanged = null; -export type CurrentRecordingTarget = - | { window: { id: number; bounds: Bounds } } - | { screen: { id: number } } - | { area: { screen: number; bounds: Bounds } }; -export type CursorAnimationStyle = "regular" | "slow" | "fast"; -export type CursorConfiguration = { - hide?: boolean; - hideWhenIdle: boolean; - size: number; - type: CursorType; - animationStyle: CursorAnimationStyle; - tension: number; - mass: number; - friction: number; - raw?: boolean; - motionBlur?: number; - useSvg?: boolean; -}; -export type CursorMeta = { - imagePath: string; - hotspot: XY; - shape?: string | null; -}; -export type CursorType = "pointer" | "circle"; -export type Cursors = - | { [key in string]: string } - | { [key in string]: CursorMeta }; -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; -export type DisplayId = string; -export type DownloadProgress = { progress: number; message: string }; -export type EditorStateChanged = { playhead_position: number }; -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; -export type ExportEstimates = { - duration_seconds: number; - estimated_time_seconds: number; - estimated_size_mb: number; -}; -export type ExportSettings = - | ({ format: "Mp4" } & Mp4ExportSettings) - | ({ format: "Gif" } & GifExportSettings); -export type FileType = "recording" | "screenshot"; -export type Flags = { captions: boolean }; -export type FramesRendered = { - renderedCount: number; - totalFrames: number; - type: "FramesRendered"; -}; -export type GeneralSettingsStore = { - instanceId?: string; - uploadIndividualFiles?: boolean; - hideDockIcon?: boolean; - hapticsEnabled?: boolean; - autoCreateShareableLink?: boolean; - enableNotifications?: boolean; - disableAutoOpenLinks?: boolean; - hasCompletedStartup?: boolean; - theme?: AppTheme; - commercialLicense?: CommercialLicense | null; - lastVersion?: string | null; - windowTransparency?: boolean; - postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; - mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; - customCursorCapture?: boolean; - serverUrl?: string; - recordingCountdown?: number | null; - enableNativeCameraPreview: boolean; - autoZoomOnClicks?: boolean; - enableNewRecordingFlow: boolean; - postDeletionBehaviour?: PostDeletionBehaviour; -}; -export type GifExportSettings = { fps: number; resolution_base: XY }; -export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; -export type Hotkey = { - code: string; - meta: boolean; - ctrl: boolean; - alt: boolean; - shift: boolean; -}; -export type HotkeyAction = - | "startRecording" - | "stopRecording" - | "restartRecording"; -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 LogicalBounds = { position: LogicalPosition; size: LogicalSize }; -export type LogicalPosition = { x: number; y: number }; -export type LogicalSize = { width: number; height: number }; -export type MainWindowRecordingStartBehaviour = "close" | "minimise"; -export type ModelIDType = string; -export type Mp4ExportSettings = { - fps: number; - resolution_base: XY; - compression: ExportCompression; -}; -export type MultipleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - mic?: AudioMeta | null; - system_audio?: AudioMeta | null; - cursor?: string | null; -}; -export type MultipleSegments = { - segments: MultipleSegment[]; - cursors: Cursors; -}; -export type NewNotification = { - title: string; - body: string; - is_error: boolean; -}; -export type NewScreenshotAdded = { path: string }; -export type NewStudioRecordingAdded = { path: string }; -export type OSPermission = - | "screenRecording" - | "camera" - | "microphone" - | "accessibility"; -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; -export type OSPermissionsCheck = { - screenRecording: OSPermissionStatus; - microphone: OSPermissionStatus; - camera: OSPermissionStatus; - accessibility: OSPermissionStatus; -}; -export type OnEscapePress = null; -export type PhysicalSize = { width: number; height: number }; -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; -export type Platform = "MacOS" | "Windows"; -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; -export type Preset = { name: string; config: ProjectConfiguration }; -export type PresetsStore = { presets: Preset[]; default: number | null }; -export type ProjectConfiguration = { - aspectRatio: AspectRatio | null; - background: BackgroundConfiguration; - camera: Camera; - audio: AudioConfiguration; - cursor: CursorConfiguration; - hotkeys: HotkeysConfiguration; - timeline?: TimelineConfiguration | null; - captions?: CaptionsData | null; -}; -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; -export type RecordingDeleted = { path: string }; -export type RecordingEvent = - | { variant: "Countdown"; value: number } - | { variant: "Started" } - | { variant: "Stopped" } - | { variant: "Failed"; error: string }; -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}; -export type RecordingMetaWithType = (( - | StudioRecordingMeta - | InstantRecordingMeta -) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}) & { type: RecordingType }; -export type RecordingMode = "studio" | "instant"; -export type RecordingOptionsChanged = null; -export type RecordingStarted = null; -export type RecordingStopped = null; -export type RecordingType = "studio" | "instant"; -export type RenderFrameEvent = { - frame_number: number; - fps: number; - resolution_base: XY; -}; -export type RequestNewScreenshot = null; -export type RequestOpenSettings = { page: string }; -export type RequestStartRecording = null; -export type S3UploadMeta = { id: string }; -export type ScreenCaptureTarget = - | { variant: "window"; id: number } - | { variant: "screen"; id: number } - | { variant: "area"; screen: number; bounds: Bounds }; -export type ScreenUnderCursor = { - name: string; - physical_size: PhysicalSize; - refresh_rate: string; -}; -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; -}; -export type ShadowConfiguration = { - size: number; - opacity: number; - blur: number; -}; -export type SharingMeta = { id: string; link: string }; -export type ShowCapWindow = - | "Setup" - | "Main" - | { Settings: { page: string | null } } - | { Editor: { project_path: string } } - | "RecordingsOverlay" - | { WindowCaptureOccluder: { screen_id: number } } - | { TargetSelectOverlay: { display_id: DisplayId } } - | { CaptureArea: { screen_id: number } } - | "Camera" - | { InProgressRecording: { countdown: number | null } } - | "Upgrade" - | "ModeSelect"; -export type SingleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - audio?: AudioMeta | null; - cursor?: string | null; -}; -export type StartRecordingInputs = { - capture_target: ScreenCaptureTarget; - capture_system_audio?: boolean; - mode: RecordingMode; -}; -export type StereoMode = "stereo" | "monoL" | "monoR"; -export type StudioRecordingMeta = - | { segment: SingleSegment } - | { inner: MultipleSegments }; -export type TargetUnderCursor = { - display_id: DisplayId | null; - window: WindowUnderCursor | null; - screen: ScreenUnderCursor | null; -}; -export type TimelineConfiguration = { - segments: TimelineSegment[]; - zoomSegments: ZoomSegment[]; -}; -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 }; -export type UploadResult = - | { Success: string } - | "NotAuthenticated" - | "PlanCheckFailed" - | "UpgradeRequired"; -export type Video = { - duration: number; - width: number; - height: number; - fps: number; - start_time: number; -}; -export type VideoMeta = { - path: string; - fps?: number; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type VideoRecordingMetadata = { duration: number; size: number }; -export type VideoUploadInfo = { - id: string; - link: string; - config: S3UploadMeta; -}; -export type WindowId = string; -export type WindowUnderCursor = { - id: WindowId; - app_name: string; - bounds: LogicalBounds; - icon: string | null; -}; -export type XY = { x: T; y: T }; -export type ZoomMode = "auto" | { manual: { x: number; y: number } }; -export type ZoomSegment = { - start: number; - end: number; - amount: number; - mode: ZoomMode; -}; +export type AppTheme = "system" | "light" | "dark" +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" +export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } +export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } +export type AudioInputLevelChange = number +export type AudioMeta = { path: string; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type AuthSecret = { api_key: string } | { token: string; expires: number } +export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } +export type AuthenticationInvalid = null +export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } +export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } +export type BlurSegment = { start: number; end: number; blur_amount: number | null; rect: Rect } +export type Bounds = { x: number; y: number; width: number; height: number } +export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } +export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } +export type CameraPreviewShape = "round" | "square" | "full" +export type CameraPreviewSize = "sm" | "lg" +export type CameraShape = "square" | "source" +export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } +export type CameraXPosition = "left" | "center" | "right" +export type CameraYPosition = "top" | "bottom" +export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } +export type CaptionSegment = { id: string; start: number; end: number; text: string } +export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } +export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } +export type CaptureScreen = { id: number; name: string; refresh_rate: number } +export type CaptureWindow = { id: number; owner_name: string; name: string; bounds: Bounds; refresh_rate: number } +export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } +export type Crop = { position: XY; size: XY } +export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } +export type CurrentRecordingChanged = null +export type CurrentRecordingTarget = { window: { id: number; bounds: Bounds } } | { screen: { id: number } } | { area: { screen: number; bounds: Bounds } } +export type CursorAnimationStyle = "regular" | "slow" | "fast" +export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } +export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } +export type CursorType = "pointer" | "circle" +export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } +export type DisplayId = string +export type DownloadProgress = { progress: number; message: string } +export type EditorStateChanged = { playhead_position: number } +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" +export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } +export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) +export type FileType = "recording" | "screenshot" +export type Flags = { captions: boolean } +export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } +export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } +export type GifExportSettings = { fps: number; resolution_base: XY } +export type HapticPattern = "Alignment" | "LevelChange" | "Generic" +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" +export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } +export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" +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 LogicalBounds = { position: LogicalPosition; size: LogicalSize } +export type LogicalPosition = { x: number; y: number } +export type LogicalSize = { width: number; height: number } +export type MainWindowRecordingStartBehaviour = "close" | "minimise" +export type ModelIDType = string +export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } +export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } +export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } +export type NewNotification = { title: string; body: string; is_error: boolean } +export type NewScreenshotAdded = { path: string } +export type NewStudioRecordingAdded = { path: string } +export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" +export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } +export type OnEscapePress = null +export type PhysicalSize = { width: number; height: number } +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } +export type Platform = "MacOS" | "Windows" +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" +export type Preset = { name: string; config: ProjectConfiguration } +export type PresetsStore = { presets: Preset[]; default: number | null } +export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } +export type RecordingDeleted = { path: string } +export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } +export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } +export type RecordingMode = "studio" | "instant" +export type RecordingOptionsChanged = null +export type RecordingStarted = null +export type RecordingStopped = null +export type RecordingType = "studio" | "instant" +export type Rect = { x: number; y: number; width: number; height: number } +export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } +export type RequestNewScreenshot = null +export type RequestOpenSettings = { page: string } +export type RequestStartRecording = null +export type S3UploadMeta = { id: string } +export type ScreenCaptureTarget = { variant: "window"; id: number } | { variant: "screen"; id: number } | { variant: "area"; screen: number; bounds: Bounds } +export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } +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 } +export type ShadowConfiguration = { size: number; opacity: number; blur: number } +export type SharingMeta = { id: string; link: string } +export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: number } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: number } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" +export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } +export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } +export type StereoMode = "stereo" | "monoL" | "monoR" +export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } +export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } +export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; blurSegments: BlurSegment[] | null } +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 } +export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" +export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } +export type VideoMeta = { path: string; fps?: number; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type VideoRecordingMetadata = { duration: number; size: number } +export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } +export type WindowId = string +export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } +export type XY = { x: T; y: T } +export type ZoomMode = "auto" | { manual: { x: number; y: number } } +export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } /** tauri-specta globals **/ import { - type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -807,8 +474,9 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & - ((handle: __WebviewWindow__) => __EventObj__); + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; }, { get: (_, event) => { diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs index 3202b1e5c..f749c5006 100644 --- a/crates/project/src/configuration.rs +++ b/crates/project/src/configuration.rs @@ -449,6 +449,25 @@ pub struct ZoomSegment { pub mode: ZoomMode, } +#[derive(Serialize, Deserialize, Clone, Debug, Type)] +#[serde(rename_all = "camelCase")] +pub struct Rect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + + +#[derive(Serialize, Deserialize, Clone, Debug, Type)] +pub struct BlurSegment { + pub start:f32, + pub end: f32, + pub blur_amount: Option, + pub rect: Rect, +} + + #[derive(Type, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub enum ZoomMode { @@ -461,6 +480,8 @@ pub enum ZoomMode { pub struct TimelineConfiguration { pub segments: Vec, pub zoom_segments: Vec, + pub blur_segments: Option>, + } impl TimelineConfiguration { diff --git a/crates/rendering/src/frame_pipeline.rs b/crates/rendering/src/frame_pipeline.rs index 700176605..ef430afc1 100644 --- a/crates/rendering/src/frame_pipeline.rs +++ b/crates/rendering/src/frame_pipeline.rs @@ -3,6 +3,7 @@ use wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; use crate::{ProjectUniforms, RenderSession, RenderingError}; + // pub struct FramePipelineState<'a> { // pub constants: &'a RenderVideoConstants, // pub uniforms: &'a ProjectUniforms, diff --git a/crates/rendering/src/layers/mod.rs b/crates/rendering/src/layers/mod.rs index 1469690f2..ef9e226af 100644 --- a/crates/rendering/src/layers/mod.rs +++ b/crates/rendering/src/layers/mod.rs @@ -4,6 +4,8 @@ mod camera; mod captions; mod cursor; mod display; +mod selective_blur; + pub use background::*; pub use blur::*; @@ -11,3 +13,4 @@ pub use camera::*; pub use captions::*; pub use cursor::*; pub use display::*; +pub use selective_blur::*; \ No newline at end of file diff --git a/crates/rendering/src/layers/selective_blur.rs b/crates/rendering/src/layers/selective_blur.rs new file mode 100644 index 000000000..22fbf0057 --- /dev/null +++ b/crates/rendering/src/layers/selective_blur.rs @@ -0,0 +1,114 @@ +use crate::selective_blur_pipeline::{ + BlurSegment as GpuBlurSegment, SelectiveBlurPipeline, SelectiveBlurUniforms, +}; +use crate::{ProjectUniforms}; +use bytemuck::cast_slice; +use wgpu::{util::DeviceExt, RenderPass}; + +pub struct SelectiveBlurLayer { + pipeline: SelectiveBlurPipeline, + sampler: wgpu::Sampler, +} + +impl SelectiveBlurLayer { + pub fn new(device: &wgpu::Device) -> Self { + let pipeline = SelectiveBlurPipeline::new(device, wgpu::TextureFormat::Rgba8UnormSrgb); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Selective Blur Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + Self { pipeline, sampler } + } + + pub fn render( + &self, + pass: &mut RenderPass, + device: &wgpu::Device, + input_texture_view: &wgpu::TextureView, + uniforms: &ProjectUniforms, + current_time: f32, + ) { + let active_segments: Vec<&cap_project::BlurSegment> = uniforms + .project + .timeline + .as_ref() + .and_then(|t| t.blur_segments.as_ref()) + .map(|segments| { + segments + .iter() + .filter(|segment| current_time >= segment.start && current_time <= segment.end) + .collect() + }) + .unwrap_or_default(); + + if active_segments.is_empty() { + return; + } + + let gpu_blur_segments: Vec = active_segments + .iter() + .map(|segment| GpuBlurSegment { + rect: [ + segment.rect.x as f32, + segment.rect.y as f32, + segment.rect.width as f32, + segment.rect.height as f32, + ], + blur_amount: segment.blur_amount.unwrap_or(8.0), + _padding: [0.0; 3], + }) + .collect(); + + let blur_uniforms = SelectiveBlurUniforms { + output_size: [uniforms.output_size.0 as f32, uniforms.output_size.1 as f32], + blur_segments_count: gpu_blur_segments.len() as u32, + _padding: 0.0, + }; + + let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Selective Blur Uniforms"), + contents: cast_slice(&[blur_uniforms]), + usage: wgpu::BufferUsages::UNIFORM, + }); + + let segments_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Blur Segments Buffer"), + contents: cast_slice(&gpu_blur_segments), + usage: wgpu::BufferUsages::STORAGE, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Selective Blur Bind Group"), + layout: &self.pipeline.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniforms_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(input_texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: segments_buffer.as_entire_binding(), + }, + ], + }); + + pass.set_pipeline(&self.pipeline.render_pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.draw(0..3, 0..1); + } +} \ No newline at end of file diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index cbae60a98..99af9a39b 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -4,14 +4,14 @@ use cap_project::{ ProjectConfiguration, RecordingMeta, StudioRecordingMeta, XY, }; use composite_frame::CompositeVideoFrameUniforms; -use core::f64; +use core::{f64, time}; use cursor_interpolation::{InterpolatedCursorPosition, interpolate_cursor}; use decoder::{AsyncVideoDecoderHandle, spawn_decoder}; use frame_pipeline::finish_encoder; use futures::FutureExt; use futures::future::OptionFuture; use layers::{ - Background, BackgroundLayer, BlurLayer, CameraLayer, CaptionsLayer, CursorLayer, DisplayLayer, + Background, BackgroundLayer, BlurLayer, CameraLayer, CaptionsLayer, CursorLayer, DisplayLayer,SelectiveBlurLayer }; use specta::Type; use spring_mass_damper::SpringMassDamperSimulationConfig; @@ -19,16 +19,18 @@ use std::{collections::HashMap, sync::Arc}; use std::{path::PathBuf, time::Instant}; use tokio::sync::mpsc; use tracing::error; - mod composite_frame; mod coord; -mod cursor_interpolation; +mod cursor_interpolation; pub mod decoder; mod frame_pipeline; mod layers; mod project_recordings; mod spring_mass_damper; mod zoom; +mod selective_blur_pipeline; + + pub use coord::*; pub use decoder::DecodedFrame; @@ -823,6 +825,7 @@ pub struct RendererLayers { camera: CameraLayer, #[allow(unused)] captions: CaptionsLayer, + selective_blur:SelectiveBlurLayer } impl RendererLayers { @@ -834,6 +837,7 @@ impl RendererLayers { cursor: CursorLayer::new(device), camera: CameraLayer::new(device), captions: CaptionsLayer::new(device, queue), + selective_blur:SelectiveBlurLayer::new(device), } } @@ -902,6 +906,8 @@ impl RendererLayers { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, session: &mut RenderSession, + uniforms: &ProjectUniforms, // Add this parameter + current_time: f32, // Add this parameter ) { macro_rules! render_pass { ($view:expr, $load:expr) => { @@ -953,6 +959,16 @@ impl RendererLayers { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.camera.render(&mut pass); } + { + let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); + self.selective_blur.render( + &mut pass, + device, + session.current_texture_view(), + uniforms, + current_time, + ); + } // { // let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); @@ -1076,7 +1092,7 @@ async fn produce_frame( }), ); - layers.render(&constants.device, &mut encoder, session); + layers.render(&constants.device, &mut encoder, session,&uniforms); finish_encoder( session, diff --git a/crates/rendering/src/selective_blur_pipeline.rs b/crates/rendering/src/selective_blur_pipeline.rs new file mode 100644 index 000000000..af5988e33 --- /dev/null +++ b/crates/rendering/src/selective_blur_pipeline.rs @@ -0,0 +1,160 @@ +use bytemuck::{Pod, Zeroable}; +use std::{borrow::Cow, default}; +use tracing::info; +use wgpu::util::DeviceExt; + +#[repr(C)] +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +pub struct BlurSegment { + pub rect: [f32; 4], // Corresponds to vec4 + pub blur_amount: f32, + pub _padding: [f32; 3], // Crucial for memory alignment to match WGSL's vec4 rules. +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +pub struct SelectiveBlurUniforms { + pub output_size: [f32; 2], + pub blur_segments_count: u32, + pub _padding: f32, +} + +pub struct SelectiveBlurPipeline { + pub bind_group_layout: wgpu::BindGroupLayout, + pub render_pipeline: wgpu::RenderPipeline, +} + +impl SelectiveBlurPipeline { + pub fn new(device: &wgpu::Device, texture_format: wgpu::TextureFormat) -> Self { + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Selective Blur Shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("./shaders/blur.wgsl"))), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Selective Blur Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Selective Blur Pipeline Layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Selective Blur Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache:None + }); + + Self { + bind_group_layout, + render_pipeline, + } + } + + pub fn create_bind_group( + &self, + device: &wgpu::Device, + uniforms: &SelectiveBlurUniforms, + texture_view: &wgpu::TextureView, + sampler: &wgpu::Sampler, + blur_segments: &[BlurSegment], + ) -> wgpu::BindGroup { + info!("Creating bind group with {} blur segments", blur_segments.len()); + + let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Selective Blur Uniforms Buffer"), + contents: bytemuck::cast_slice(&[*uniforms]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let blur_segments_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Blur Segments Buffer"), + contents: bytemuck::cast_slice(blur_segments), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + }); + + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniforms_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: blur_segments_buffer.as_entire_binding(), + }, + ], + label: Some("Selective Blur Bind Group"), + }) + } +} \ No newline at end of file diff --git a/crates/rendering/src/shaders/blur.wgsl b/crates/rendering/src/shaders/blur.wgsl new file mode 100644 index 000000000..8c9d7f981 --- /dev/null +++ b/crates/rendering/src/shaders/blur.wgsl @@ -0,0 +1,64 @@ +struct BlurSegment { + rect: vec4, // [x, y, width, height] + blur_amount: f32, + @align(16) _padding: vec3, +} + +struct Uniforms { + output_size: vec2, + blur_segments_count: u32, + @align(16) _padding: f32, +} + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var t_input: texture_2d; +@group(0) @binding(2) var s_input: sampler; +@group(0) @binding(3) var blur_segments: array; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let uv = vec2( + f32(vertex_index & 1u), + f32((vertex_index >> 1u) & 1u) + ); + let pos = vec4(uv * 2.0 - 1.0, 0.0, 1.0); + return VertexOutput(pos, uv); +} + +fn apply_blur(uv: vec2, blur_amount: f32) -> vec4 { + let pixel_size = 1.0 / uniforms.output_size; + var color = vec4(0.0); + var total_weight = 0.0; + let radius = i32(ceil(blur_amount)); + + for (var x = -2; x <= 2; x = x + 1) { + for (var y = -2; y <= 2; y = y + 1) { + let offset = vec2(f32(x), f32(y)) * pixel_size * blur_amount / 2.0; + let weight = exp(-f32(x * x + y * y) / (2.0 * blur_amount * blur_amount)); + color = color + textureSample(t_input, s_input, uv + offset) * weight; + total_weight = total_weight + weight; + } + } + return color / total_weight; +} + +@fragment +fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { + let debug_mode = false; + for (var i: u32 = 0u; i < uniforms.blur_segments_count; i = i + 1u) { + let segment = blur_segments[i]; + if (uv.x >= segment.rect.x && uv.x <= segment.rect.x + segment.rect.z && + uv.y >= segment.rect.y && uv.y <= segment.rect.y + segment.rect.w) { + if (debug_mode) { + return vec4(1.0, 0.0, 0.0, 1.0); + } + return apply_blur(uv, segment.blur_amount); + } + } + return textureSample(t_input, s_input, uv); +} \ No newline at end of file diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 7c165f2b9..4532b8fce 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -4,89 +4,90 @@ // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import // biome-ignore lint: disable -export {}; +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 IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; - const IconCapCaptions: typeof import("~icons/cap/captions.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 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 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 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 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 IconCapPauseCircle: typeof import("~icons/cap/pause-circle.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 IconCapRestart: typeof import("~icons/cap/restart.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 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 IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"]; - const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"]; - const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"]; - const IconFa6SolidDisplay: typeof import("~icons/fa6-solid/display.jsx")["default"]; - const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"]; - const IconIcBaselineMonitor: typeof import("~icons/ic/baseline-monitor.jsx")["default"]; - const IconIcRoundSearch: typeof import("~icons/ic/round-search.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 IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"]; - const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; - const IconLucideEye: typeof import("~icons/lucide/eye.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 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 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 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 IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"]; - const IconLucideX: typeof import("~icons/lucide/x.jsx")["default"]; - const IconMaterialSymbolsLightScreenshotFrame2: typeof import("~icons/material-symbols-light/screenshot-frame2.jsx")["default"]; - const IconMaterialSymbolsLightScreenshotFrame2MaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols-light/screenshot-frame2-material-symbols-screenshot-frame2-rounded.jsx")["default"]; - const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"]; - const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"]; - const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; + 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 IconCapBlur: typeof import('~icons/cap/blur.jsx')['default'] + const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] + const IconCapCaptions: typeof import('~icons/cap/captions.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 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 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 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 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 IconCapPauseCircle: typeof import('~icons/cap/pause-circle.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 IconCapRestart: typeof import('~icons/cap/restart.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 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 IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] + const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] + const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] + const IconFa6SolidDisplay: typeof import("~icons/fa6-solid/display.jsx")["default"] + const IconHugeiconsEaseCurveControlPoints: typeof import('~icons/hugeicons/ease-curve-control-points.jsx')['default'] + const IconIcBaselineMonitor: typeof import("~icons/ic/baseline-monitor.jsx")["default"] + const IconIcRoundSearch: typeof import("~icons/ic/round-search.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 IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"] + const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"] + const IconLucideEye: typeof import("~icons/lucide/eye.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 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 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 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 IconLucideVolumeX: typeof import('~icons/lucide/volume-x.jsx')['default'] + const IconLucideX: typeof import("~icons/lucide/x.jsx")["default"] + const IconMaterialSymbolsLightScreenshotFrame2: typeof import("~icons/material-symbols-light/screenshot-frame2.jsx")["default"] + const IconMaterialSymbolsLightScreenshotFrame2MaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols-light/screenshot-frame2-material-symbols-screenshot-frame2-rounded.jsx")["default"] + const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"] + const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"] + const IconPhMonitorBold: typeof import('~icons/ph/monitor-bold.jsx')['default'] } From 2afa490db8aee9049bf28f69312ace58d4ead134 Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 20 Aug 2025 14:18:06 +0530 Subject: [PATCH 2/3] fix blur size and opacity correctly with brute force gaussian blur --- .../desktop/src/routes/editor/BlurOverlay.tsx | 64 +++++------ .../src/routes/editor/ConfigSidebar.tsx | 12 ++- .../src/routes/editor/Timeline/BlurTrack.tsx | 46 ++------ crates/rendering/src/layers/selective_blur.rs | 83 ++++++--------- crates/rendering/src/lib.rs | 30 +++++- crates/rendering/src/shaders/blur.wgsl | 100 +++++++++++++----- 6 files changed, 181 insertions(+), 154 deletions(-) diff --git a/apps/desktop/src/routes/editor/BlurOverlay.tsx b/apps/desktop/src/routes/editor/BlurOverlay.tsx index 93f2307a5..65374785d 100644 --- a/apps/desktop/src/routes/editor/BlurOverlay.tsx +++ b/apps/desktop/src/routes/editor/BlurOverlay.tsx @@ -21,6 +21,7 @@ export function BlurOverlay() { const containerBounds = createElementBounds(canvasContainerRef); const currentTime = () => editorState.previewTime ?? editorState.playbackTime ?? 0; + const activeBlurSegmentsWithIndex = () => { return (project.timeline?.blurSegments || []).map((segment, index) => ({ segment, index })).filter( @@ -86,7 +87,7 @@ function BlurRectangle(props: BlurRectangleProps) { const startX = e.clientX; const startY = e.clientY; const startRect = { ...props.rect }; - + createRoot((dispose) => { createEventListenerMap(window, { mousemove: (moveEvent: MouseEvent) => { @@ -96,42 +97,35 @@ function BlurRectangle(props: BlurRectangleProps) { let newRect = { ...startRect }; if (action === 'move') { + // Clamp the new position to stay within the 0.0 to 1.0 bounds newRect.x = Math.max(0, Math.min(1 - newRect.width, startRect.x + deltaX)); newRect.y = Math.max(0, Math.min(1 - newRect.height, startRect.y + deltaY)); } else if (action === 'resize') { - switch (corner) { - case 'nw': // Northwest corner - newRect.x = Math.max(0, startRect.x + deltaX); - newRect.y = Math.max(0, startRect.y + deltaY); - newRect.width = startRect.width - deltaX; - newRect.height = startRect.height - deltaY; - break; - case 'ne': // Northeast corner - newRect.y = Math.max(0, startRect.y + deltaY); - newRect.width = startRect.width + deltaX; - newRect.height = startRect.height - deltaY; - break; - case 'sw': // Southwest corner - newRect.x = Math.max(0, startRect.x + deltaX); - newRect.width = startRect.width - deltaX; - newRect.height = startRect.height + deltaY; - break; - case 'se': // Southeast corner - newRect.width = startRect.width + deltaX; - newRect.height = startRect.height + deltaY; - break; - } + // --- This resize logic needs the bounds check --- + let right = startRect.x + startRect.width; + let bottom = startRect.y + startRect.height; - // Ensure minimum size - newRect.width = Math.max(0.05, newRect.width); - newRect.height = Math.max(0.05, newRect.height); - - // Ensure within bounds - newRect.x = Math.max(0, Math.min(1 - newRect.width, newRect.x)); - newRect.y = Math.max(0, Math.min(1 - newRect.height, newRect.y)); - newRect.width = Math.min(1 - newRect.x, newRect.width); - newRect.height = Math.min(1 - newRect.y, newRect.height); + if (corner?.includes('w')) { // West (left) handles + newRect.x = Math.max(0, startRect.x + deltaX); + newRect.width = right - newRect.x; + } + if (corner?.includes('n')) { // North (top) handles + newRect.y = Math.max(0, startRect.y + deltaY); + newRect.height = bottom - newRect.y; + } + if (corner?.includes('e')) { // East (right) handles + right = Math.min(1, right + deltaX); + newRect.width = right - newRect.x; + } + if (corner?.includes('s')) { // South (bottom) handles + bottom = Math.min(1, bottom + deltaY); + newRect.height = bottom - newRect.y; + } } + + // Ensure minimum size after any operation + if (newRect.width < 0.05) newRect.width = 0.05; + if (newRect.height < 0.05) newRect.height = 0.05; props.onUpdate(newRect); }, @@ -141,7 +135,7 @@ function BlurRectangle(props: BlurRectangleProps) { }); }); }; - + const scaledBlurAmount = () => (props.blurAmount ?? 0) * 20; return (
diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 049bd1859..51bbb0ffb 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -2034,8 +2034,9 @@ function BlurSegmentConfig(props: {
}> - setProject( "timeline", @@ -2045,10 +2046,11 @@ function BlurSegmentConfig(props: { v[0], ) } + minValue={0} - maxValue={20} - step={0.1} - formatTooltip="px" + maxValue={1} + step={0.01} + formatTooltip={(value) => `${Math.round(value * 100)}%`} /> diff --git a/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx b/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx index fd350d822..a8d891330 100644 --- a/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/BlurTrack.tsx @@ -2,7 +2,6 @@ import { createEventListener, createEventListenerMap, } from "@solid-primitives/event-listener"; -import { Menu } from "@tauri-apps/api/menu"; import { cx } from "cva"; import { batch, @@ -13,7 +12,6 @@ import { Show, } from "solid-js"; import { produce } from "solid-js/store"; -import { commands } from "~/utils/tauri"; import { useEditorContext } from "../context"; import { useSegmentContext, @@ -39,33 +37,8 @@ export function BlurTrack(props: { const [hoveringSegment, setHoveringSegment] = createSignal(false); const [hoveredTime, setHoveredTime] = createSignal(); - const handleGenerateBlurSegments = async () => { - try { - const blurSegments = await commands.generateBlurSegmentsFromClicks(); - setProject("timeline", "blurSegments", blurSegments); - } catch (error) { - console.error("Failed to generate blur segments:", error); - } - }; - return ( { - if (!import.meta.env.DEV) return; - - e.preventDefault(); - const menu = await Menu.new({ - id: "blur-track-options", - items: [ - { - id: "generateBlurSegments", - text: "Generate blur segments from clicks", - action: handleGenerateBlurSegments, - }, - ], - }); - menu.popup(); - }} onMouseMove={(e) => { if (hoveringSegment()) { setHoveredTime(undefined); @@ -161,7 +134,8 @@ export function BlurTrack(props: { const blurPercentage = () => { const amount = segment.blur_amount; - return `${amount.toFixed(1)}x`; + // Handle potential null or undefined amount + return amount ? `${amount.toFixed(1)}x` : '...'; }; const blurSegments = () => project.timeline!.blurSegments!; @@ -249,7 +223,7 @@ export function BlurTrack(props: { ? "wobble-wrapper border-blue-400" : "border-transparent", )} - + innerClass="ring-red-5" segment={segment} onMouseEnter={() => { @@ -299,7 +273,7 @@ export function BlurTrack(props: { "timeline", "blurSegments", produce((s) => { - s.sort((a, b) => a.start - b.start); + s?.sort((a, b) => a.start - b.start); }), ); }, @@ -337,10 +311,11 @@ export function BlurTrack(props: { else if (newEnd > value.maxEnd) delta = value.maxEnd - value.original.end; - setProject("timeline", "blurSegments", i(), { + setProject("timeline", "blurSegments", i(), (prev) => ({ + ...prev, start: value.original.start + delta, end: value.original.end + delta, - }); + })); }, )} > @@ -352,7 +327,8 @@ export function BlurTrack(props: {
Blur
- {" "} + {/* Assuming you have an IconCapBlur component */} + {/* {" "} */} {blurPercentage()}{" "}
@@ -399,7 +375,7 @@ export function BlurTrack(props: { "timeline", "blurSegments", produce((s) => { - s.sort((a, b) => a.start - b.start); + s?.sort((a, b) => a.start - b.start); }), ); }, @@ -431,4 +407,4 @@ export function BlurTrack(props: {
); -} +} \ No newline at end of file diff --git a/crates/rendering/src/layers/selective_blur.rs b/crates/rendering/src/layers/selective_blur.rs index 22fbf0057..4cb43038f 100644 --- a/crates/rendering/src/layers/selective_blur.rs +++ b/crates/rendering/src/layers/selective_blur.rs @@ -13,6 +13,8 @@ pub struct SelectiveBlurLayer { impl SelectiveBlurLayer { pub fn new(device: &wgpu::Device) -> Self { let pipeline = SelectiveBlurPipeline::new(device, wgpu::TextureFormat::Rgba8UnormSrgb); + + // High-quality sampler for smooth blur let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Selective Blur Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, @@ -21,6 +23,7 @@ impl SelectiveBlurLayer { mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, + anisotropy_clamp: 1, ..Default::default() }); @@ -43,28 +46,39 @@ impl SelectiveBlurLayer { .map(|segments| { segments .iter() - .filter(|segment| current_time >= segment.start && current_time <= segment.end) + .filter(|segment| { + current_time >= segment.start as f32 && + current_time <= segment.end as f32 + }) .collect() }) .unwrap_or_default(); - if active_segments.is_empty() { - return; - } - let gpu_blur_segments: Vec = active_segments .iter() - .map(|segment| GpuBlurSegment { - rect: [ - segment.rect.x as f32, - segment.rect.y as f32, - segment.rect.width as f32, - segment.rect.height as f32, - ], - blur_amount: segment.blur_amount.unwrap_or(8.0), - _padding: [0.0; 3], + .filter(|segment| segment.blur_amount.unwrap_or(0.0) >= 0.01) + .map(|segment| { + // Convert from 0-1 slider range to shader-appropriate values + let blur_intensity = segment.blur_amount.unwrap_or(0.0) as f32; + + let shader_blur_amount = blur_intensity * 8.0; + + GpuBlurSegment { + rect: [ + segment.rect.x as f32, + segment.rect.y as f32, + segment.rect.width as f32, + segment.rect.height as f32, + ], + blur_amount: shader_blur_amount, + _padding: [0.0; 3], + } }) .collect(); + + if gpu_blur_segments.is_empty() { + return; + } let blur_uniforms = SelectiveBlurUniforms { output_size: [uniforms.output_size.0 as f32, uniforms.output_size.1 as f32], @@ -72,40 +86,13 @@ impl SelectiveBlurLayer { _padding: 0.0, }; - let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Selective Blur Uniforms"), - contents: cast_slice(&[blur_uniforms]), - usage: wgpu::BufferUsages::UNIFORM, - }); - - let segments_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Blur Segments Buffer"), - contents: cast_slice(&gpu_blur_segments), - usage: wgpu::BufferUsages::STORAGE, - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Selective Blur Bind Group"), - layout: &self.pipeline.bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniforms_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(input_texture_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: segments_buffer.as_entire_binding(), - }, - ], - }); + let bind_group = self.pipeline.create_bind_group( + device, + &blur_uniforms, + input_texture_view, + &self.sampler, + &gpu_blur_segments, + ); pass.set_pipeline(&self.pipeline.render_pipeline); pass.set_bind_group(0, &bind_group, &[]); diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 99af9a39b..43eb82dff 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -959,17 +959,26 @@ impl RendererLayers { let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); self.camera.render(&mut pass); } - { - let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); + if !active_blur_segments(uniforms, current_time).is_empty() { + // Start a new render pass that WRITES to the OTHER texture. + let mut pass = render_pass!(session.other_texture_view(), wgpu::LoadOp::Load); + self.selective_blur.render( &mut pass, device, + // READ from the CURRENT texture. session.current_texture_view(), uniforms, current_time, ); + + // IMPORTANT: After the pass is done, swap the textures. + // The blurred result is now the "current" texture. + drop(pass); + session.swap_textures(); } + // { // let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load); // self.captions.render(&mut pass); @@ -977,6 +986,21 @@ impl RendererLayers { } } +fn active_blur_segments(uniforms: &ProjectUniforms, current_time: f32) -> Vec<&cap_project::BlurSegment> { + uniforms + .project + .timeline + .as_ref() + .and_then(|t| t.blur_segments.as_ref()) + .map(|segments| { + segments + .iter() + .filter(|segment| current_time >= segment.start as f32 && current_time <= segment.end as f32) + .collect() + }) + .unwrap_or_default() +} + pub struct RenderSession { textures: (wgpu::Texture, wgpu::Texture), texture_views: (wgpu::TextureView, wgpu::TextureView), @@ -1092,7 +1116,7 @@ async fn produce_frame( }), ); - layers.render(&constants.device, &mut encoder, session,&uniforms); + layers.render(&constants.device, &mut encoder, session,&uniforms,segment_frames.recording_time); finish_encoder( session, diff --git a/crates/rendering/src/shaders/blur.wgsl b/crates/rendering/src/shaders/blur.wgsl index 8c9d7f981..258901337 100644 --- a/crates/rendering/src/shaders/blur.wgsl +++ b/crates/rendering/src/shaders/blur.wgsl @@ -1,14 +1,24 @@ +/** + * @struct BlurSegment + * Defines the data structure for a single blur region. + */ struct BlurSegment { - rect: vec4, // [x, y, width, height] + rect: vec4, blur_amount: f32, - @align(16) _padding: vec3, -} + _padding1: f32, + _padding2: f32, + _padding3: f32, +}; +/** + * @struct Uniforms + * Defines global settings for the shader pass. + */ struct Uniforms { output_size: vec2, blur_segments_count: u32, - @align(16) _padding: f32, -} + _padding: f32, +}; @group(0) @binding(0) var uniforms: Uniforms; @group(0) @binding(1) var t_input: texture_2d; @@ -18,47 +28,81 @@ struct Uniforms { struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, -} +}; @vertex fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { - let uv = vec2( - f32(vertex_index & 1u), - f32((vertex_index >> 1u) & 1u) + // Generate a full-screen triangle with consistent UV mapping + // (0,0) = top-left, (1,1) = bottom-right (matches frontend) + var positions = array, 3>( + vec2(-1.0, -1.0), // Bottom-left in clip space + vec2( 3.0, -1.0), // Far bottom-right in clip space + vec2(-1.0, 3.0) // Far top-left in clip space + ); + + // Fixed UV coordinates - consistent mapping + var uvs = array, 3>( + vec2(0.0, 1.0), // Bottom-left in texture space + vec2(2.0, 1.0), // Far bottom-right in texture space + vec2(0.0, -1.0) // Far top-left in texture space ); - let pos = vec4(uv * 2.0 - 1.0, 0.0, 1.0); - return VertexOutput(pos, uv); + + let pos = positions[vertex_index]; + let uv = uvs[vertex_index]; + + return VertexOutput(vec4(pos, 0.0, 1.0), uv); } +/** + * @function apply_blur + * Performs Gaussian blur with proper sampling + */ fn apply_blur(uv: vec2, blur_amount: f32) -> vec4 { let pixel_size = 1.0 / uniforms.output_size; var color = vec4(0.0); var total_weight = 0.0; - let radius = i32(ceil(blur_amount)); + + // Reduced kernel size for better performance + let radius = i32(blur_amount * 8.0); // Dynamic radius based on blur amount + let max_radius = min(radius, 25); // Cap at 25 to prevent excessive samples + let sigma = f32(max_radius) / 2.5; - for (var x = -2; x <= 2; x = x + 1) { - for (var y = -2; y <= 2; y = y + 1) { - let offset = vec2(f32(x), f32(y)) * pixel_size * blur_amount / 2.0; - let weight = exp(-f32(x * x + y * y) / (2.0 * blur_amount * blur_amount)); - color = color + textureSample(t_input, s_input, uv + offset) * weight; - total_weight = total_weight + weight; + for (var y = -max_radius; y <= max_radius; y = y + 1) { + for (var x = -max_radius; x <= max_radius; x = x + 1) { + let offset = vec2(f32(x), f32(y)) * pixel_size; + let sample_pos = uv + offset; + + // Sample from anywhere in the texture, not restricted to rectangle + // This allows blur to sample from outside the blur region + let sample_uv = clamp(sample_pos, vec2(0.0), vec2(1.0)); + + let dist_sq = f32(x * x + y * y); + let weight = exp(-dist_sq / (2.0 * sigma * sigma)); + + color += textureSample(t_input, s_input, sample_uv) * weight; + total_weight += weight; } } - return color / total_weight; + + return color / max(total_weight, 0.001); } @fragment -fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { - let debug_mode = false; +fn fs_main(frag_in: VertexOutput) -> @location(0) vec4 { + // Check if current pixel is inside any blur rectangle for (var i: u32 = 0u; i < uniforms.blur_segments_count; i = i + 1u) { let segment = blur_segments[i]; - if (uv.x >= segment.rect.x && uv.x <= segment.rect.x + segment.rect.z && - uv.y >= segment.rect.y && uv.y <= segment.rect.y + segment.rect.w) { - if (debug_mode) { - return vec4(1.0, 0.0, 0.0, 1.0); - } - return apply_blur(uv, segment.blur_amount); + let rect = segment.rect; + + // Check if pixel is inside the blur rectangle + if (frag_in.uv.x >= rect.x && frag_in.uv.x <= rect.x + rect.z && + frag_in.uv.y >= rect.y && frag_in.uv.y <= rect.y + rect.w) { + + // Apply blur - sample from entire texture, not just rectangle + return apply_blur(frag_in.uv, segment.blur_amount); } } - return textureSample(t_input, s_input, uv); + + // If pixel is not in any blur rectangle, return original color + return textureSample(t_input, s_input, frag_in.uv); } \ No newline at end of file From 832647b23a1b3378827d3473e17552a92f396d2d Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 20 Aug 2025 16:48:42 +0530 Subject: [PATCH 3/3] remove some comments --- crates/rendering/src/shaders/blur.wgsl | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/rendering/src/shaders/blur.wgsl b/crates/rendering/src/shaders/blur.wgsl index 258901337..75c823424 100644 --- a/crates/rendering/src/shaders/blur.wgsl +++ b/crates/rendering/src/shaders/blur.wgsl @@ -32,15 +32,14 @@ struct VertexOutput { @vertex fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { - // Generate a full-screen triangle with consistent UV mapping - // (0,0) = top-left, (1,1) = bottom-right (matches frontend) + var positions = array, 3>( vec2(-1.0, -1.0), // Bottom-left in clip space vec2( 3.0, -1.0), // Far bottom-right in clip space vec2(-1.0, 3.0) // Far top-left in clip space ); - // Fixed UV coordinates - consistent mapping + var uvs = array, 3>( vec2(0.0, 1.0), // Bottom-left in texture space vec2(2.0, 1.0), // Far bottom-right in texture space @@ -53,10 +52,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return VertexOutput(vec4(pos, 0.0, 1.0), uv); } -/** - * @function apply_blur - * Performs Gaussian blur with proper sampling - */ + fn apply_blur(uv: vec2, blur_amount: f32) -> vec4 { let pixel_size = 1.0 / uniforms.output_size; var color = vec4(0.0); @@ -72,8 +68,7 @@ fn apply_blur(uv: vec2, blur_amount: f32) -> vec4 { let offset = vec2(f32(x), f32(y)) * pixel_size; let sample_pos = uv + offset; - // Sample from anywhere in the texture, not restricted to rectangle - // This allows blur to sample from outside the blur region + let sample_uv = clamp(sample_pos, vec2(0.0), vec2(1.0)); let dist_sq = f32(x * x + y * y); @@ -89,7 +84,7 @@ fn apply_blur(uv: vec2, blur_amount: f32) -> vec4 { @fragment fn fs_main(frag_in: VertexOutput) -> @location(0) vec4 { - // Check if current pixel is inside any blur rectangle + for (var i: u32 = 0u; i < uniforms.blur_segments_count; i = i + 1u) { let segment = blur_segments[i]; let rect = segment.rect; @@ -103,6 +98,5 @@ fn fs_main(frag_in: VertexOutput) -> @location(0) vec4 { } } - // If pixel is not in any blur rectangle, return original color return textureSample(t_input, s_input, frag_in.uv); } \ No newline at end of file