mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Fix blurry overlay rendering when the pixel display ratio isn't 100% (#2204)
* support hi dpi overlay rendering * Code review and make scaling ratio dynamic --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
9954e49530
commit
33ac141fb8
5 changed files with 122 additions and 4 deletions
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,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<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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue