Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 9 additions & 55 deletions apps/desktop/src/routes/editor/ConfigSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,61 +482,6 @@ export function ConfigSidebar() {
</KCollapsible.Content>
</KCollapsible>
</Show>

{/* <Field name="Motion Blur">
<Slider
value={[project.cursor.motionBlur]}
onChange={(v) => setProject("cursor", "motionBlur", v[0])}
minValue={0}
maxValue={1}
step={0.001}
/>
</Field> */}
{/* <Field name="Animation Style" icon={<IconLucideRabbit />}>
<RadioGroup
defaultValue="regular"
value={project.cursor.animationStyle}
onChange={(value) => {
setProject(
"cursor",
"animationStyle",
value as CursorAnimationStyle
);
}}
class="flex flex-col gap-2"
disabled
>
{(
Object.entries(CURSOR_ANIMATION_STYLES) as [
CursorAnimationStyle,
string
][]
).map(([value, label]) => (
<RadioGroup.Item value={value} class="flex items-center">
<RadioGroup.ItemInput class="sr-only peer" />
<RadioGroup.ItemControl
class={cx(
"mr-2 w-4 h-4 rounded-full border border-gray-300",
"relative after:absolute after:inset-0 after:m-auto after:block after:w-2 after:h-2 after:rounded-full",
"after:transition-colors after:duration-200",
"peer-checked:border-blue-500 peer-checked:after:bg-blue-400",
"peer-focus-visible:ring-2 peer-focus-visible:ring-blue-400/50",
"peer-disabled:opacity-50"
)}
/>
<span
class={cx(
"text-gray-12",
"peer-checked:text-gray-900",
"peer-disabled:opacity-50"
)}
>
{label}
</span>
</RadioGroup.Item>
))}
</RadioGroup>
</Field> */}
</KTabs.Content>
<KTabs.Content value="hotkeys">
<Field name="Hotkeys" icon={<IconCapHotkeys />}>
Expand Down Expand Up @@ -1389,6 +1334,15 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) {
formatTooltip="%"
/>
</Field>
<Field name="Motion Blur" icon={<IconCapBgBlur class="size-4" />}>
<Slider
value={[project.background.motionBlur ?? 0.5]}
onChange={(v) => setProject("background", "motionBlur", v[0])}
minValue={0}
maxValue={1}
step={0.001}
/>
</Field>
<Field name="Shadow" icon={<IconCapShadow class="size-4" />}>
<Slider
value={[project.background.shadow!]}
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export type AudioMeta = { path: string;
start_time?: number | null }
export type AuthStore = { token: string; user_id: string | null; expires: number; 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 BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null; motionBlur?: number }
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 }
Expand All @@ -249,7 +249,7 @@ export type CurrentRecording = { target: CurrentRecordingTarget; type: Recording
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 }
export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean }
export type CursorMeta = { imagePath: string; hotspot: XY<number> }
export type CursorType = "pointer" | "circle"
export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta }
Expand Down
6 changes: 3 additions & 3 deletions crates/project/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@
pub shadow: f32,
#[serde(default)]
pub advanced_shadow: Option<ShadowConfiguration>,
#[serde(default)]
pub motion_blur: f32,
}

impl Default for BackgroundConfiguration {
Expand All @@ -194,6 +196,7 @@
crop: None,
shadow: 73.6,
advanced_shadow: Some(ShadowConfiguration::default()),
motion_blur: 0.5,
}
}
}
Expand Down Expand Up @@ -341,8 +344,6 @@
pub friction: f32,
#[serde(default = "CursorConfiguration::default_raw")]
pub raw: bool,
#[serde(default)]
pub motion_blur: f32,
}

impl Default for CursorConfiguration {
Expand All @@ -357,7 +358,6 @@
mass: 1.0,
friction: 20.0,
raw: false,
motion_blur: 0.5,
}
}
}
Expand Down Expand Up @@ -461,7 +461,7 @@
pub fn load(project_path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
let config_str =
std::fs::read_to_string(project_path.as_ref().join("project-config.json"))?;
let mut config: Self = serde_json::from_str(&config_str).unwrap_or_default();

Check warning on line 464 in crates/project/src/configuration.rs

View workflow job for this annotation

GitHub Actions / Clippy

variable does not need to be mutable

warning: variable does not need to be mutable --> crates/project/src/configuration.rs:464:13 | 464 | let mut config: Self = serde_json::from_str(&config_str).unwrap_or_default(); | ----^^^^^^ | | | help: remove this `mut` | = note: `#[warn(unused_mut)]` on by default

Ok(config)
}
Expand All @@ -476,24 +476,24 @@
pub fn get_segment_time(&self, frame_time: f64) -> Option<(f64, u32)> {
self.timeline
.as_ref()
.map(|t| t.get_segment_time(frame_time as f64))

Check warning on line 479 in crates/project/src/configuration.rs

View workflow job for this annotation

GitHub Actions / Clippy

casting to the same type is unnecessary (`f64` -> `f64`)

warning: casting to the same type is unnecessary (`f64` -> `f64`) --> crates/project/src/configuration.rs:479:41 | 479 | .map(|t| t.get_segment_time(frame_time as f64)) | ^^^^^^^^^^^^^^^^^ help: try: `frame_time` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast = note: `#[warn(clippy::unnecessary_cast)]` on by default
.unwrap_or(Some((frame_time as f64, 0)))

Check warning on line 480 in crates/project/src/configuration.rs

View workflow job for this annotation

GitHub Actions / Clippy

casting to the same type is unnecessary (`f64` -> `f64`)

warning: casting to the same type is unnecessary (`f64` -> `f64`) --> crates/project/src/configuration.rs:480:30 | 480 | .unwrap_or(Some((frame_time as f64, 0))) | ^^^^^^^^^^^^^^^^^ help: try: `frame_time` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast
}
}

impl Default for ProjectConfiguration {
fn default() -> Self {
ProjectConfiguration {
aspect_ratio: None,
background: BackgroundConfiguration::default(),
camera: Camera::default(),
audio: AudioConfiguration::default(),
cursor: CursorConfiguration::default(),
hotkeys: HotkeysConfiguration::default(),
timeline: None,
}
}
}

Check warning on line 496 in crates/project/src/configuration.rs

View workflow job for this annotation

GitHub Actions / Clippy

this `impl` can be derived

warning: this `impl` can be derived --> crates/project/src/configuration.rs:484:1 | 484 | / impl Default for ProjectConfiguration { 485 | | fn default() -> Self { 486 | | ProjectConfiguration { 487 | | aspect_ratio: None, ... | 496 | | } | |_^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#derivable_impls = note: `#[warn(clippy::derivable_impls)]` on by default help: replace the manual implementation with a derive attribute | 449 + #[derive(Default)] 450 ~ pub struct ProjectConfiguration { |

pub const SLOW_SMOOTHING_SAMPLES: usize = 24;
pub const REGULAR_SMOOTHING_SAMPLES: usize = 16;
Expand Down
20 changes: 14 additions & 6 deletions crates/rendering/src/layers/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,22 @@ impl CursorLayer {
return;
};

let velocity: [f32; 2] = [0.0, 0.0];
// let velocity: [f32; 2] = [
// interpolated_cursor.velocity.x * 75.0,
// interpolated_cursor.velocity.y * 75.0,
// ];
// Calculate cursor velocity in pixels/second
let velocity: [f32; 2] = [
interpolated_cursor.velocity.x * 75.0,
interpolated_cursor.velocity.y * 75.0,
];

let speed = (velocity[0] * velocity[0] + velocity[1] * velocity[1]).sqrt();
let motion_blur_amount = (speed * 0.3).min(1.0) * 0.0; // uniforms.project.cursor.motion_blur;

fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}

let base_amount = uniforms.project.background.motion_blur;
let speed_factor = smoothstep(0.0, 300.0, speed);
let motion_blur_amount = base_amount * speed_factor;

let Some(cursor_texture) = constants
.cursor_textures
Expand Down
44 changes: 38 additions & 6 deletions crates/rendering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ impl ProjectUniforms {

let velocity = [0.0, 0.0];

let motion_blur_amount = 0.0;
// Base motion blur amount set by project settings
let motion_blur_amount = project.background.motion_blur;

let crop = Self::get_crop(options, project);

Expand Down Expand Up @@ -731,6 +732,38 @@ impl ProjectUniforms {
.unwrap_or_else(|| Coord::new(XY::new(0.5, 0.5))),
);

// Previous frame zoom state for motion blur calculation
let prev_zoom = InterpolatedZoom::new(
SegmentsCursor::new(
(frame_time - 1.0 / fps as f32) as f64,
project
.timeline
.as_ref()
.map(|t| t.zoom_segments.as_slice())
.unwrap_or(&[]),
),
interpolate_cursor(
cursor_events,
(segment_frames.recording_time - 0.2 - 1.0 / fps as f32).max(0.0),
(!project.cursor.raw).then(|| SpringMassDamperSimulationConfig {
tension: project.cursor.tension,
mass: project.cursor.mass,
friction: project.cursor.friction,
}),
)
.as_ref()
.map(|i| i.position)
.unwrap_or_else(|| Coord::new(XY::new(0.5, 0.5))),
);

let zoom_progress = zoom.t as f32; // 0 to 1 across zoom
let blur_factor = (5.0 * zoom_progress * (1.0 - zoom_progress)).clamp(0.0, 1.0);

let screen_blur_scale = 0.3;

let motion_blur_amount_for_display = motion_blur_amount * blur_factor * screen_blur_scale;
let camera_motion_blur = motion_blur_amount * blur_factor * screen_blur_scale;

let display = {
let output_size = XY::new(output_size.0 as f64, output_size.1 as f64);
let size = [options.screen_size.x as f32, options.screen_size.y as f32];
Expand Down Expand Up @@ -774,8 +807,8 @@ impl ProjectUniforms {
rounding_px: (project.background.rounding / 100.0 * 0.5 * min_target_axis) as f32,
mirror_x: 0.0,
velocity_uv: velocity,
motion_blur_amount,
camera_motion_blur_amount: 0.0,
motion_blur_amount: motion_blur_amount_for_display,
camera_motion_blur_amount: camera_motion_blur,
shadow: project.background.shadow,
shadow_size: project
.background
Expand Down Expand Up @@ -841,8 +874,7 @@ impl ProjectUniforms {
position[1] + size[1],
];

// Calculate camera motion blur based on zoom transition
let camera_motion_blur = 0.0;
// Camera motion blur based on zoom transition

CompositeVideoFrameUniforms {
output_size,
Expand All @@ -861,7 +893,7 @@ impl ProjectUniforms {
rounding_px: project.camera.rounding / 100.0 * 0.5 * size[0],
mirror_x: if project.camera.mirror { 1.0 } else { 0.0 },
velocity_uv: [0.0, 0.0],
motion_blur_amount,
motion_blur_amount: motion_blur_amount_for_display,
camera_motion_blur_amount: camera_motion_blur,
shadow: project.camera.shadow,
shadow_size: project
Expand Down
15 changes: 10 additions & 5 deletions crates/rendering/src/shaders/composite-video-frame.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,22 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
var base_color = sample_texture(target_uv, crop_bounds_uv);
base_color = apply_rounded_corners(base_color, target_uv);

// Use either the screen or camera blur value directly without the additional checks
// that were preventing blur from being applied correctly
let blur_amount = select(uniforms.motion_blur_amount, uniforms.camera_motion_blur_amount, uniforms.camera_motion_blur_amount > 0.0);

if blur_amount < 0.01 {
// Skip blur processing if the blur amount is minimal
if blur_amount <= 0.0 {
// First blend shadow with intermediate, then blend result with base color
return mix(shadow_color, base_color, base_color.a);
}

let center_uv = vec2<f32>(0.5, 0.5);
let dir = normalize(target_uv - center_uv);

let base_samples = 16.0;
let num_samples = i32(base_samples * smoothstep(0.0, 1.0, blur_amount));
// Responsive sample count that scales with blur amount
let base_samples = 8.0 + 12.0 * blur_amount; // 8-20 samples depending on blur amount
let num_samples = i32(base_samples);

var accum = base_color;
var weight_sum = 1.0;
Expand All @@ -119,9 +123,10 @@ fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {

let random_offset = (rand(target_uv + vec2<f32>(t)) - 0.5) * 0.1 * smoothstep(0.0, 0.2, blur_amount);

// Apply a reduced blur scale to make it less intense overall
let base_scale = select(
0.08, // Regular content scale
0.16, // Camera scale
0.06, // Regular content scale (reduced from 0.08)
0.12, // Camera scale (reduced from 0.16)
uniforms.camera_motion_blur_amount > 0.0
);
let scale = dist_from_center * blur_amount * (base_scale + random_offset) * smoothstep(0.0, 0.1, blur_amount);
Expand Down
59 changes: 39 additions & 20 deletions crates/rendering/src/shaders/cursor.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -55,49 +55,68 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {

@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// Base texture lookup for static case
let base_texture = textureSample(t_cursor, s_cursor, input.uv);

// Calculate velocity magnitude for adaptive blur strength
let velocity_mag = length(uniforms.velocity);

// Apply blur even at very low motion_blur_amount values, but skip if truly zero
if (uniforms.motion_blur_amount <= 0.0) {
return base_texture;
}

// Increase samples for higher quality blur
let num_samples = 20;
let num_samples = 32; // Increased for smoother blur
var color_sum = vec4<f32>(0.0);
var weight_sum = 0.0;

// Calculate velocity magnitude for adaptive blur strength
let velocity_mag = length(uniforms.velocity);
let adaptive_blur = uniforms.motion_blur_amount * smoothstep(0.0, 50.0, velocity_mag);
// Make blur responsive to even small velocity values
// Scale blur amount by velocity but ensure even slow movements have some blur
let min_blur = uniforms.motion_blur_amount * 0.05; // Minimum blur amount
let velocity_factor = smoothstep(0.0, 40.0, velocity_mag); // Wider range for smoother transitions
let adaptive_blur = min_blur + (uniforms.motion_blur_amount * velocity_factor * 0.95);

// Calculate blur direction from velocity
var blur_dir = uniforms.velocity;
// Use actual velocity direction but normalize magnitude for consistent behavior
var blur_dir = normalize(uniforms.velocity) * min(velocity_mag, 60.0);

// Enhanced blur trail
let max_blur_offset = 3.0 * adaptive_blur;
// Enhanced blur trail with smoother gradient
let max_blur_offset = 8.0 * adaptive_blur;

for (var i = 0; i < num_samples; i++) {
// Non-linear sampling for better blur distribution
let t = i / num_samples;
// Use a more gradual power curve for smoother transitions
let t = pow(f32(i) / f32(num_samples), 1.0);

// Calculate sample offset with velocity-based scaling
let offset = blur_dir * max_blur_offset * (f32(i) / f32(num_samples));
let sample_uv = input.uv + offset / uniforms.output_size.xy;
let offset = blur_dir * max_blur_offset * t;
let sample_uv = input.uv - offset / uniforms.output_size.xy;

// Sample with bilinear filtering
let sample = textureSample(t_cursor, s_cursor, sample_uv);

// Accumulate weighted sample
color_sum += sample;
// Apply weight based on sample position in the trail
// Use a more gradual falloff for smoother blending
let weight = 1.0 - (0.5 * t);
color_sum += sample * weight;
weight_sum += weight;
}

// Normalize the result
var final_color = color_sum / f32(num_samples);
var final_color = color_sum / weight_sum;

// Enhance contrast slightly for fast movements
if (velocity_mag > 30.0) {
// Create new color with enhanced contrast instead of modifying components
// Enhance contrast slightly for fast movements with lower threshold
if (velocity_mag > 10.0) {
// Create new color with enhanced contrast
final_color = vec4<f32>(
pow(final_color.r, 0.95),
pow(final_color.g, 0.95),
pow(final_color.b, 0.95),
pow(final_color.r, 0.9),
pow(final_color.g, 0.9),
pow(final_color.b, 0.9),
final_color.a
);
}

return final_color * vec4<f32>(1.0, 1.0, 1.0, 1.0 - uniforms.motion_blur_amount * 0.2);
// Less opacity reduction to maintain cursor visibility
return final_color * vec4<f32>(1.0, 1.0, 1.0, 1.0 - uniforms.motion_blur_amount * 0.05);
}
Loading