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

Fix blurry overlay rendering when the pixel display ratio isn't 100% #2204

Merged
merged 2 commits into from
Jan 25, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -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),
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ pub struct OverlaysMessageHandler {
pub overlay_providers: HashSet<OverlayProvider>,
canvas: Option<web_sys::HtmlCanvasElement>,
context: Option<web_sys::CanvasRenderingContext2d>,
device_pixel_ratio: Option<f64>,
}

impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessageHandler {
@@ -22,6 +23,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> 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<OverlaysMessage, OverlaysMessageData<'_>> 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<OverlaysMessage, OverlaysMessageData<'_>> 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);
}
69 changes: 68 additions & 1 deletion editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
@@ -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<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
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,13 +87,17 @@ 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>) {
self.dashed_line(start, end, color, None, None, None)
}

pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
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<f64>, 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<Item = impl Borrow<Subpath<PointId>>>, 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 {
35 changes: 33 additions & 2 deletions frontend/src/components/panels/Document.svelte
Original file line number Diff line number Diff line change
@@ -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:
// <https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#browser_compatibility>
// <https://bugs.webkit.org/show_bug.cgi?id=124862>
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 @@
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
{/if}
</div>
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
<canvas
class="overlays"
width={canvasWidthScaledRoundedToEven}
height={canvasHeightScaledRoundedToEven}
style:width={canvasWidthCSS}
style:height={canvasHeightCSS}
data-overlays-canvas
>
</canvas>
</div>
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>
7 changes: 7 additions & 0 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
@@ -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) {