Skip to content

Commit 0a3b5d2

Browse files
andystopiaKeavon
authored andcommitted
support hi dpi overlay rendering
1 parent 9954e49 commit 0a3b5d2

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
@@ -26,6 +26,12 @@ pub struct OverlayContext {
2626
#[specta(skip)]
2727
pub render_context: web_sys::CanvasRenderingContext2d,
2828
pub size: DVec2,
29+
// The device pixel ratio is a property provided
30+
// by the browser window and is the css pixel size
31+
// divided by the physical pixel size. It allows
32+
// better pixel density of visualizations on
33+
// high dpi displays (such as Retina displays).
34+
pub device_pixel_ratio: f64,
2935
}
3036
// Message hashing isn't used but is required by the message system macros
3137
impl core::hash::Hash for OverlayContext {
@@ -39,6 +45,7 @@ impl OverlayContext {
3945

4046
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>) {
4147
// Set the dash pattern
48+
self.start_dpi_aware_transform();
4249
if let Some(dash_width) = dash_width {
4350
let dash_gap_width = dash_gap_width.unwrap_or(1.);
4451
let array = js_sys::Array::new();
@@ -82,6 +89,7 @@ impl OverlayContext {
8289
if dash_offset.is_some() && dash_offset != Some(0.) {
8390
self.render_context.set_line_dash_offset(0.);
8491
}
92+
self.end_dpi_aware_transform();
8593
}
8694

8795
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
@@ -90,6 +98,7 @@ impl OverlayContext {
9098

9199
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>) {
92100
// Set the dash pattern
101+
self.start_dpi_aware_transform();
93102
if let Some(dash_width) = dash_width {
94103
let dash_gap_width = dash_gap_width.unwrap_or(1.);
95104
let array = js_sys::Array::new();
@@ -127,9 +136,11 @@ impl OverlayContext {
127136
if dash_offset.is_some() && dash_offset != Some(0.) {
128137
self.render_context.set_line_dash_offset(0.);
129138
}
139+
self.end_dpi_aware_transform();
130140
}
131141

132142
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
143+
self.start_dpi_aware_transform();
133144
let position = position.round() - DVec2::splat(0.5);
134145

135146
self.render_context.begin_path();
@@ -142,6 +153,7 @@ impl OverlayContext {
142153
self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE));
143154
self.render_context.fill();
144155
self.render_context.stroke();
156+
self.end_dpi_aware_transform();
145157
}
146158

147159
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
@@ -150,6 +162,25 @@ impl OverlayContext {
150162
self.square(position, None, Some(color_fill), Some(color_stroke));
151163
}
152164

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

192+
self.start_dpi_aware_transform();
161193
self.render_context.begin_path();
162194
self.render_context.rect(corner.x, corner.y, size, size);
163195
self.render_context.set_fill_style_str(color_fill);
164196
self.render_context.set_stroke_style_str(color_stroke);
165197
self.render_context.fill();
166198
self.render_context.stroke();
199+
self.end_dpi_aware_transform();
167200
}
168201

169202
pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
@@ -173,22 +206,27 @@ impl OverlayContext {
173206
let position = position.round() - DVec2::splat(0.5);
174207
let corner = position - DVec2::splat(size) / 2.;
175208

209+
self.start_dpi_aware_transform();
176210
self.render_context.begin_path();
177211
self.render_context.rect(corner.x, corner.y, size, size);
178212
self.render_context.set_fill_style_str(color_fill);
179213
self.render_context.fill();
214+
self.end_dpi_aware_transform();
180215
}
181216

182217
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
183218
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
184219
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
185220
let position = position.round();
221+
222+
self.start_dpi_aware_transform();
186223
self.render_context.begin_path();
187224
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
188225
self.render_context.set_fill_style_str(color_fill);
189226
self.render_context.set_stroke_style_str(color_stroke);
190227
self.render_context.fill();
191228
self.render_context.stroke();
229+
self.end_dpi_aware_transform();
192230
}
193231

194232
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
@@ -252,6 +290,7 @@ impl OverlayContext {
252290
pub fn pivot(&mut self, position: DVec2) {
253291
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
254292

293+
self.start_dpi_aware_transform();
255294
// Circle
256295

257296
self.render_context.begin_path();
@@ -276,9 +315,11 @@ impl OverlayContext {
276315
self.render_context.move_to(x, y - crosshair_radius);
277316
self.render_context.line_to(x, y + crosshair_radius);
278317
self.render_context.stroke();
318+
self.end_dpi_aware_transform();
279319
}
280320

281321
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
322+
self.start_dpi_aware_transform();
282323
self.render_context.begin_path();
283324
let mut last_point = None;
284325
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
@@ -290,16 +331,20 @@ impl OverlayContext {
290331

291332
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
292333
self.render_context.stroke();
334+
self.end_dpi_aware_transform();
293335
}
294336

295337
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
338+
self.start_dpi_aware_transform();
296339
self.render_context.begin_path();
297340
self.bezier_command(bezier, transform, true);
298341
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
299342
self.render_context.stroke();
343+
self.end_dpi_aware_transform();
300344
}
301345

302346
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
347+
self.start_dpi_aware_transform();
303348
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
304349
if move_to {
305350
self.render_context.move_to(start.x, start.y);
@@ -310,9 +355,11 @@ impl OverlayContext {
310355
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
311356
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),
312357
}
358+
self.end_dpi_aware_transform();
313359
}
314360

315361
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
362+
self.start_dpi_aware_transform();
316363
self.render_context.begin_path();
317364
for subpath in subpaths {
318365
let subpath = subpath.borrow();
@@ -359,6 +406,7 @@ impl OverlayContext {
359406

360407
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
361408
self.render_context.stroke();
409+
self.end_dpi_aware_transform();
362410
}
363411

364412
pub fn get_width(&self, text: &str) -> f64 {
@@ -378,7 +426,7 @@ impl OverlayContext {
378426
Pivot::End => -padding,
379427
};
380428

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

384432
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)