Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gpui: Draw Path with 2x to anti-aliasing. #13

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
202 changes: 157 additions & 45 deletions crates/gpui/examples/painting.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use gpui::{
canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path,
canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, Hsla, MouseDownEvent, Path,
Pixels, Point, Render, ViewContext, WindowOptions,
};
struct PaintingViewer {
default_lines: Vec<Path<Pixels>>,
default_lines: Vec<(Path<Pixels>, Hsla)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
_painting: bool,
Expand All @@ -14,34 +14,61 @@ impl PaintingViewer {
let mut lines = vec![];

// draw a line
let mut path = Path::new(point(px(50.), px(180.)));
path.line_to(point(px(100.), px(120.)));
// go back to close the path
path.line_to(point(px(100.), px(121.)));
path.line_to(point(px(50.), px(181.)));
lines.push(path);
let mut path = Path::new(point(px(20.), px(100.)));
path.line_to(point(px(50.), px(160.)));
path.line_to(point(px(80.), px(100.)));
path.line_to(point(px(80.5), px(100.5)));
path.line_to(point(px(51.), px(160.)));
path.line_to(point(px(21.), px(100.)));
lines.push((path, gpui::black()));

// draw a triangle
let mut path = Path::new(point(px(25.), px(0.)));
path.line_to(point(px(50.), px(50.)));
path.line_to(point(px(0.), px(50.)));
path.translate(point(px(100.), px(100.)));
lines.push((path, gpui::red()));

// draw a lightening bolt ⚡
let mut path = Path::new(point(px(150.), px(200.)));
path.line_to(point(px(200.), px(125.)));
path.line_to(point(px(200.), px(175.)));
path.line_to(point(px(250.), px(100.)));
lines.push(path);
let mut path = Path::new(point(px(-50.), px(50.)));
path.line_to(point(px(0.), px(-25.)));
path.line_to(point(px(0.), px(0.)));
path.move_to(point(px(0.), px(0.)));
path.line_to(point(px(50.), px(-50.)));
path.line_to(point(px(0.), px(25.)));
path.line_to(point(px(0.), px(5.)));
path.translate(point(px(220.), px(150.)));
lines.push((path, gpui::blue()));

// draw a ⭐
let mut path = Path::new(point(px(350.), px(100.)));
path.line_to(point(px(370.), px(160.)));
path.line_to(point(px(430.), px(160.)));
path.line_to(point(px(380.), px(200.)));
path.line_to(point(px(400.), px(260.)));
path.line_to(point(px(350.), px(220.)));
path.line_to(point(px(300.), px(260.)));
path.line_to(point(px(320.), px(200.)));
path.line_to(point(px(270.), px(160.)));
path.line_to(point(px(330.), px(160.)));
path.line_to(point(px(350.), px(100.)));
lines.push(path);
let mut path = Path::new(point(px(76.8), px(116.864)));
path.line_to(point(px(31.6608), px(142.1312)));
path.line_to(point(px(41.7408), px(91.392)));
path.line_to(point(px(3.7568), px(56.2688)));
path.line_to(point(px(55.1296), px(50.176)));
path.line_to(point(px(76.8), px(3.2)));
path.line_to(point(px(98.4704), px(50.176)));
path.line_to(point(px(149.8432), px(56.2688)));
path.line_to(point(px(111.8592), px(91.392)));
path.line_to(point(px(121.9392), px(142.1312)));
path.translate(point(px(270.), px(80.)));
lines.push((path, gpui::yellow()));

// draw double square
// https://yqnn.github.io/svg-path-editor/#P=M_2_1_L_2_3_L_4_3_L_4_4_L_6_4_L_6_2_L_4_2_L_4_1_L_2_1
let mut path = Path::new(point(px(0.), px(50.)));
path.line_to(point(px(0.), px(150.)));
path.line_to(point(px(100.), px(150.)));
path.line_to(point(px(100.), px(200.)));
path.line_to(point(px(200.), px(200.)));
path.line_to(point(px(200.), px(100.)));
path.line_to(point(px(100.), px(100.)));
path.line_to(point(px(100.), px(50.)));
path.line_to(point(px(0.), px(50.)));
path.translate(point(px(20.), px(200.)));
lines.push((path, gpui::black()));

// draw a square with rounded corners
let square_bounds = Bounds {
origin: point(px(450.), px(100.)),
size: size(px(200.), px(80.)),
Expand All @@ -59,8 +86,7 @@ impl PaintingViewer {
square_bounds.lower_right(),
square_bounds.upper_right() + point(px(0.0), vertical_offset),
);
path.line_to(square_bounds.lower_left());
lines.push(path);
lines.push((path, gpui::green()));

Self {
default_lines: lines.clone(),
Expand All @@ -75,6 +101,107 @@ impl PaintingViewer {
cx.notify();
}
}

fn build_line_path(points: Vec<Point<Pixels>>, width: f32) -> Path<Pixels> {
let mut path = Path::new(point(points[0].x, points[0].y));
let half_width = width / 2.0;
let angle_threshold: f32 = 15.;
// 4~6 for performance, 8~12 for medium, 16~24 for high quality
const SEGMENT: usize = 0;
let angle_threshold_cos = angle_threshold.to_radians().cos();

for i in 0..points.len() - 1 {
let p0 = points[i];
let p1 = points[i + 1];

// Calculate direction vector and normal
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let length = (dx * dx + dy * dy).0.sqrt();
let dir = [dx / length, dy / length];
let normal = [-dir[1] * half_width, dir[0] * half_width];

// Current segment boundary vertices
let left0 = [p0.x - normal[0], p0.y - normal[1]];
let right0 = [p0.x + normal[0], p0.y + normal[1]];
let left1 = [p1.x - normal[0], p1.y - normal[1]];
let right1 = [p1.x + normal[0], p1.y + normal[1]];

// Add main triangles of the current segment
path.move_to(point(left0[0], left0[1]));
path.line_to(point(right0[0], right0[1]));
path.line_to(point(left1[0], left1[1]));

path.move_to(point(right0[0], right0[1]));
path.line_to(point(right1[0], right1[1]));
path.line_to(point(left1[0], left1[1]));

// Corner handling
if i < points.len() - 2 {
let p2 = points[i + 2];

// Previous and next direction vectors
let next_length = ((p2.x - p1.x).0.powi(2) + (p2.y - p1.y).0.powi(2)).sqrt();
let prev_dir = [dir[0], dir[1]];
let next_dir = [(p2.x - p1.x) / next_length, (p2.y - p1.y) / next_length];

// Calculate angle
let cos_angle = prev_dir[0] * next_dir[0] + prev_dir[1] * next_dir[1];

if cos_angle.0 < -0.99 {
// 180 degree turn: fill intersection area
path.line_to(point(p1.x - normal[0], p1.y - normal[1]));
path.line_to(point(p1.x + normal[0], p1.y + normal[1]));
continue;
} else if cos_angle.0 > angle_threshold_cos {
// Sharp angle: fill intersection area, generate polygon cover
let mut intersection_points = vec![
[p1.x + normal[0], p1.y + normal[1]],
[p1.x - normal[0], p1.y - normal[1]],
];
let step = (1.0 - cos_angle.0) * (std::f32::consts::PI / 2.0) / SEGMENT as f32;
for j in 0..=SEGMENT {
let theta = j as f32 * step;
let rotated = [
prev_dir[0] * theta.cos() - prev_dir[1] * theta.sin(),
prev_dir[0] * theta.sin() + prev_dir[1] * theta.cos(),
];
let rounded_vertex = [
p1.x + rotated[0] * half_width,
p1.y + rotated[1] * half_width,
];
intersection_points.push(rounded_vertex);
}
for k in 1..intersection_points.len() - 1 {
path.move_to(point(intersection_points[0][0], intersection_points[0][1]));
path.line_to(point(intersection_points[k][0], intersection_points[k][1]));
path.line_to(point(
intersection_points[k + 1][0],
intersection_points[k + 1][1],
));
}
} else {
// Regular corner handling
let step = (std::f32::consts::PI - cos_angle.0.acos()) / SEGMENT as f32;
for j in 0..=SEGMENT {
let theta = j as f32 * step;
let rotated = [
prev_dir[0] * theta.cos() - prev_dir[1] * theta.sin(),
prev_dir[0] * theta.sin() + prev_dir[1] * theta.cos(),
];
let rounded_vertex = [
p1.x + rotated[0] * half_width,
p1.y + rotated[1] * half_width,
];
path.line_to(point(rounded_vertex[0], rounded_vertex[1]));
}
}
}
}

path
}

impl Render for PaintingViewer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone();
Expand Down Expand Up @@ -115,26 +242,11 @@ impl Render for PaintingViewer {
canvas(
move |_, _| {},
move |_, _, cx| {
const STROKE_WIDTH: Pixels = px(2.0);
for path in default_lines {
cx.paint_path(path, gpui::black());
for (path, color) in default_lines {
cx.paint_path(path, color);
}
for points in lines {
let mut path = Path::new(points[0]);
for p in points.iter().skip(1) {
path.line_to(*p);
}

let mut last = points.last().unwrap();
for p in points.iter().rev() {
let mut offset_x = px(0.);
if last.x == p.x {
offset_x = STROKE_WIDTH;
}
path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH));
last = p;
}

let path = build_line_path(points, 1.5);
cx.paint_path(path, gpui::black());
}
},
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2456,6 +2456,7 @@ impl From<usize> for Pixels {
Copy,
Default,
Div,
Mul,
Eq,
Hash,
Ord,
Expand Down
19 changes: 13 additions & 6 deletions crates/gpui/src/platform/blade/blade_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
use crate::{
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla,
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
ScaledPixels, Scene, Shadow, Size, Underline,
ScaledPixels, Scene, Shadow, Size, Underline, PATH_SUBPIXEL_VARIANTS,
};
use bytemuck::{Pod, Zeroable};
use collections::HashMap;
Expand Down Expand Up @@ -622,12 +622,19 @@ impl BladeRenderer {
for path in paths {
let tile = &self.path_tiles[&path.id];
let tex_info = self.atlas.get_texture_info(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
let origin = path
.bounds
.intersect(&path.content_mask.bounds)
.origin
.map(|p| (p / PATH_SUBPIXEL_VARIANTS).floor());
let size = tile
.bounds
.size
.map(|s| s / PATH_SUBPIXEL_VARIANTS as i32)
.map(Into::into);

let sprites = [PathSprite {
bounds: Bounds {
origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into),
},
bounds: Bounds { origin, size },
color: path.color,
tile: (*tile).clone(),
}];
Expand Down
17 changes: 12 additions & 5 deletions crates/gpui/src/platform/mac/metal_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
PATH_SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Result};
use block::ConcreteBlock;
Expand Down Expand Up @@ -733,12 +734,18 @@ impl MetalRenderer {
if let Some((path, tile)) = paths_and_tiles.peek() {
if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
prev_texture_id = Some(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
let origin = path
.bounds
.intersect(&path.content_mask.bounds)
.origin
.map(|p| (p / PATH_SUBPIXEL_VARIANTS).floor());
let size = tile
.bounds
.size
.map(|s| s / PATH_SUBPIXEL_VARIANTS as i32)
.map(Into::into);
sprites.push(PathSprite {
bounds: Bounds {
origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into),
},
bounds: Bounds { origin, size },
color: path.color,
tile: (*tile).clone(),
});
Expand Down
23 changes: 22 additions & 1 deletion crates/gpui/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use crate::{
};
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};

/// Subpixel variants for antialiasing for Path render.
pub(crate) const PATH_SUBPIXEL_VARIANTS: f32 = 2.0;

#[allow(non_camel_case_types, unused)]
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;

Expand Down Expand Up @@ -785,13 +788,20 @@ impl Path<Pixels> {
.iter()
.map(|vertex| vertex.scale(factor))
.collect(),
start: self.start.map(|start| start.scale(factor)),
start: self.start.scale(factor),
current: self.current.scale(factor),
contour_count: self.contour_count,
color: self.color,
}
}

/// Move the current point of the path to the given point.
pub fn move_to(&mut self, to: Point<Pixels>) {
self.start = to;
self.current = to;
self.contour_count = 0;
}

/// Draw a straight line from the current point to the given point.
pub fn line_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
Expand Down Expand Up @@ -821,6 +831,17 @@ impl Path<Pixels> {
self.current = to;
}

/// Translate path by the given offset.
pub fn translate(&mut self, offset: Point<Pixels>) {
self.start = self.start + offset;
self.current = self.current + offset;
self.bounds.origin = self.bounds.origin + offset;
self.content_mask.bounds.origin = self.content_mask.bounds.origin + offset;
for vertex in &mut self.vertices {
vertex.xy_position = vertex.xy_position + offset;
}
}

fn push_triangle(
&mut self,
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
Expand Down
4 changes: 2 additions & 2 deletions crates/gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix, Underline,
UnderlineStyle, View, VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
PATH_SUBPIXEL_VARIANTS, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
Expand Down Expand Up @@ -2330,7 +2330,7 @@ impl<'a> WindowContext<'a> {
self.window
.next_frame
.scene
.insert_primitive(path.scale(scale_factor));
.insert_primitive(path.scale(scale_factor * PATH_SUBPIXEL_VARIANTS));
}

/// Paint an underline into the scene for the next frame at the current z-index.
Expand Down