diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message.rs b/editor/src/messages/portfolio/document/overlays/overlays_message.rs index 408126bd2a..6716e17db3 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message.rs @@ -5,7 +5,7 @@ use crate::messages::prelude::*; #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum OverlaysMessage { Draw, - + SetDevicePixelRatio { ratio: f64 }, // Serde functionality isn't used but is required by the message system macros AddProvider(#[serde(skip, default = "empty_provider")] OverlayProvider), RemoveProvider(#[serde(skip, default = "empty_provider")] OverlayProvider), 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 2ea0467c51..17d6e38728 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -11,6 +11,7 @@ pub struct OverlaysMessageHandler { pub overlay_providers: HashSet, canvas: Option, context: Option, + device_pixel_ratio: Option, } impl MessageHandler> for OverlaysMessageHandler { @@ -22,6 +23,7 @@ impl MessageHandler> for OverlaysMessag OverlaysMessage::Draw => { use super::utility_functions::overlay_canvas_element; use super::utility_types::OverlayContext; + use glam::{DAffine2, DVec2}; use wasm_bindgen::JsCast; let canvas = match &self.canvas { @@ -39,17 +41,24 @@ impl MessageHandler> for OverlaysMessag let size = ipp.viewport_bounds.size().as_uvec2(); + let device_pixel_ratio = self.device_pixel_ratio.unwrap_or(1.); + + let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array(); + let _ = context.set_transform(a, b, c, d, e, f); context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y); + let _ = context.reset_transform(); if overlays_visible { responses.add(DocumentMessage::GridOverlays(OverlayContext { render_context: context.clone(), size: size.as_dvec2(), + device_pixel_ratio, })); for provider in &self.overlay_providers { responses.add(provider(OverlayContext { render_context: context.clone(), size: size.as_dvec2(), + device_pixel_ratio, })); } } @@ -61,6 +70,10 @@ impl MessageHandler> for OverlaysMessag self.canvas, self.context ); } + OverlaysMessage::SetDevicePixelRatio { ratio } => { + self.device_pixel_ratio = Some(ratio); + responses.add(OverlaysMessage::Draw); + } OverlaysMessage::AddProvider(message) => { self.overlay_providers.insert(message); } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 4be7a567d7..b93f296f54 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -26,6 +26,9 @@ pub struct OverlayContext { #[specta(skip)] pub render_context: web_sys::CanvasRenderingContext2d, pub size: DVec2, + // The device pixel ratio is a property provided by the browser window and is the CSS pixel size divided by the physical monitor's pixel size. + // It allows better pixel density of visualizations on high-DPI displays where the OS display scaling is not 100%, or where the browser is zoomed. + pub device_pixel_ratio: f64, } // Message hashing isn't used but is required by the message system macros impl core::hash::Hash for OverlayContext { @@ -38,6 +41,8 @@ impl OverlayContext { } pub fn dashed_quad(&mut self, quad: Quad, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.start_dpi_aware_transform(); + // Set the dash pattern if let Some(dash_width) = dash_width { let dash_gap_width = dash_gap_width.unwrap_or(1.); @@ -82,6 +87,8 @@ impl OverlayContext { if dash_offset.is_some() && dash_offset != Some(0.) { self.render_context.set_line_dash_offset(0.); } + + self.end_dpi_aware_transform(); } pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) { @@ -89,6 +96,8 @@ impl OverlayContext { } pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.start_dpi_aware_transform(); + // Set the dash pattern if let Some(dash_width) = dash_width { let dash_gap_width = dash_gap_width.unwrap_or(1.); @@ -127,9 +136,13 @@ impl OverlayContext { if dash_offset.is_some() && dash_offset != Some(0.) { self.render_context.set_line_dash_offset(0.); } + + self.end_dpi_aware_transform(); } pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.start_dpi_aware_transform(); + let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); @@ -142,6 +155,8 @@ impl OverlayContext { self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); self.render_context.fill(); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { @@ -150,6 +165,23 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } + /// Transforms the canvas context to adjust for DPI scaling + /// + /// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`]. + fn start_dpi_aware_transform(&self) { + let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(self.device_pixel_ratio)).to_cols_array(); + self.render_context + .set_transform(a, b, c, d, e, f) + .expect("transform should be able to be set to be able to account for DPI"); + } + + /// Un-transforms the Canvas context to adjust for DPI scaling + /// + /// Warning: this function doesn't only reset the DPI scaling adjustment, it resets the entire transform. + fn end_dpi_aware_transform(&self) { + self.render_context.reset_transform().expect("transform should be able to be reset to be able to account for DPI"); + } + 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); @@ -158,12 +190,16 @@ impl OverlayContext { let position = position.round() - DVec2::splat(0.5); let corner = position - DVec2::splat(size) / 2.; + self.start_dpi_aware_transform(); + self.render_context.begin_path(); self.render_context.rect(corner.x, corner.y, size, size); self.render_context.set_fill_style_str(color_fill); self.render_context.set_stroke_style_str(color_stroke); self.render_context.fill(); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } pub fn pixel(&mut self, position: DVec2, color: Option<&str>) { @@ -173,22 +209,31 @@ impl OverlayContext { let position = position.round() - DVec2::splat(0.5); let corner = position - DVec2::splat(size) / 2.; + self.start_dpi_aware_transform(); + self.render_context.begin_path(); self.render_context.rect(corner.x, corner.y, size, size); self.render_context.set_fill_style_str(color_fill); self.render_context.fill(); + + self.end_dpi_aware_transform(); } pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let position = position.round(); + + self.start_dpi_aware_transform(); + self.render_context.begin_path(); self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); self.render_context.set_fill_style_str(color_fill); self.render_context.set_stroke_style_str(color_stroke); self.render_context.fill(); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { @@ -252,6 +297,8 @@ impl OverlayContext { pub fn pivot(&mut self, position: DVec2) { let (x, y) = (position.round() - DVec2::splat(0.5)).into(); + self.start_dpi_aware_transform(); + // Circle self.render_context.begin_path(); @@ -276,9 +323,15 @@ impl OverlayContext { self.render_context.move_to(x, y - crosshair_radius); self.render_context.line_to(x, y + crosshair_radius); self.render_context.stroke(); + + self.render_context.set_line_cap("butt"); + + self.end_dpi_aware_transform(); } pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { + self.start_dpi_aware_transform(); + self.render_context.begin_path(); let mut last_point = None; for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() { @@ -290,16 +343,24 @@ impl OverlayContext { self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + self.start_dpi_aware_transform(); + self.render_context.begin_path(); self.bezier_command(bezier, transform, true); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) { + self.start_dpi_aware_transform(); + let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point)); if move_to { self.render_context.move_to(start.x, start.y); @@ -310,9 +371,13 @@ impl OverlayContext { bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y), bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y), } + + self.end_dpi_aware_transform(); } pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + self.start_dpi_aware_transform(); + self.render_context.begin_path(); for subpath in subpaths { let subpath = subpath.borrow(); @@ -359,6 +424,8 @@ impl OverlayContext { self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); + + self.end_dpi_aware_transform(); } pub fn get_width(&self, text: &str) -> f64 { @@ -378,7 +445,7 @@ impl OverlayContext { Pivot::End => -padding, }; - let [a, b, c, d, e, f] = (transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array(); + let [a, b, c, d, e, f] = (DAffine2::from_scale(DVec2::splat(self.device_pixel_ratio)) * transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array(); self.render_context.set_transform(a, b, c, d, e, f).expect("Failed to rotate the render context to the specified angle"); if let Some(background) = background_color { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 9101df87c3..ece02f4bdd 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -75,7 +75,8 @@ let canvasSvgWidth: number | undefined = undefined; let canvasSvgHeight: number | undefined = undefined; - // Used to set the canvas rendering dimensions. + let devicePixelRatio: number | undefined; + // 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); @@ -84,6 +85,13 @@ $: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%"; $: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%"; + $: canvasWidthScaled = canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio); + $: canvasHeightScaled = canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio); + + // Used to set the canvas rendering dimensions. + $: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled); + $: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled); + $: toolShelfTotalToolsAndSeparators = ((layoutGroup) => { if (!isWidgetSpanRow(layoutGroup)) return undefined; @@ -362,6 +370,22 @@ } onMount(() => { + // Not compatible with Safari: + // + // + let removeUpdatePixelRatio: (() => void) | undefined = undefined; + const updatePixelRatio = () => { + removeUpdatePixelRatio?.(); + const mediaQueryList = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + // The event is one-time use, so we have to set up a new listener and remove the old one every time + mediaQueryList.addEventListener("change", updatePixelRatio); + removeUpdatePixelRatio = () => mediaQueryList.removeEventListener("change", updatePixelRatio); + + devicePixelRatio = window.devicePixelRatio; + editor.handle.setDevicePixelRatio(devicePixelRatio); + }; + updatePixelRatio(); + // Update rendered SVGs editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => { await tick(); @@ -508,7 +532,14 @@
{/if}
- +
diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 425fa4837b..19b7550863 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -352,6 +352,13 @@ impl EditorHandle { self.dispatch(message); } + /// Inform the overlays system of the current device pixel ratio + #[wasm_bindgen(js_name = setDevicePixelRatio)] + pub fn set_device_pixel_ratio(&self, ratio: f64) { + let message = OverlaysMessage::SetDevicePixelRatio { ratio }; + self.dispatch(message); + } + /// Mouse movement within the screenspace bounds of the viewport #[wasm_bindgen(js_name = onMouseMove)] pub fn on_mouse_move(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {