Skip to content

Commit 51c4c37

Browse files
committed
support hi dpi overlay rendering
1 parent ab724d8 commit 51c4c37

File tree

3 files changed

+77
-3
lines changed

3 files changed

+77
-3
lines changed

editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub struct OverlaysMessageHandler {
1111
pub overlay_providers: HashSet<OverlayProvider>,
1212
canvas: Option<web_sys::HtmlCanvasElement>,
1313
context: Option<web_sys::CanvasRenderingContext2d>,
14+
#[allow(dead_code)]
15+
device_pixel_ratio: Option<f64>,
1416
}
1517

1618
impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessageHandler {
@@ -22,6 +24,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
2224
OverlaysMessage::Draw => {
2325
use super::utility_functions::overlay_canvas_element;
2426
use super::utility_types::OverlayContext;
27+
use glam::{DAffine2, DVec2};
2528
use wasm_bindgen::JsCast;
2629

2730
let canvas = match &self.canvas {
@@ -39,17 +42,24 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
3942

4043
let size = ipp.viewport_bounds.size().as_uvec2();
4144

45+
let device_pixel_ratio = *self.device_pixel_ratio.get_or_insert_with(|| web_sys::window().map(|w| w.device_pixel_ratio()).unwrap_or(1.0));
46+
47+
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array();
48+
context.set_transform(a, b, c, d, e, f).expect("scaling is necessary to support HiDPI displays");
4249
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
50+
context.reset_transform().expect("scaling is necessary to support HiDPI displays");
4351

4452
if overlays_visible {
4553
responses.add(DocumentMessage::GridOverlays(OverlayContext {
4654
render_context: context.clone(),
4755
size: size.as_dvec2(),
56+
device_pixel_ratio,
4857
}));
4958
for provider in &self.overlay_providers {
5059
responses.add(provider(OverlayContext {
5160
render_context: context.clone(),
5261
size: size.as_dvec2(),
62+
device_pixel_ratio,
5363
}));
5464
}
5565
}

editor/src/messages/portfolio/document/overlays/utility_types.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pub struct OverlayContext {
2424
#[specta(skip)]
2525
pub render_context: web_sys::CanvasRenderingContext2d,
2626
pub size: DVec2,
27+
// The device pixel ratio is a property provided
28+
// by the browser window and is the css pixel size
29+
// divided by the physical pixel size. It allows
30+
// better pixel density of visualizations on
31+
// high dpi displays (such as Retina displays).
32+
pub device_pixel_ratio: f64,
2733
}
2834
// Message hashing isn't used but is required by the message system macros
2935
impl core::hash::Hash for OverlayContext {
@@ -37,6 +43,7 @@ impl OverlayContext {
3743

3844
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>) {
3945
// Set the dash pattern
46+
self.start_dpi_aware_transform();
4047
if let Some(dash_width) = dash_width {
4148
let dash_gap_width = dash_gap_width.unwrap_or(1.);
4249
let array = js_sys::Array::new();
@@ -80,6 +87,7 @@ impl OverlayContext {
8087
if dash_offset.is_some() && dash_offset != Some(0.) {
8188
self.render_context.set_line_dash_offset(0.);
8289
}
90+
self.end_dpi_aware_transform();
8391
}
8492

8593
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
@@ -88,6 +96,7 @@ impl OverlayContext {
8896

8997
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>) {
9098
// Set the dash pattern
99+
self.start_dpi_aware_transform();
91100
if let Some(dash_width) = dash_width {
92101
let dash_gap_width = dash_gap_width.unwrap_or(1.);
93102
let array = js_sys::Array::new();
@@ -125,9 +134,11 @@ impl OverlayContext {
125134
if dash_offset.is_some() && dash_offset != Some(0.) {
126135
self.render_context.set_line_dash_offset(0.);
127136
}
137+
self.end_dpi_aware_transform();
128138
}
129139

130140
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
141+
self.start_dpi_aware_transform();
131142
let position = position.round() - DVec2::splat(0.5);
132143

133144
self.render_context.begin_path();
@@ -140,6 +151,7 @@ impl OverlayContext {
140151
self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE));
141152
self.render_context.fill();
142153
self.render_context.stroke();
154+
self.end_dpi_aware_transform();
143155
}
144156

145157
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
@@ -148,6 +160,25 @@ impl OverlayContext {
148160
self.square(position, None, Some(color_fill), Some(color_stroke));
149161
}
150162

163+
/// Transforms the Canvas Context To Adjust for DPI
164+
///
165+
/// Overwrites all existing tranforms.
166+
/// This operation can be reversed with [`Self::reset_transform`].
167+
fn start_dpi_aware_transform(&self) {
168+
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(self.device_pixel_ratio)).to_cols_array();
169+
self.render_context
170+
.set_transform(a, b, c, d, e, f)
171+
.expect("transform should be able to be set to be able to account for DPI");
172+
}
173+
174+
/// Untransforms the Canvas Context To Adjust for DPI
175+
///
176+
/// Warning: this function doesn't only reset the
177+
/// DPI adjustment, it resets the entire transform.
178+
fn end_dpi_aware_transform(&self) {
179+
self.render_context.reset_transform().expect("transform should be able to be reset to be able to account for DPI");
180+
}
181+
151182
pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
152183
let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
153184
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
@@ -156,12 +187,14 @@ impl OverlayContext {
156187
let position = position.round() - DVec2::splat(0.5);
157188
let corner = position - DVec2::splat(size) / 2.;
158189

190+
self.start_dpi_aware_transform();
159191
self.render_context.begin_path();
160192
self.render_context.rect(corner.x, corner.y, size, size);
161193
self.render_context.set_fill_style_str(color_fill);
162194
self.render_context.set_stroke_style_str(color_stroke);
163195
self.render_context.fill();
164196
self.render_context.stroke();
197+
self.end_dpi_aware_transform();
165198
}
166199

167200
pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
@@ -171,26 +204,32 @@ impl OverlayContext {
171204
let position = position.round() - DVec2::splat(0.5);
172205
let corner = position - DVec2::splat(size) / 2.;
173206

207+
self.start_dpi_aware_transform();
174208
self.render_context.begin_path();
175209
self.render_context.rect(corner.x, corner.y, size, size);
176210
self.render_context.set_fill_style_str(color_fill);
177211
self.render_context.fill();
212+
self.end_dpi_aware_transform();
178213
}
179214

180215
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
181216
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
182217
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
183218
let position = position.round();
219+
220+
self.start_dpi_aware_transform();
184221
self.render_context.begin_path();
185222
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
186223
self.render_context.set_fill_style_str(color_fill);
187224
self.render_context.set_stroke_style_str(color_stroke);
188225
self.render_context.fill();
189226
self.render_context.stroke();
227+
self.end_dpi_aware_transform();
190228
}
191229
pub fn pivot(&mut self, position: DVec2) {
192230
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
193231

232+
self.start_dpi_aware_transform();
194233
// Circle
195234

196235
self.render_context.begin_path();
@@ -215,9 +254,11 @@ impl OverlayContext {
215254
self.render_context.move_to(x, y - crosshair_radius);
216255
self.render_context.line_to(x, y + crosshair_radius);
217256
self.render_context.stroke();
257+
self.end_dpi_aware_transform();
218258
}
219259

220260
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
261+
self.start_dpi_aware_transform();
221262
self.render_context.begin_path();
222263
let mut last_point = None;
223264
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
@@ -229,16 +270,20 @@ impl OverlayContext {
229270

230271
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
231272
self.render_context.stroke();
273+
self.end_dpi_aware_transform();
232274
}
233275

234276
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
277+
self.start_dpi_aware_transform();
235278
self.render_context.begin_path();
236279
self.bezier_command(bezier, transform, true);
237280
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
238281
self.render_context.stroke();
282+
self.end_dpi_aware_transform();
239283
}
240284

241285
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
286+
self.start_dpi_aware_transform();
242287
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
243288
if move_to {
244289
self.render_context.move_to(start.x, start.y);
@@ -249,9 +294,11 @@ impl OverlayContext {
249294
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
250295
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),
251296
}
297+
self.end_dpi_aware_transform();
252298
}
253299

254300
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
301+
self.start_dpi_aware_transform();
255302
self.render_context.begin_path();
256303
for subpath in subpaths {
257304
let subpath = subpath.borrow();
@@ -298,6 +345,7 @@ impl OverlayContext {
298345

299346
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
300347
self.render_context.stroke();
348+
self.end_dpi_aware_transform();
301349
}
302350

303351
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
@@ -313,7 +361,7 @@ impl OverlayContext {
313361
Pivot::End => -padding,
314362
};
315363

316-
let [a, b, c, d, e, f] = (transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array();
364+
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();
317365
self.render_context.set_transform(a, b, c, d, e, f).expect("Failed to rotate the render context to the specified angle");
318366

319367
if let Some(background) = background_color {

frontend/src/components/panels/Document.svelte

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
let canvasSvgWidth: number | undefined = undefined;
7676
let canvasSvgHeight: number | undefined = undefined;
7777
78-
// Used to set the canvas rendering dimensions.
78+
let devicePixelRatio: number | undefined;
79+
7980
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
8081
$: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth);
8182
$: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight);
@@ -84,6 +85,13 @@
8485
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
8586
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
8687
88+
$: canvasWidthScaled = canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio);
89+
$: canvasHeightScaled = canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio);
90+
91+
// Used to set the canvas rendering dimensions.
92+
$: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled);
93+
$: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled);
94+
8795
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
8896
if (!isWidgetSpanRow(layoutGroup)) return undefined;
8997
@@ -362,6 +370,7 @@
362370
}
363371
364372
onMount(() => {
373+
devicePixelRatio = window.devicePixelRatio;
365374
// Update rendered SVGs
366375
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => {
367376
await tick();
@@ -508,7 +517,14 @@
508517
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
509518
{/if}
510519
</div>
511-
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
520+
<canvas
521+
class="overlays"
522+
width={canvasWidthScaledRoundedToEven}
523+
height={canvasHeightScaledRoundedToEven}
524+
style:width={canvasWidthCSS}
525+
style:height={canvasHeightCSS}
526+
data-overlays-canvas
527+
>
512528
</canvas>
513529
</div>
514530
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>

0 commit comments

Comments
 (0)