From fc8b41914bed8a4e502dab283e29f057cc56b91f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 13 Feb 2024 20:47:16 -0800 Subject: [PATCH] Fix anti-aliasing in overlays by aligning everything with the pixel grid (#1603) --- .github/workflows/ci.yml | 2 +- .github/workflows/deploy.yml | 2 +- editor/src/consts.rs | 2 +- .../overlays/overlays_message_handler.rs | 2 - .../document/overlays/utility_functions.rs | 10 +- .../document/overlays/utility_types.rs | 92 ++++++++++++------- .../tool/common_functionality/snapping.rs | 3 +- .../transformation_cage.rs | 2 +- .../tool/tool_messages/gradient_tool.rs | 6 +- .../messages/tool/tool_messages/path_tool.rs | 2 +- .../src/components/panels/Document.svelte | 35 +++---- 11 files changed, 92 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 521d82aeb..75b75beec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: - master env: CARGO_TERM_COLOR: always - INDEX_HTML_HEAD_REPLACEMENT: + INDEX_HTML_HEAD_REPLACEMENT: jobs: build: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3ff7dbdab..6e67982e6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,7 @@ jobs: RUSTC_WRAPPER: /usr/bin/sccache CARGO_INCREMENTAL: 0 SCCACHE_DIR: /var/lib/github-actions/.cache - INDEX_HTML_HEAD_REPLACEMENT: + INDEX_HTML_HEAD_REPLACEMENT: steps: - name: 📥 Clone and checkout repository diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 9d50dfba2..2c82c6b3d 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -42,7 +42,7 @@ pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.; pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.; // Path tool -pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.; +pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.; pub const SELECTION_THRESHOLD: f64 = 10.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.; diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 4adb63f01..9768c9fce 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -25,8 +25,6 @@ impl MessageHandler f }); let size = ipp.viewport_bounds.size().as_uvec2(); - canvas.set_width(size.x); - canvas.set_height(size.y); context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y); diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 09a7d42a2..030e07254 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -40,15 +40,15 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) { let handle_position = transform.transform_point2(in_handle); overlay_context.line(handle_position, anchor_position, None); - overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle))); + overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle))); } if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) { let handle_position = transform.transform_point2(out_handle); overlay_context.line(handle_position, anchor_position, None); - overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle))); + overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle))); } - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None); + overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None); } } } @@ -66,14 +66,14 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: & let anchor = first_manipulator.anchor; let anchor_position = transform.transform_point2(anchor); - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None); + overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None); }; if let Some(last_manipulator) = manipulator_groups.last() { let anchor = last_manipulator.anchor; let anchor_position = transform.transform_point2(anchor); - overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None); + overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None); }; } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index e99113aa6..0cd94e8c1 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -30,15 +30,18 @@ impl core::hash::Hash for OverlayContext { impl OverlayContext { pub fn quad(&mut self, quad: Quad) { self.render_context.begin_path(); - self.render_context.move_to(quad.0[3].x.round(), quad.0[3].y.round()); + self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5); for i in 0..4 { - self.render_context.line_to(quad.0[i].x.round(), quad.0[i].y.round()); + self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5); } self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE)); self.render_context.stroke(); } pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) { + let start = start.round() - DVec2::splat(0.5); + let end = end.round() - DVec2::splat(0.5); + self.render_context.begin_path(); self.render_context.move_to(start.x, start.y); self.render_context.line_to(end.x, end.y); @@ -46,43 +49,53 @@ impl OverlayContext { self.render_context.stroke(); } - pub fn handle(&mut self, position: DVec2, selected: bool) { + pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) { + let position = position.round() - DVec2::splat(0.5); + self.render_context.begin_path(); - let position = position.round(); - self.render_context - .arc(position.x + 0.5, position.y + 0.5, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.) - .expect("draw circle"); + self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.).expect("draw circle"); let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); - self.render_context.fill(); self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE)); + self.render_context.fill(); self.render_context.stroke(); } - pub fn square(&mut self, position: DVec2, selected: bool, color_selected: Option<&str>) { - let color_selected = color_selected.unwrap_or(COLOR_OVERLAY_BLUE); + pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE); + let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE }; + self.square(position, None, Some(color_fill), Some(color_stroke)); + } + + pub fn square(&mut self, position: DVec2, size: Option, color_fill: Option<&str>, color_stroke: Option<&str>) { + let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE); + let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + + let position = position.round() - DVec2::splat(0.5); + let corner = position - DVec2::splat(size) / 2.; self.render_context.begin_path(); - let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.; - self.render_context - .rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE); - let fill = if selected { color_selected } else { COLOR_OVERLAY_WHITE }; - self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); + self.render_context.rect(corner.x, corner.y, size, size); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); self.render_context.fill(); - self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_selected)); self.render_context.stroke(); } - pub fn pivot(&mut self, pivot: DVec2) { - let x = pivot.x.round(); - let y = pivot.y.round(); + pub fn pivot(&mut self, position: DVec2) { + let (x, y) = (position.round() - DVec2::splat(0.5)).into(); + + // Circle self.render_context.begin_path(); self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., PI * 2.).expect("draw circle"); self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW)); self.render_context.fill(); + // Crosshair + // Round line caps add half the stroke width to the length on each end, so we subtract that here before halving to get the radius let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.; @@ -101,31 +114,44 @@ impl OverlayContext { } pub fn outline<'a>(&mut self, subpaths: impl Iterator>, transform: DAffine2) { - let transform = |point| transform.transform_point2(point); self.render_context.begin_path(); for subpath in subpaths { let mut curves = subpath.iter().peekable(); + let Some(first) = curves.peek() else { continue; }; - self.render_context.move_to(transform(first.start()).x, transform(first.start()).y); + + self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y); for curve in curves { match curve.handles { - bezier_rs::BezierHandles::Linear => self.render_context.line_to(transform(curve.end()).x, transform(curve.end()).y), - bezier_rs::BezierHandles::Quadratic { handle } => { - self.render_context - .quadratic_curve_to(transform(handle).x, transform(handle).y, transform(curve.end()).x, transform(curve.end()).y) + bezier_rs::BezierHandles::Linear => { + let a = transform.transform_point2(curve.end()); + let a = a.round() - DVec2::splat(0.5); + + self.render_context.line_to(a.x, a.y) + } + bezier_rs::BezierHandles::Quadratic { handle } => { + let a = transform.transform_point2(handle); + let b = transform.transform_point2(curve.end()); + let a = a.round() - DVec2::splat(0.5); + let b = b.round() - DVec2::splat(0.5); + + self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y) + } + bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + let a = transform.transform_point2(handle_start); + let b = transform.transform_point2(handle_end); + let c = transform.transform_point2(curve.end()); + let a = a.round() - DVec2::splat(0.5); + let b = b.round() - DVec2::splat(0.5); + let c = c.round() - DVec2::splat(0.5); + + self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y) } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to( - transform(handle_start).x, - transform(handle_start).y, - transform(handle_end).x, - transform(handle_end).y, - transform(curve.end()).x, - transform(curve.end()).y, - ), } } + if subpath.closed() { self.render_context.close_path(); } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 419c30f22..ec5ad9df0 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -1,6 +1,7 @@ mod grid_snapper; mod layer_snapper; mod snap_results; +use crate::consts::COLOR_OVERLAY_BLUE; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget}; @@ -334,7 +335,7 @@ impl SnapManager { let viewport = to_viewport.transform_point2(ind.snapped_point_document); overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.); - overlay_context.square(viewport, true, None); + overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); } } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 8b2c1b8c7..368217d08 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -246,7 +246,7 @@ impl BoundingBoxManager { overlay_context.quad(self.transform * Quad::from_box(self.bounds)); for position in self.evaluate_transform_handle_positions() { - overlay_context.square(position, false, None); + overlay_context.square(position, Some(6.), None, None); } } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 352cd4160..ca16476f3 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -259,15 +259,15 @@ impl Fsm for GradientToolFsmState { let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); overlay_context.line(start, end, None); - overlay_context.handle(start, dragging == Some(GradientDragTarget::Start)); - overlay_context.handle(end, dragging == Some(GradientDragTarget::End)); + overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start)); + overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End)); for (index, (position, _)) in positions.into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index))); + overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index))); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 9dc42582d..8eff07d4d 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -427,7 +427,7 @@ impl Fsm for PathToolFsmState { let state = tool_data.update_insertion(shape_editor, document, responses, input.mouse.position); if let Some(closest_segment) = &tool_data.segment { - overlay_context.square(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW)); + overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW)); } responses.add(PathToolMessage::SelectedPointUpdated); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5105b08ee..c0a4e5b32 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -41,11 +41,6 @@ let showTextInput: boolean; let textInputMatrix: number[]; - // CSS properties - let canvasSvgWidth: number | undefined = undefined; - let canvasSvgHeight: number | undefined = undefined; - let canvasCursor = "default"; - // Scrollbars let scrollbarPos: XY = { x: 0.5, y: 0.5 }; let scrollbarSize: XY = { x: 0.5, y: 0.5 }; @@ -64,6 +59,9 @@ let rasterizedCanvas: HTMLCanvasElement | undefined = undefined; let rasterizedContext: CanvasRenderingContext2D | undefined = undefined; + // Cursor icon to display while hovering over the canvas + let canvasCursor = "default"; + // Cursor position for cursor floating menus like the Eyedropper tool zoom let cursorLeft = 0; let cursorTop = 0; @@ -73,8 +71,19 @@ let cursorEyedropperPreviewColorPrimary = ""; let cursorEyedropperPreviewColorSecondary = ""; - $: canvasWidthCSS = canvasDimensionCSS(canvasSvgWidth); - $: canvasHeightCSS = canvasDimensionCSS(canvasSvgHeight); + // Canvas dimensions + let canvasSvgWidth: number | undefined = undefined; + let canvasSvgHeight: number | undefined = undefined; + + // Used to set the canvas rendering dimensions. + // Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing + $: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth); + $: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight); + // Used to set the canvas element size on the page. + // The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived + $: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%"; + $: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%"; + $: toolShelfTotalToolsAndSeparators = ((layoutGroup) => { if (!isWidgetSpanRow(layoutGroup)) return undefined; @@ -345,15 +354,6 @@ rulerVertical?.resize(); } - function canvasDimensionCSS(dimension: number | undefined): string { - // Temporary placeholder until the first actual value is populated - // This at least gets close to the correct value but an actual number is required to prevent CSS from causing non-integer sizing making the SVG render with anti-aliasing - if (dimension === undefined) return "100%"; - - // Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing - return `${dimension % 2 === 1 ? dimension + 1 : dimension}px`; - } - onMount(() => { // Update rendered SVGs editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => { @@ -491,7 +491,8 @@
{/if}
- + +