diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx
index 4344bc09c7..c95d9b6bdc 100644
--- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx
+++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx
@@ -651,6 +651,36 @@ export function ConfigSidebar() {
step={1}
/>
+ }>
+ setProject("cursor", "cursorOpacity", v[0])}
+ minValue={0}
+ maxValue={2}
+ step={0.01}
+ formatTooltip={(value) => `${Math.round(value * 100)}%`}
+ />
+
+
+ }
+ value={
+
+ setProject(
+ "cursor",
+ "circleColor",
+ e.currentTarget.value.toUpperCase(),
+ )
+ }
+ class="h-7 w-10 cursor-pointer rounded border border-gray-3 bg-transparent"
+ />
+ }
+ />
+
}>
; shape?: string | null }
export type CursorType = "auto" | "pointer" | "circle"
export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta }
diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs
index e6607e5c18..7fe630f3c0 100644
--- a/crates/project/src/configuration.rs
+++ b/crates/project/src/configuration.rs
@@ -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,
#[serde(default)]
@@ -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,
};
@@ -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
}
diff --git a/crates/rendering/src/layers/cursor.rs b/crates/rendering/src/layers/cursor.rs
index 4ef81e5bb5..983f576577 100644
--- a/crates/rendering/src/layers/cursor.rs
+++ b/crates/rendering/src/layers/cursor.rs
@@ -37,6 +37,7 @@ pub struct CursorLayer {
circle_cursor: Option,
prev_is_svg_assets_enabled: Option,
prev_cursor_type: Option,
+ prev_circle_color: Option,
}
struct Statics {
@@ -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);
@@ -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;
@@ -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;
@@ -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;
}
}
}
@@ -323,7 +338,7 @@ 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
@@ -331,7 +346,7 @@ impl CursorLayer {
.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,
@@ -348,6 +363,12 @@ 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();
@@ -355,7 +376,8 @@ impl CursorLayer {
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 {
diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts
index 2513f98d22..340854dd95 100644
--- a/packages/ui-solid/src/auto-imports.d.ts
+++ b/packages/ui-solid/src/auto-imports.d.ts
@@ -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']