Skip to content

Commit 0301372

Browse files
committed
[*] refactor
1 parent 6680478 commit 0301372

File tree

12 files changed

+347
-51
lines changed

12 files changed

+347
-51
lines changed

lib/video-editor/src/filters/video.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub mod zoom;
1414

1515
pub use border::BorderFilter;
1616
pub use chroma::ChromaKeyFilter;
17-
pub use crop::CropFilter;
17+
pub use crop::{CropFilter, CropShape};
1818
pub use fade_in::FadeInFilter;
1919
pub use fade_out::FadeOutFilter;
2020
pub use flip::{FlipDirection, FlipFilter};

lib/video-editor/src/filters/video/crop.rs

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,35 @@ use crate::{
44
tracks::video_frame_cache::VideoImage,
55
};
66
use image::{RgbaImage, imageops};
7+
use rayon::prelude::*;
78
use video_utils::convert::resize_rgba_image_contain;
89

10+
#[derive(
11+
Debug,
12+
Clone,
13+
Copy,
14+
PartialEq,
15+
Eq,
16+
Default,
17+
serde::Serialize,
18+
serde::Deserialize,
19+
num_enum::TryFromPrimitive,
20+
num_enum::IntoPrimitive,
21+
)]
22+
#[repr(u8)]
23+
pub enum CropShape {
24+
#[default]
25+
Rectangle = 0,
26+
Circle = 1,
27+
}
28+
929
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1030
pub struct CropFilter {
1131
pub left: f32, // 左边缘偏移(0, 1)
1232
pub top: f32, // 上边缘偏移(0, 1)
1333
pub width: f32, // 裁剪宽度(0, 1)
1434
pub height: f32, // 裁剪高度(0, 1)
35+
pub shape: CropShape,
1536
}
1637

1738
impl Default for CropFilter {
@@ -21,19 +42,21 @@ impl Default for CropFilter {
2142
top: 0.,
2243
width: 1.0,
2344
height: 1.0,
45+
shape: CropShape::default(),
2446
}
2547
}
2648
}
2749

2850
impl CropFilter {
2951
pub const NAME: &'static str = "crop";
3052

31-
pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
53+
pub fn new(left: f32, top: f32, width: f32, height: f32, shape: CropShape) -> Self {
3254
Self {
3355
left,
3456
top,
3557
width,
3658
height,
59+
shape,
3760
}
3861
}
3962

@@ -62,11 +85,109 @@ impl CropFilter {
6285
return Ok(());
6386
}
6487

88+
match self.shape {
89+
CropShape::Rectangle => {
90+
let cropped = imageops::crop(image, px_left, px_top, px_width, px_height);
91+
let cropped_image = cropped.to_image();
92+
*image =
93+
resize_rgba_image_contain(cropped_image, target_width, target_height, false)?;
94+
}
95+
CropShape::Circle => {
96+
self.apply_circular_crop(
97+
image,
98+
px_left,
99+
px_top,
100+
px_width,
101+
px_height,
102+
target_width,
103+
target_height,
104+
)?;
105+
}
106+
}
107+
108+
Ok(())
109+
}
110+
111+
/// Apply circular crop with anti-aliasing for smooth edges.
112+
/// The circle/ellipse is centered within the crop bounds and scaled to fill the area.
113+
fn apply_circular_crop(
114+
&self,
115+
image: &mut RgbaImage,
116+
px_left: u32,
117+
px_top: u32,
118+
px_width: u32,
119+
px_height: u32,
120+
target_width: u32,
121+
target_height: u32,
122+
) -> Result<()> {
123+
// First, extract the rectangular crop region
65124
let cropped = imageops::crop(image, px_left, px_top, px_width, px_height);
66125
let cropped_image = cropped.to_image();
67126

68-
*image = resize_rgba_image_contain(cropped_image, target_width, target_height, false)?;
127+
// Create output image at target size
128+
let mut result = RgbaImage::new(target_width, target_height);
129+
130+
// Calculate center and radii for the ellipse scaled to target dimensions
131+
// Use the smaller dimension to create a proper circle if width/height are similar
132+
// Otherwise create an ellipse that fills the crop area
133+
let center_x = target_width as f32 / 2.0;
134+
let center_y = target_height as f32 / 2.0;
135+
let radius_x = target_width as f32 / 2.0;
136+
let radius_y = target_height as f32 / 2.0;
137+
138+
// Anti-aliasing edge distance (in pixels)
139+
let aa_edge_distance = 1.0;
140+
141+
// Sample from cropped image and apply elliptical mask
142+
result
143+
.par_enumerate_pixels_mut()
144+
.for_each(|(px, py, pixel)| {
145+
// Calculate normalized position in target image (0-1)
146+
let norm_x = px as f32 / target_width as f32;
147+
let norm_y = py as f32 / target_height as f32;
148+
149+
// Map to cropped image coordinates
150+
let src_x = (norm_x * px_width as f32).round() as u32;
151+
let src_y = (norm_y * px_height as f32).round() as u32;
152+
153+
// Clamp to valid range
154+
let src_x = src_x.min(px_width.saturating_sub(1));
155+
let src_y = src_y.min(px_height.saturating_sub(1));
156+
157+
// Get source pixel
158+
let src_pixel = cropped_image.get_pixel(src_x, src_y);
159+
160+
// Check if inside the ellipse using parametric equation
161+
// (x-cx)^2/rx^2 + (y-cy)^2/ry^2 <= 1 means inside
162+
let dx = px as f32 - center_x;
163+
let dy = py as f32 - center_y;
164+
let ellipse_dist =
165+
(dx * dx) / (radius_x * radius_x) + (dy * dy) / (radius_y * radius_y);
166+
167+
// Calculate alpha based on distance from edge
168+
// ellipse_dist < 1 means inside, > 1 means outside
169+
if ellipse_dist <= 1.0 - aa_edge_distance / radius_x.min(radius_y) {
170+
// Fully inside the ellipse
171+
*pixel = *src_pixel;
172+
} else if ellipse_dist <= 1.0 + aa_edge_distance / radius_x.min(radius_y) {
173+
// Anti-aliasing zone near the edge
174+
let edge_threshold = 1.0;
175+
let aa_factor = (edge_threshold
176+
- (ellipse_dist - 1.0) * radius_x.min(radius_y) / aa_edge_distance)
177+
.clamp(0.0, 1.0);
178+
179+
// Blend with transparency
180+
pixel[0] = src_pixel[0];
181+
pixel[1] = src_pixel[1];
182+
pixel[2] = src_pixel[2];
183+
pixel[3] = ((src_pixel[3] as f32 * aa_factor) as u8).min(src_pixel[3]);
184+
} else {
185+
// Outside the ellipse - transparent
186+
pixel[3] = 0;
187+
}
188+
});
69189

190+
*image = result;
70191
Ok(())
71192
}
72193
}

lib/video-editor/src/project/project.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ pub struct ProjectPreviewConfig {
459459
#[derivative(Default(value = "25.0"))]
460460
pub fps: f32,
461461

462+
#[derivative(Default(value = "Resolution::P720"))]
462463
pub resolution: Resolution,
463464

464465
#[derivative(Default(value = "2"))]

wayshot/src/logic/video_editor/filters/conversion.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use video_editor::filters::{
3434
},
3535
traits::EasingFunction,
3636
video::{
37-
BorderFilter, ChromaKeyFilter, CropFilter, FadeInFilter as VideoFadeInFilter,
37+
BorderFilter, ChromaKeyFilter, CropFilter, CropShape, FadeInFilter as VideoFadeInFilter,
3838
FadeOutFilter as VideoFadeOutFilter, FlipDirection, FlipFilter, FlyInFilter, MosaicFilter,
3939
OpacityFilter, SlideDirection, SlideFilter, TransformFilter, WipeDirection, WipeFilter,
4040
ZoomFilter,
@@ -45,11 +45,13 @@ use video_editor::filters::{
4545

4646
impl From<CropFilter> for UICropDetail {
4747
fn from(f: CropFilter) -> Self {
48+
let shape: u8 = f.shape.into();
4849
Self {
4950
left: f.left,
5051
top: f.top,
5152
width: f.width,
5253
height: f.height,
54+
shape: shape as i32,
5355
}
5456
}
5557
}
@@ -118,11 +120,13 @@ impl From<UITransformDetail> for TransformFilter {
118120

119121
impl From<UICropDetail> for CropFilter {
120122
fn from(d: UICropDetail) -> Self {
123+
let shape = CropShape::try_from(d.shape as u8).unwrap_or_default();
121124
Self {
122125
left: d.left,
123126
top: d.top,
124127
width: d.width,
125128
height: d.height,
129+
shape,
126130
}
127131
}
128132
}

wayshot/ui/base/switch-btn.slint

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export component SwitchBtn inherits Rectangle {
1010
in-out property <length> indicator-size: Theme.icon-size;
1111
in-out property <length> hpadding: Theme.padding;
1212
in-out property <length> vpadding: Theme.padding;
13+
in-out property <bool> gain-focus-when-clicked: true;
1314

1415
width: indicator.width * 2 + hpadding * 2;
1516
height: indicator.height + vpadding * 2;
@@ -39,7 +40,9 @@ export component SwitchBtn inherits Rectangle {
3940
mouse-cursor: MouseCursor.pointer;
4041

4142
clicked => {
42-
fs.focus();
43+
if (gain-focus-when-clicked) {
44+
fs.focus();
45+
}
4346
root.checked = !root.checked;
4447
root.toggled();
4548
}

wayshot/ui/logic.slint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export global Logic {
390390
callback video-editor-add-subtitle(subtitle: VideoEditorSubtitle);
391391
callback video-editor-update-subtitle(subtitle: VideoEditorSubtitle);
392392
callback video-editor-update-edited-subtitle-from-segment(track-index: int, segment-index: int);
393+
callback video-editor-transcribe-audio();
393394

394395
callback video-editor-start-recording-audio();
395396
callback video-editor-stop-recording-audio();

wayshot/ui/panel/desktop/video-editor/filter.slint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export struct CropDetail {
3030
top: float,
3131
width: float,
3232
height: float,
33+
shape: int, // 0=Rectangle, 1=Circle
3334
}
3435

3536
export struct FlipDetail {

0 commit comments

Comments
 (0)