Skip to content

Commit 56421f6

Browse files
committed
feat:Add blur track with shaders file for it
1 parent 35dedfa commit 56421f6

File tree

16 files changed

+1730
-858
lines changed

16 files changed

+1730
-858
lines changed

apps/desktop/src-tauri/src/recording.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use cap_media::{
2525
use cap_project::{
2626
CursorClickEvent, Platform, ProjectConfiguration, RecordingMeta, RecordingMetaInner,
2727
SharingMeta, StudioRecordingMeta, TimelineConfiguration, TimelineSegment, ZoomMode,
28-
ZoomSegment, cursor::CursorEvents,
28+
BlurSegment,ZoomSegment, cursor::CursorEvents,
2929
};
3030
use cap_recording::{
3131
CompletedStudioRecording, RecordingError, RecordingMode, StudioRecordingHandle,
@@ -985,6 +985,7 @@ fn project_config_from_recording(
985985
} else {
986986
Vec::new()
987987
},
988+
blur_segments:Some(Vec::new())
988989
}),
989990
..default_config.unwrap_or_default()
990991
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { createElementBounds } from "@solid-primitives/bounds";
2+
import { createEventListenerMap } from "@solid-primitives/event-listener";
3+
import { createRoot, createSignal, For, Show } from "solid-js";
4+
import { cx } from "cva";
5+
import { useEditorContext } from "./context";
6+
7+
8+
interface BlurRectangleProps {
9+
rect: { x: number; y: number; width: number; height: number };
10+
style: { left: string; top: string; width: string; height: string; filter?: string };
11+
onUpdate: (rect: { x: number; y: number; width: number; height: number }) => void;
12+
containerBounds: { width?: number | null; height?: number | null };
13+
blurAmount: number;
14+
isEditing: boolean;
15+
}
16+
17+
export function BlurOverlay() {
18+
const { project, setProject, editorState } = useEditorContext();
19+
20+
const [canvasContainerRef, setCanvasContainerRef] = createSignal<HTMLDivElement>();
21+
const containerBounds = createElementBounds(canvasContainerRef);
22+
23+
const currentTime = () => editorState.previewTime ?? editorState.playbackTime ?? 0;
24+
25+
const activeBlurSegmentsWithIndex = () => {
26+
return (project.timeline?.blurSegments || []).map((segment, index) => ({ segment, index })).filter(
27+
({ segment }) => currentTime() >= segment.start && currentTime() <= segment.end
28+
);
29+
};
30+
31+
const updateBlurRect = (index: number, rect: { x: number; y: number; width: number; height: number }) => {
32+
setProject("timeline", "blurSegments", index, "rect", rect);
33+
};
34+
35+
const isSelected = (index: number) => {
36+
const selection = editorState.timeline.selection;
37+
return selection?.type === "blur" && selection.index === index;
38+
};
39+
40+
return (
41+
<div
42+
ref={setCanvasContainerRef}
43+
class="absolute inset-0 pointer-events-none"
44+
>
45+
<For each={activeBlurSegmentsWithIndex()}>
46+
{({ segment, index }) => {
47+
// Convert normalized coordinates to pixel coordinates
48+
const rectStyle = () => {
49+
const containerWidth = containerBounds.width ?? 1;
50+
const containerHeight = containerBounds.height ?? 1;
51+
52+
return {
53+
left: `${segment.rect.x * containerWidth}px`,
54+
top: `${segment.rect.y * containerHeight}px`,
55+
width: `${segment.rect.width * containerWidth}px`,
56+
height: `${segment.rect.height * containerHeight}px`,
57+
};
58+
};
59+
60+
return (
61+
<BlurRectangle
62+
rect={segment.rect}
63+
style={rectStyle()}
64+
blurAmount={segment.blur_amount || 0}
65+
onUpdate={(newRect) => updateBlurRect(index, newRect)}
66+
containerBounds={containerBounds}
67+
isEditing={isSelected(index)}
68+
/>
69+
);
70+
}}
71+
</For>
72+
</div>
73+
);
74+
}
75+
76+
77+
78+
function BlurRectangle(props: BlurRectangleProps) {
79+
const handleMouseDown = (e: MouseEvent, action: 'move' | 'resize', corner?: string) => {
80+
e.preventDefault();
81+
e.stopPropagation();
82+
83+
const containerWidth = props.containerBounds.width ?? 1;
84+
const containerHeight = props.containerBounds.height ?? 1;
85+
86+
const startX = e.clientX;
87+
const startY = e.clientY;
88+
const startRect = { ...props.rect };
89+
90+
createRoot((dispose) => {
91+
createEventListenerMap(window, {
92+
mousemove: (moveEvent: MouseEvent) => {
93+
const deltaX = (moveEvent.clientX - startX) / containerWidth;
94+
const deltaY = (moveEvent.clientY - startY) / containerHeight;
95+
96+
let newRect = { ...startRect };
97+
98+
if (action === 'move') {
99+
newRect.x = Math.max(0, Math.min(1 - newRect.width, startRect.x + deltaX));
100+
newRect.y = Math.max(0, Math.min(1 - newRect.height, startRect.y + deltaY));
101+
} else if (action === 'resize') {
102+
switch (corner) {
103+
case 'nw': // Northwest corner
104+
newRect.x = Math.max(0, startRect.x + deltaX);
105+
newRect.y = Math.max(0, startRect.y + deltaY);
106+
newRect.width = startRect.width - deltaX;
107+
newRect.height = startRect.height - deltaY;
108+
break;
109+
case 'ne': // Northeast corner
110+
newRect.y = Math.max(0, startRect.y + deltaY);
111+
newRect.width = startRect.width + deltaX;
112+
newRect.height = startRect.height - deltaY;
113+
break;
114+
case 'sw': // Southwest corner
115+
newRect.x = Math.max(0, startRect.x + deltaX);
116+
newRect.width = startRect.width - deltaX;
117+
newRect.height = startRect.height + deltaY;
118+
break;
119+
case 'se': // Southeast corner
120+
newRect.width = startRect.width + deltaX;
121+
newRect.height = startRect.height + deltaY;
122+
break;
123+
}
124+
125+
// Ensure minimum size
126+
newRect.width = Math.max(0.05, newRect.width);
127+
newRect.height = Math.max(0.05, newRect.height);
128+
129+
// Ensure within bounds
130+
newRect.x = Math.max(0, Math.min(1 - newRect.width, newRect.x));
131+
newRect.y = Math.max(0, Math.min(1 - newRect.height, newRect.y));
132+
newRect.width = Math.min(1 - newRect.x, newRect.width);
133+
newRect.height = Math.min(1 - newRect.y, newRect.height);
134+
}
135+
136+
props.onUpdate(newRect);
137+
},
138+
mouseup: () => {
139+
dispose();
140+
},
141+
});
142+
});
143+
};
144+
145+
return (
146+
<div
147+
class={cx(
148+
"absolute",
149+
props.isEditing ? "pointer-events-auto border-2 border-blue-400 bg-blue-400/20" : "pointer-events-none border-none bg-transparent"
150+
)}
151+
style={{
152+
...props.style,
153+
"backdrop-filter": `blur(${props.blurAmount}px)`,
154+
"-webkit-backdrop-filter": `blur(${props.blurAmount}px)`, // Fallback for WebKit browsers
155+
}}
156+
>
157+
<Show when={props.isEditing}>
158+
{/* Main draggable area */}
159+
<div
160+
class="absolute inset-0 cursor-move"
161+
onMouseDown={(e) => handleMouseDown(e, 'move')}
162+
/>
163+
164+
{/* Resize handles */}
165+
<div
166+
class="absolute -top-1 -left-1 w-3 h-3 bg-blue-400 border border-white cursor-nw-resize rounded-full"
167+
onMouseDown={(e) => handleMouseDown(e, 'resize', 'nw')}
168+
/>
169+
<div
170+
class="absolute -top-1 -right-1 w-3 h-3 bg-blue-400 border border-white cursor-ne-resize rounded-full"
171+
onMouseDown={(e) => handleMouseDown(e, 'resize', 'ne')}
172+
/>
173+
<div
174+
class="absolute -bottom-1 -left-1 w-3 h-3 bg-blue-400 border border-white cursor-sw-resize rounded-full"
175+
onMouseDown={(e) => handleMouseDown(e, 'resize', 'sw')}
176+
/>
177+
<div
178+
class="absolute -bottom-1 -right-1 w-3 h-3 bg-blue-400 border border-white cursor-se-resize rounded-full"
179+
onMouseDown={(e) => handleMouseDown(e, 'resize', 'se')}
180+
/>
181+
182+
{/* Center label */}
183+
{/* <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
184+
<div class="px-2 py-1 bg-blue-500 text-white text-xs rounded shadow-lg">
185+
<IconCapBlur class="inline w-3 h-3 mr-1" />
186+
Blur Area
187+
</div>
188+
</div> */}
189+
</Show>
190+
</div>
191+
);
192+
}

0 commit comments

Comments
 (0)