🐛 Fix stroke weight visually different with different levels of zoom

This commit is contained in:
Alejandro Alonso
2026-01-29 09:07:12 +01:00
committed by Alonso Torres
parent b881e36875
commit 6a84215911
2 changed files with 54 additions and 23 deletions

View File

@@ -27,8 +27,8 @@ fn draw_stroke_on_rect(
// - The same rect if it's a center stroke
// - A bigger rect if it's an outer stroke
// - A smaller rect if it's an outer stroke
let stroke_rect = stroke.outer_rect(rect);
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
let stroke_rect = stroke.aligned_rect(rect, scale);
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
// Apply both blur and shadow filters if present, composing them if necessary.
let filter = compose_filters(blur, shadow);
@@ -63,8 +63,8 @@ fn draw_stroke_on_circle(
// - The same oval if it's a center stroke
// - A bigger oval if it's an outer stroke
// - A smaller oval if it's an outer stroke
let stroke_rect = stroke.outer_rect(rect);
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
let stroke_rect = stroke.aligned_rect(rect, scale);
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
// Apply both blur and shadow filters if present, composing them if necessary.
let filter = compose_filters(blur, shadow);
@@ -131,7 +131,6 @@ pub fn draw_stroke_on_path(
selrect: &Rect,
path_transform: Option<&Matrix>,
svg_attrs: Option<&SvgAttrs>,
scale: f32,
shadow: Option<&ImageFilter>,
blur: Option<&ImageFilter>,
antialias: bool,
@@ -142,7 +141,7 @@ pub fn draw_stroke_on_path(
let is_open = path.is_open();
let mut paint: skia_safe::Handle<_> =
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
let filter = compose_filters(blur, shadow);
paint.set_image_filter(filter);
@@ -166,7 +165,6 @@ pub fn draw_stroke_on_path(
canvas,
is_open,
svg_attrs,
scale,
blur,
antialias,
);
@@ -218,7 +216,6 @@ fn handle_stroke_caps(
canvas: &skia::Canvas,
is_open: bool,
svg_attrs: Option<&SvgAttrs>,
scale: f32,
blur: Option<&ImageFilter>,
antialias: bool,
) {
@@ -233,8 +230,7 @@ fn handle_stroke_caps(
let first_point = points.first().unwrap();
let last_point = points.last().unwrap();
let mut paint_stroke =
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
if let Some(filter) = blur {
paint_stroke.set_image_filter(filter.clone());
@@ -405,7 +401,7 @@ fn draw_image_stroke_in_container(
// Draw the stroke based on the shape type, we are using this stroke as
// a "selector" of the area of the image we want to show.
let outer_rect = stroke.outer_rect(container);
let outer_rect = stroke.aligned_rect(container, scale);
match &shape.shape_type {
shape_type @ (Type::Rect(_) | Type::Frame(_)) => {
@@ -450,8 +446,7 @@ fn draw_image_stroke_in_container(
}
}
let is_open = p.is_open();
let mut paint =
stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, scale, antialias);
let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias);
canvas.draw_path(&path, &paint);
if stroke.render_kind(is_open) == StrokeKind::Outer {
// Small extra inner stroke to overlap with the fill
@@ -466,7 +461,6 @@ fn draw_image_stroke_in_container(
canvas,
is_open,
svg_attrs,
scale,
shape.image_filter(1.).as_ref(),
antialias,
);
@@ -662,7 +656,6 @@ fn render_internal(
&selrect,
path_transform.as_ref(),
svg_attrs,
scale,
shadow,
shape.image_filter(1.).as_ref(),
antialias,
@@ -685,14 +678,13 @@ pub fn render_text_paths(
shadow: Option<&ImageFilter>,
antialias: bool,
) {
let scale = render_state.get_scale();
let canvas = render_state
.surfaces
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
let selrect = &shape.selrect;
let svg_attrs = shape.svg_attrs.as_ref();
let mut paint: skia_safe::Handle<_> =
stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
stroke.to_text_stroked_paint(false, selrect, svg_attrs, antialias);
if let Some(filter) = shadow {
paint.set_image_filter(filter.clone());

View File

@@ -1,3 +1,4 @@
use crate::math::is_close_to;
use crate::shapes::fills::{Fill, SolidColor};
use skia_safe::{self as skia, Rect};
@@ -144,6 +145,15 @@ impl Stroke {
}
}
pub fn aligned_rect(&self, rect: &Rect, scale: f32) -> Rect {
let stroke_rect = self.outer_rect(rect);
if self.kind != StrokeKind::Center {
return stroke_rect;
}
align_rect_to_half_pixel(&stroke_rect, self.width, scale)
}
pub fn outer_corners(&self, corners: &Corners) -> Corners {
let offset = match self.kind {
StrokeKind::Center => 0.0,
@@ -162,7 +172,6 @@ impl Stroke {
&self,
rect: &Rect,
svg_attrs: Option<&SvgAttrs>,
scale: f32,
antialias: bool,
) -> skia::Paint {
let mut paint = self.fill.to_paint(rect, antialias);
@@ -171,7 +180,7 @@ impl Stroke {
let width = match self.kind {
StrokeKind::Inner => self.width,
StrokeKind::Center => self.width,
StrokeKind::Outer => self.width + (1. / scale),
StrokeKind::Outer => self.width,
};
paint.set_stroke_width(width);
@@ -230,10 +239,9 @@ impl Stroke {
is_open: bool,
rect: &Rect,
svg_attrs: Option<&SvgAttrs>,
scale: f32,
antialias: bool,
) -> skia::Paint {
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
let mut paint = self.to_paint(rect, svg_attrs, antialias);
match self.render_kind(is_open) {
StrokeKind::Inner => {
paint.set_stroke_width(2. * paint.stroke_width());
@@ -254,10 +262,9 @@ impl Stroke {
is_open: bool,
rect: &Rect,
svg_attrs: Option<&SvgAttrs>,
scale: f32,
antialias: bool,
) -> skia::Paint {
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
let mut paint = self.to_paint(rect, svg_attrs, antialias);
match self.render_kind(is_open) {
StrokeKind::Inner => {
paint.set_stroke_width(2. * paint.stroke_width());
@@ -284,6 +291,38 @@ impl Stroke {
}
}
fn align_rect_to_half_pixel(rect: &Rect, stroke_width: f32, scale: f32) -> Rect {
if scale <= 0.0 {
return *rect;
}
let stroke_pixels = stroke_width * scale;
let stroke_pixels_rounded = stroke_pixels.round();
if !is_close_to(stroke_pixels, stroke_pixels_rounded) {
return *rect;
}
if (stroke_pixels_rounded as i32) % 2 == 0 {
return *rect;
}
let left_px = rect.left * scale;
let top_px = rect.top * scale;
let target_frac = 0.5;
let dx_px = target_frac - (left_px - left_px.floor());
let dy_px = target_frac - (top_px - top_px.floor());
if is_close_to(dx_px, 0.0) && is_close_to(dy_px, 0.0) {
return *rect;
}
Rect::from_xywh(
rect.left + (dx_px / scale),
rect.top + (dy_px / scale),
rect.width(),
rect.height(),
)
}
fn cap_margin_for_cap(cap: Option<StrokeCap>, width: f32) -> f32 {
match cap {
Some(StrokeCap::LineArrow)