Skip to content

Commit 2b775b0

Browse files
committed
feat: Add border option to editor
1 parent 55edfb2 commit 2b775b0

File tree

10 files changed

+3027
-3134
lines changed

10 files changed

+3027
-3134
lines changed

apps/desktop/src/routes/editor/ConfigSidebar.tsx

Lines changed: 2336 additions & 2251 deletions
Large diffs are not rendered by default.

apps/desktop/src/utils/tauri.ts

Lines changed: 444 additions & 790 deletions
Large diffs are not rendered by default.

crates/project/src/configuration.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ pub struct ShadowConfiguration {
193193
pub blur: f32, // Shadow blur amount (0-100)
194194
}
195195

196+
#[derive(Type, Serialize, Deserialize, Clone, Debug, PartialEq)]
197+
#[serde(rename_all = "camelCase")]
198+
pub struct BorderConfiguration {
199+
pub enabled: bool,
200+
pub width: f32, // Border width in pixels
201+
pub color: Color, // Border color (RGB)
202+
pub opacity: f32, // Border opacity (0-100)
203+
}
204+
196205
#[derive(Type, Serialize, Deserialize, Clone, Debug)]
197206
#[serde(rename_all = "camelCase")]
198207
pub struct BackgroundConfiguration {
@@ -206,6 +215,19 @@ pub struct BackgroundConfiguration {
206215
pub shadow: f32,
207216
#[serde(default)]
208217
pub advanced_shadow: Option<ShadowConfiguration>,
218+
#[serde(default)]
219+
pub border: Option<BorderConfiguration>,
220+
}
221+
222+
impl Default for BorderConfiguration {
223+
fn default() -> Self {
224+
Self {
225+
enabled: false,
226+
width: 5.0,
227+
color: [255, 255, 255], // White
228+
opacity: 80.0, // 80% opacity
229+
}
230+
}
209231
}
210232

211233
impl Default for BackgroundConfiguration {
@@ -219,6 +241,7 @@ impl Default for BackgroundConfiguration {
219241
crop: None,
220242
shadow: 73.6,
221243
advanced_shadow: Some(ShadowConfiguration::default()),
244+
border: None, // Border is disabled by default for backwards compatibility
222245
}
223246
}
224247
}

crates/rendering-skia/src/layers/background.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@ impl From<BackgroundSource> for Background {
5858
pub struct BackgroundLayer {
5959
// Current background configuration
6060
current_background: Option<Background>,
61+
current_border: Option<cap_project::BorderConfiguration>,
6162

6263
// Track what we rendered last to detect changes
6364
last_rendered_background: Option<Background>,
65+
last_rendered_border: Option<cap_project::BorderConfiguration>,
6466
last_rendered_size: (u32, u32),
6567

6668
// For image backgrounds
@@ -72,7 +74,9 @@ impl BackgroundLayer {
7274
pub fn new() -> Self {
7375
Self {
7476
current_background: None,
77+
current_border: None,
7578
last_rendered_background: None,
79+
last_rendered_border: None,
7680
last_rendered_size: (0, 0),
7781
image_path: None,
7882
loaded_image: None,
@@ -102,6 +106,41 @@ impl BackgroundLayer {
102106
}
103107
}
104108

109+
fn render_border(
110+
&self,
111+
canvas: &Canvas,
112+
bounds: Rect,
113+
border: &cap_project::BorderConfiguration,
114+
) {
115+
if !border.enabled || border.width <= 0.0 {
116+
return;
117+
}
118+
119+
let mut paint = Paint::default();
120+
paint.set_style(skia_safe::PaintStyle::Stroke);
121+
paint.set_stroke_width(border.width);
122+
paint.set_anti_alias(true);
123+
124+
let alpha = ((border.opacity / 100.0).clamp(0.0, 1.0) * 255.0) as u8;
125+
let border_color = Color::from_argb(
126+
alpha,
127+
(border.color[0] >> 8) as u8,
128+
(border.color[1] >> 8) as u8,
129+
(border.color[2] >> 8) as u8,
130+
);
131+
paint.set_color(border_color);
132+
133+
let inset = border.width / 2.0;
134+
let border_rect = Rect::from_xywh(
135+
bounds.left() + inset,
136+
bounds.top() + inset,
137+
bounds.width() - border.width,
138+
bounds.height() - border.width,
139+
);
140+
141+
canvas.draw_rect(border_rect, &paint);
142+
}
143+
105144
fn render_color(&self, canvas: &Canvas, color: &[u16; 3], _bounds: Rect) {
106145
// Convert from u16 (0-65535) to u8 (0-255)
107146
let skia_color = Color::from_argb(
@@ -121,7 +160,6 @@ impl BackgroundLayer {
121160
angle: u16,
122161
bounds: Rect,
123162
) {
124-
// Convert colors from u16 (0-65535) to u8 (0-255)
125163
let start_color = Color::from_argb(
126164
255, // Full opacity
127165
(from[0] >> 8) as u8,
@@ -208,19 +246,29 @@ impl RecordableLayer for BackgroundLayer {
208246
let canvas = recorder.begin_recording(bounds, None);
209247
self.render_background(canvas, bounds);
210248

249+
// Render border if enabled
250+
if let Some(border) = &uniforms.border {
251+
if border.enabled {
252+
self.render_border(canvas, bounds, border);
253+
}
254+
}
255+
211256
// Update what was last rendered
212257
self.last_rendered_background = self.current_background.clone();
258+
self.last_rendered_border = self.current_border.clone();
213259
self.last_rendered_size = uniforms.output_size;
214260

215261
recorder.finish_recording_as_picture(None)
216262
}
217263

218264
fn needs_update(&self, uniforms: &SkiaProjectUniforms) -> bool {
219265
let new_background = Background::from(uniforms.background.clone());
266+
let new_border = uniforms.border.clone();
220267
let new_size = uniforms.output_size;
221268

222269
// Check against what was last rendered, not what's currently prepared
223270
self.last_rendered_background.as_ref() != Some(&new_background)
271+
|| self.last_rendered_border != new_border
224272
|| self.last_rendered_size != new_size
225273
}
226274

@@ -261,6 +309,7 @@ impl RecordableLayer for BackgroundLayer {
261309

262310
// Update current state (but not last_rendered, that happens in record())
263311
self.current_background = Some(new_background);
312+
self.current_border = frame_data.uniforms.border.clone();
264313

265314
Ok(())
266315
}

crates/rendering-skia/src/layers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub use background::BackgroundLayer;
1111
pub struct SkiaProjectUniforms {
1212
pub output_size: (u32, u32),
1313
pub background: cap_project::BackgroundSource,
14+
pub border: Option<cap_project::BorderConfiguration>,
1415
// Add more fields as needed
1516
}
1617

crates/rendering/src/composite_frame.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ pub struct CompositeVideoFrameUniforms {
2626
pub shadow_opacity: f32,
2727
pub shadow_blur: f32,
2828
pub opacity: f32,
29-
pub _padding: [f32; 3],
29+
pub border_enabled: f32,
30+
pub border_width: f32,
31+
pub _padding0: f32,
32+
pub _padding1: [f32; 2],
33+
pub _padding1b: [f32; 2],
34+
pub border_color: [f32; 4],
35+
pub _padding2: [f32; 4],
3036
}
3137

3238
impl Default for CompositeVideoFrameUniforms {
@@ -47,7 +53,13 @@ impl Default for CompositeVideoFrameUniforms {
4753
shadow_opacity: Default::default(),
4854
shadow_blur: Default::default(),
4955
opacity: 1.0,
50-
_padding: Default::default(),
56+
border_enabled: 0.0,
57+
border_width: 5.0,
58+
_padding0: 0.0,
59+
_padding1: [0.0; 2],
60+
_padding1b: [0.0; 2],
61+
border_color: [1.0, 1.0, 1.0, 0.8],
62+
_padding2: [0.0; 4],
5163
}
5264
}
5365
}

crates/rendering/src/layers/display.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@ impl DisplayLayer {
7575
},
7676
);
7777

78-
queue.write_buffer(&self.uniforms_buffer, 0, bytemuck::cast_slice(&[uniforms]));
78+
self.uniforms_buffer = uniforms.to_buffer(device);
79+
80+
self.bind_group = Some(self.pipeline.bind_group(
81+
device,
82+
&self.uniforms_buffer,
83+
&self.frame_texture_view,
84+
));
7985
}
8086

8187
pub fn render(&self, pass: &mut wgpu::RenderPass<'_>) {

crates/rendering/src/lib.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,38 @@ impl ProjectUniforms {
636636
.as_ref()
637637
.map_or(50.0, |s| s.blur),
638638
opacity: layout.screen_opacity as f32,
639-
_padding: [0.0; 3],
639+
border_enabled: if project
640+
.background
641+
.border
642+
.as_ref()
643+
.map_or(false, |b| b.enabled)
644+
{
645+
1.0
646+
} else {
647+
0.0
648+
},
649+
border_width: project.background.border.as_ref().map_or(5.0, |b| b.width),
650+
_padding0: 0.0,
651+
_padding1: [0.0; 2],
652+
_padding1b: [0.0; 2],
653+
border_color: {
654+
let border = project.background.border.as_ref().unwrap_or(
655+
&cap_project::BorderConfiguration {
656+
enabled: false,
657+
width: 5.0,
658+
color: [255, 255, 255],
659+
opacity: 80.0,
660+
},
661+
);
662+
663+
[
664+
border.color[0] as f32 / 255.0,
665+
border.color[1] as f32 / 255.0,
666+
border.color[2] as f32 / 255.0,
667+
(border.opacity / 100.0).clamp(0.0, 1.0),
668+
]
669+
},
670+
_padding2: [0.0; 4],
640671
}
641672
};
642673

@@ -745,7 +776,13 @@ impl ProjectUniforms {
745776
.as_ref()
746777
.map_or(50.0, |s| s.blur),
747778
opacity: layout.regular_camera_transition_opacity() as f32,
748-
_padding: [0.0; 3],
779+
border_enabled: 0.0,
780+
border_width: 0.0,
781+
_padding0: 0.0,
782+
_padding1: [0.0; 2],
783+
_padding1b: [0.0; 2],
784+
border_color: [0.0, 0.0, 0.0, 0.0],
785+
_padding2: [0.0; 4],
749786
}
750787
});
751788

@@ -808,7 +845,13 @@ impl ProjectUniforms {
808845
shadow_opacity: 0.0,
809846
shadow_blur: 0.0,
810847
opacity: layout.camera_only_transition_opacity() as f32,
811-
_padding: [0.0; 3],
848+
border_enabled: 0.0,
849+
border_width: 0.0,
850+
_padding0: 0.0,
851+
_padding1: [0.0; 2],
852+
_padding1b: [0.0; 2],
853+
border_color: [0.0, 0.0, 0.0, 0.0],
854+
_padding2: [0.0; 4],
812855
}
813856
});
814857

crates/rendering/src/shaders/composite-video-frame.wgsl

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
struct Uniforms {
2-
crop_bounds: vec4<f32>,
3-
target_bounds: vec4<f32>,
2+
crop_bounds: vec4<f32>,
3+
target_bounds: vec4<f32>,
44
output_size: vec2<f32>,
55
frame_size: vec2<f32>,
66
velocity_uv: vec2<f32>,
@@ -14,6 +14,11 @@ struct Uniforms {
1414
shadow_opacity: f32,
1515
shadow_blur: f32,
1616
opacity: f32,
17+
border_enabled: f32,
18+
border_width: f32,
19+
_padding1: vec2<f32>,
20+
border_color: vec4<f32>,
21+
_padding2: vec4<f32>,
1722
};
1823

1924
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -49,9 +54,9 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
4954
let p = frag_coord.xy;
5055
let center = (uniforms.target_bounds.xy + uniforms.target_bounds.zw) * 0.5;
5156
let size = (uniforms.target_bounds.zw - uniforms.target_bounds.xy) * 0.5;
57+
5258
let dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px);
5359

54-
// Advanced shadow calculation
5560
let min_frame_size = min(size.x, size.y);
5661
let shadow_enabled = uniforms.shadow > 0.0;
5762

@@ -77,7 +82,6 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
7782
shadow_enabled
7883
);
7984

80-
// Calculate shadow with no offset
8185
let shadow_dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px);
8286

8387
// Apply blur and size to shadow
@@ -90,7 +94,24 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
9094

9195
let bg_color = vec4<f32>(0.0);
9296

93-
// If outside the target area, just blend shadow with intermediate
97+
if (uniforms.border_enabled > 0.0) {
98+
let border_outer_dist = sdf_rounded_rect(
99+
p - center,
100+
size + vec2<f32>(uniforms.border_width),
101+
uniforms.rounding_px + uniforms.border_width
102+
);
103+
let border_inner_dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px);
104+
105+
if (border_outer_dist <= 0.0 && border_inner_dist > 0.0) {
106+
let inner_alpha = smoothstep(-0.5, 0.5, border_inner_dist);
107+
let outer_alpha = 1.0 - smoothstep(-0.5, 0.5, border_outer_dist);
108+
let edge_alpha = inner_alpha * outer_alpha;
109+
110+
let border_alpha = edge_alpha * uniforms.border_color.w;
111+
return vec4<f32>(uniforms.border_color.xyz, border_alpha);
112+
}
113+
}
114+
94115
if target_uv.x < 0.0 || target_uv.x > 1.0 || target_uv.y < 0.0 || target_uv.y > 1.0 {
95116
return shadow_color;
96117
}
@@ -102,7 +123,6 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
102123
let blur_amount = select(uniforms.motion_blur_amount, uniforms.camera_motion_blur_amount, uniforms.camera_motion_blur_amount > 0.0);
103124

104125
if blur_amount < 0.01 {
105-
// First blend shadow with intermediate, then blend result with base color
106126
return mix(shadow_color, base_color, base_color.a);
107127
}
108128

0 commit comments

Comments
 (0)