Skip to content
Draft
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
30 changes: 30 additions & 0 deletions apps/desktop/src/routes/editor/ConfigSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,36 @@ export function ConfigSidebar() {
step={1}
/>
</Field>
<Field name="Opacity" icon={<IconLucideDroplet class="size-4" />}>
<Slider
value={[project.cursor.cursorOpacity ?? 1]}
onChange={(v) => setProject("cursor", "cursorOpacity", v[0])}
minValue={0}
maxValue={2}
step={0.01}
formatTooltip={(value) => `${Math.round(value * 100)}%`}
/>
</Field>
<Show when={project.cursor.type === "circle"}>
<Field
name="Circle Color"
icon={<IconLucidePalette class="size-4" />}
value={
<input
type="color"
value={project.cursor.circleColor ?? "#FFFFFF"}
onInput={(e) =>
setProject(
"cursor",
"circleColor",
e.currentTarget.value.toUpperCase(),
)
}
class="h-7 w-10 cursor-pointer rounded border border-gray-3 bg-transparent"
/>
}
/>
</Show>
<Field name="Tilt" icon={<IconLucideRotate3d class="size-4" />}>
<Slider
value={[project.cursor.rotationAmount ?? 0.15]}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export type CurrentRecording = { target: CurrentRecordingTarget; mode: Recording
export type CurrentRecordingChanged = null
export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds | null } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } | "camera"
export type CursorAnimationStyle = "slow" | "smooth" | "mellow" | "fast" | "custom"
export type CursorConfiguration = { hide: boolean; hideWhenIdle: boolean; hideWhenIdleDelay: number; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw: boolean; motionBlur: number; useSvg: boolean; rotationAmount?: number; baseRotation?: number; clickSpring?: ClickSpringConfig | null; stopMovementInLastSeconds?: number | null }
export type CursorConfiguration = { hide: boolean; hideWhenIdle: boolean; hideWhenIdleDelay: number; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw: boolean; motionBlur: number; useSvg: boolean; rotationAmount?: number; baseRotation?: number; cursorOpacity?: number; circleColor?: string; clickSpring?: ClickSpringConfig | null; stopMovementInLastSeconds?: number | null }
export type CursorMeta = { imagePath: string; hotspot: XY<number>; shape?: string | null }
export type CursorType = "auto" | "pointer" | "circle"
export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta }
Expand Down
14 changes: 14 additions & 0 deletions crates/project/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ pub struct CursorConfiguration {
pub rotation_amount: f32,
#[serde(default)]
pub base_rotation: f32,
#[serde(default = "CursorConfiguration::default_cursor_opacity")]
pub cursor_opacity: f32,
#[serde(default = "CursorConfiguration::default_circle_color")]
pub circle_color: String,
#[serde(default)]
pub click_spring: Option<ClickSpringConfig>,
#[serde(default)]
Expand All @@ -568,6 +572,8 @@ impl Default for CursorConfiguration {
use_svg: true,
rotation_amount: Self::default_rotation_amount(),
base_rotation: 0.0,
cursor_opacity: Self::default_cursor_opacity(),
circle_color: Self::default_circle_color(),
click_spring: None,
stop_movement_in_last_seconds: None,
};
Expand All @@ -590,6 +596,14 @@ impl CursorConfiguration {
0.15
}

fn default_cursor_opacity() -> f32 {
1.0
}

fn default_circle_color() -> String {
"#FFFFFF".to_string()
}

pub fn cursor_type(&self) -> &CursorType {
&self.r#type
}
Expand Down
40 changes: 31 additions & 9 deletions crates/rendering/src/layers/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct CursorLayer {
circle_cursor: Option<CursorTexture>,
prev_is_svg_assets_enabled: Option<bool>,
prev_cursor_type: Option<CursorType>,
prev_circle_color: Option<String>,
}

struct Statics {
Expand Down Expand Up @@ -185,6 +186,18 @@ impl Statics {
}
}

fn parse_hex_color(hex: &str) -> [f32; 3] {
let stripped = hex.strip_prefix('#').unwrap_or(hex);
if stripped.len() != 6 {
return [1.0, 1.0, 1.0];
}
let parse = |i: usize| u8::from_str_radix(&stripped[i..i + 2], 16);
match (parse(0), parse(2), parse(4)) {
(Ok(r), Ok(g), Ok(b)) => [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0],
_ => [1.0, 1.0, 1.0],
}
}

impl CursorLayer {
pub fn new(device: &wgpu::Device) -> Self {
let statics = Statics::new(device);
Expand All @@ -196,10 +209,11 @@ impl CursorLayer {
circle_cursor: None,
prev_is_svg_assets_enabled: None,
prev_cursor_type: None,
prev_circle_color: None,
}
}

fn create_circle_cursor(constants: &RenderVideoConstants) -> CursorTexture {
fn create_circle_cursor(constants: &RenderVideoConstants, color: [f32; 3]) -> CursorTexture {
let size = CIRCLE_CURSOR_SIZE;
let mut rgba = vec![0u8; (size * size * 4) as usize];
let center = size as f32 / 2.0;
Expand All @@ -210,6 +224,8 @@ impl CursorLayer {
let fill_alpha = 0.2_f32;
let border_alpha = 0.55_f32;

let [r, g, b] = color;

for y in 0..size {
for x in 0..size {
let dx = x as f32 - center + 0.5;
Expand All @@ -230,11 +246,10 @@ impl CursorLayer {
let base_alpha = fill_alpha + border_factor * (border_alpha - fill_alpha);
let alpha = base_alpha * outer_fade;

let premul = (255.0 * alpha) as u8;
rgba[idx] = premul;
rgba[idx + 1] = premul;
rgba[idx + 2] = premul;
rgba[idx + 3] = premul;
rgba[idx] = ((r * 255.0) * alpha) as u8;
rgba[idx + 1] = ((g * 255.0) * alpha) as u8;
rgba[idx + 2] = ((b * 255.0) * alpha) as u8;
rgba[idx + 3] = (255.0 * alpha) as u8;
}
}
}
Expand Down Expand Up @@ -323,15 +338,15 @@ impl CursorLayer {
XY::new(0.0, 0.0)
};

let mut cursor_opacity = 1.0f32;
let mut cursor_opacity = uniforms.project.cursor.cursor_opacity.max(0.0);
if uniforms.project.cursor.hide_when_idle && !cursor.moves.is_empty() {
let hide_delay_secs = uniforms
.project
.cursor
.hide_when_idle_delay
.max((CURSOR_IDLE_MIN_DELAY_MS / 1000.0) as f32);
let hide_delay_ms = (hide_delay_secs as f64 * 1000.0).max(CURSOR_IDLE_MIN_DELAY_MS);
cursor_opacity = compute_cursor_idle_opacity(
cursor_opacity *= compute_cursor_idle_opacity(
cursor,
segment_frames.recording_time as f64 * 1000.0,
hide_delay_ms,
Expand All @@ -348,14 +363,21 @@ impl CursorLayer {
self.circle_cursor = None;
}

let circle_color_str = uniforms.project.cursor.circle_color.clone();
if self.prev_circle_color.as_deref() != Some(circle_color_str.as_str()) {
self.prev_circle_color = Some(circle_color_str.clone());
self.circle_cursor = None;
}

if self.prev_is_svg_assets_enabled != Some(uniforms.project.cursor.use_svg) {
self.prev_is_svg_assets_enabled = Some(uniforms.project.cursor.use_svg);
self.cursors.drain();
}

let cursor_texture = if cursor_type == CursorType::Circle {
if self.circle_cursor.is_none() {
self.circle_cursor = Some(Self::create_circle_cursor(constants));
let color = parse_hex_color(&circle_color_str);
self.circle_cursor = Some(Self::create_circle_cursor(constants, color));
}
self.circle_cursor.as_ref().unwrap()
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/ui-solid/src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ declare global {
const IconLucideCircleOff: typeof import('~icons/lucide/circle-off.jsx')['default']
const IconLucideClapperboard: typeof import('~icons/lucide/clapperboard.jsx')['default']
const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default']
const IconLucideDroplet: typeof import('~icons/lucide/droplet.jsx')['default']
const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default']
const IconLucideEyeOff: typeof import('~icons/lucide/eye-off.jsx')['default']
const IconLucideFastForward: typeof import('~icons/lucide/fast-forward.jsx')['default']
Expand Down