Skip to content
Merged
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
36 changes: 20 additions & 16 deletions src/apps/desktop/src/computer_use/macos_capture.m
Original file line number Diff line number Diff line change
Expand Up @@ -615,22 +615,26 @@ - (void)drawRect:(NSRect)rect {
[arrow transformUsingAffineTransform:size];
arrow.lineWidth = 3;
arrow.lineJoinStyle = NSLineJoinStyleRound;
[NSGraphicsContext saveGraphicsState];
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowColor = [NSColor colorWithWhite:0 alpha:0.45];
shadow.shadowBlurRadius = 3;
// This view is flipped, so positive Y places the shadow below the cursor.
shadow.shadowOffset = NSMakeSize(0, 2);
[shadow set];
// Cast one shadow from the complete silhouette, including its white edge.
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
CGContextBeginTransparencyLayer(context, NULL);
[[NSColor colorWithSRGBRed:64.0/255 green:64.0/255 blue:64.0/255 alpha:1] setFill];
[arrow fill];
[NSColor.whiteColor setStroke];
[arrow stroke];
CGContextEndTransparencyLayer(context);
[NSGraphicsContext restoreGraphicsState];
// A broad halo plus a denser contact shadow, both centered on the complete
// silhouette. Zero offset keeps the falloff even around every white edge.
const CGFloat blur[] = {5, 1.5};
const CGFloat opacity[] = {0.5, 0.8};
for (NSUInteger layer = 0; layer < 2; ++layer) {
[NSGraphicsContext saveGraphicsState];
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowColor = [NSColor colorWithWhite:0 alpha:opacity[layer]];
shadow.shadowBlurRadius = blur[layer];
shadow.shadowOffset = NSZeroSize;
[shadow set];
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
CGContextBeginTransparencyLayer(context, NULL);
[[NSColor colorWithSRGBRed:64.0/255 green:64.0/255 blue:64.0/255 alpha:1] setFill];
[arrow fill];
[NSColor.whiteColor setStroke];
[arrow stroke];
CGContextEndTransparencyLayer(context);
[NSGraphicsContext restoreGraphicsState];
}
}
@end
static NSPanel *obfPointerPanel;
Expand Down
45 changes: 38 additions & 7 deletions src/apps/desktop/src/computer_use/windows_pointer_feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,21 +219,30 @@ fn pointer_pixels(clicked: bool) -> Vec<u32> {
}
}
let weights: Vec<f64> = (-5..=5)
.map(|n| (-(f64::from(n).powi(2)) / (2.0 * 1.5 * 1.5)).exp())
.map(|n| (-(f64::from(n).powi(2)) / (2.0 * 2.0 * 2.0)).exp())
.collect();
let edge_weights: Vec<f64> = (-5..=5)
.map(|n| (-(f64::from(n).powi(2)) / (2.0 * 0.65 * 0.65)).exp())
.collect();
let sum: f64 = weights.iter().sum();
let edge_sum: f64 = edge_weights.iter().sum();
let mut pixels = vec![0u32; 1600];
for y in 0..40i32 {
for x in 0..40i32 {
let mut shadow = 0.0;
let mut edge_shadow = 0.0;
for dy in -5..=5 {
for dx in -5..=5 {
let (mx, my) = (x - dx, y - 2 - dy);
let (mx, my) = (x - dx, y - dy);
if (0..40).contains(&mx) && (0..40).contains(&my) {
shadow += mask[(my * 40 + mx) as usize]
* weights[(dx + 5) as usize]
* weights[(dy + 5) as usize]
/ (sum * sum);
let coverage = mask[(my * 40 + mx) as usize];
shadow +=
coverage * weights[(dx + 5) as usize] * weights[(dy + 5) as usize]
/ (sum * sum);
edge_shadow += coverage
* edge_weights[(dx + 5) as usize]
* edge_weights[(dy + 5) as usize]
/ (edge_sum * edge_sum);
}
}
}
Expand All @@ -247,7 +256,9 @@ fn pointer_pixels(clicked: bool) -> Vec<u32> {
} else {
0.0
};
let behind = ring + shadow * 0.45 * (1.0 - ring);
// Center both layers: dark beside the white edge, softer farther out.
let halo = edge_shadow * 0.8 + shadow * 0.5 * (1.0 - edge_shadow * 0.8);
let behind = ring + halo * (1.0 - ring);
let alpha = silhouette + behind * (1.0 - silhouette);
let channel =
(64.0 * fill + 255.0 * stroke + 166.0 * ring * (1.0 - silhouette)).round() as u32;
Expand Down Expand Up @@ -433,5 +444,25 @@ mod tests {
assert!(pixels.iter().all(|p| p & 255 <= p >> 24));
assert_eq!(pixels[0], 0);
assert_ne!(pixels, pointer_pixels(true));
// The contour is diagonal-symmetric; directional shadow offsets must
// not bias one side. Outside each edge, black fades toward transparency.
for y in 0..40 {
for x in 0..40 {
assert_eq!(pixels[y * 40 + x], pixels[x * 40 + y]);
}
}
for ray in [
[(12, 5), (12, 4), (12, 3)],
[(5, 12), (4, 12), (3, 12)],
[(28, 16), (29, 16), (30, 16)],
[(16, 28), (16, 29), (16, 30)],
] {
let alpha = ray.map(|(x, y)| {
let pixel = pixels[y * 40 + x];
assert_eq!(pixel & 0xffffff, 0);
pixel >> 24
});
assert!(alpha[0] > alpha[1] && alpha[1] > alpha[2] && alpha[2] > 0);
}
}
}
5 changes: 4 additions & 1 deletion src/web-ui/src/app/components/ComputerUseControlCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
stroke: var(--openbitfun-color-content-on-dark);
stroke-width: 3px;
stroke-linejoin: round;
filter: drop-shadow(0 2px 3px color-mix(in srgb, var(--openbitfun-color-content-on-light) 45%, transparent));
// Centered contour shadows: a dark contact edge with a wider soft halo.
filter:
drop-shadow(0 0 1px color-mix(in srgb, var(--openbitfun-color-content-on-light) 80%, transparent))
drop-shadow(0 0 4px color-mix(in srgb, var(--openbitfun-color-content-on-light) 50%, transparent));
}

}
Expand Down
Loading