Skip to content

Commit fc8b419

Browse files
authored
Fix anti-aliasing in overlays by aligning everything with the pixel grid (#1603)
1 parent b18822b commit fc8b419

File tree

11 files changed

+91
-65
lines changed

11 files changed

+91
-65
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
- master
1010
env:
1111
CARGO_TERM_COLOR: always
12-
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.js"></script>
12+
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
1313

1414
jobs:
1515
build:

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
RUSTC_WRAPPER: /usr/bin/sccache
1919
CARGO_INCREMENTAL: 0
2020
SCCACHE_DIR: /var/lib/github-actions/.cache
21-
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.js"></script>
21+
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
2222

2323
steps:
2424
- name: 📥 Clone and checkout repository

editor/src/consts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
4242
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
4343

4444
// Path tool
45-
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.;
45+
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
4646
pub const SELECTION_THRESHOLD: f64 = 10.;
4747
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
4848
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> f
2525
});
2626

2727
let size = ipp.viewport_bounds.size().as_uvec2();
28-
canvas.set_width(size.x);
29-
canvas.set_height(size.y);
3028

3129
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
3230

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
4040
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
4141
let handle_position = transform.transform_point2(in_handle);
4242
overlay_context.line(handle_position, anchor_position, None);
43-
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
43+
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
4444
}
4545
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
4646
let handle_position = transform.transform_point2(out_handle);
4747
overlay_context.line(handle_position, anchor_position, None);
48-
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
48+
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
4949
}
5050

51-
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
51+
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
5252
}
5353
}
5454
}
@@ -66,14 +66,14 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
6666
let anchor = first_manipulator.anchor;
6767
let anchor_position = transform.transform_point2(anchor);
6868

69-
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
69+
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
7070
};
7171

7272
if let Some(last_manipulator) = manipulator_groups.last() {
7373
let anchor = last_manipulator.anchor;
7474
let anchor_position = transform.transform_point2(anchor);
7575

76-
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
76+
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
7777
};
7878
}
7979
}

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

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,59 +30,72 @@ impl core::hash::Hash for OverlayContext {
3030
impl OverlayContext {
3131
pub fn quad(&mut self, quad: Quad) {
3232
self.render_context.begin_path();
33-
self.render_context.move_to(quad.0[3].x.round(), quad.0[3].y.round());
33+
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);
3434
for i in 0..4 {
35-
self.render_context.line_to(quad.0[i].x.round(), quad.0[i].y.round());
35+
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
3636
}
3737
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
3838
self.render_context.stroke();
3939
}
4040

4141
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
42+
let start = start.round() - DVec2::splat(0.5);
43+
let end = end.round() - DVec2::splat(0.5);
44+
4245
self.render_context.begin_path();
4346
self.render_context.move_to(start.x, start.y);
4447
self.render_context.line_to(end.x, end.y);
4548
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color.unwrap_or(COLOR_OVERLAY_BLUE)));
4649
self.render_context.stroke();
4750
}
4851

49-
pub fn handle(&mut self, position: DVec2, selected: bool) {
52+
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) {
53+
let position = position.round() - DVec2::splat(0.5);
54+
5055
self.render_context.begin_path();
51-
let position = position.round();
52-
self.render_context
53-
.arc(position.x + 0.5, position.y + 0.5, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.)
54-
.expect("draw circle");
56+
self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.).expect("draw circle");
5557

5658
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
5759
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
58-
self.render_context.fill();
5960
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
61+
self.render_context.fill();
6062
self.render_context.stroke();
6163
}
6264

63-
pub fn square(&mut self, position: DVec2, selected: bool, color_selected: Option<&str>) {
64-
let color_selected = color_selected.unwrap_or(COLOR_OVERLAY_BLUE);
65+
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
66+
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
67+
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
68+
self.square(position, None, Some(color_fill), Some(color_stroke));
69+
}
70+
71+
pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
72+
let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
73+
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
74+
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
75+
76+
let position = position.round() - DVec2::splat(0.5);
77+
let corner = position - DVec2::splat(size) / 2.;
6578

6679
self.render_context.begin_path();
67-
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
68-
self.render_context
69-
.rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE);
70-
let fill = if selected { color_selected } else { COLOR_OVERLAY_WHITE };
71-
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
80+
self.render_context.rect(corner.x, corner.y, size, size);
81+
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill));
82+
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke));
7283
self.render_context.fill();
73-
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_selected));
7484
self.render_context.stroke();
7585
}
7686

77-
pub fn pivot(&mut self, pivot: DVec2) {
78-
let x = pivot.x.round();
79-
let y = pivot.y.round();
87+
pub fn pivot(&mut self, position: DVec2) {
88+
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
89+
90+
// Circle
8091

8192
self.render_context.begin_path();
8293
self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., PI * 2.).expect("draw circle");
8394
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW));
8495
self.render_context.fill();
8596

97+
// Crosshair
98+
8699
// Round line caps add half the stroke width to the length on each end, so we subtract that here before halving to get the radius
87100
let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
88101

@@ -101,31 +114,44 @@ impl OverlayContext {
101114
}
102115

103116
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<ManipulatorGroupId>>, transform: DAffine2) {
104-
let transform = |point| transform.transform_point2(point);
105117
self.render_context.begin_path();
106118
for subpath in subpaths {
107119
let mut curves = subpath.iter().peekable();
120+
108121
let Some(first) = curves.peek() else {
109122
continue;
110123
};
111-
self.render_context.move_to(transform(first.start()).x, transform(first.start()).y);
124+
125+
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
112126
for curve in curves {
113127
match curve.handles {
114-
bezier_rs::BezierHandles::Linear => self.render_context.line_to(transform(curve.end()).x, transform(curve.end()).y),
128+
bezier_rs::BezierHandles::Linear => {
129+
let a = transform.transform_point2(curve.end());
130+
let a = a.round() - DVec2::splat(0.5);
131+
132+
self.render_context.line_to(a.x, a.y)
133+
}
115134
bezier_rs::BezierHandles::Quadratic { handle } => {
116-
self.render_context
117-
.quadratic_curve_to(transform(handle).x, transform(handle).y, transform(curve.end()).x, transform(curve.end()).y)
135+
let a = transform.transform_point2(handle);
136+
let b = transform.transform_point2(curve.end());
137+
let a = a.round() - DVec2::splat(0.5);
138+
let b = b.round() - DVec2::splat(0.5);
139+
140+
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
141+
}
142+
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
143+
let a = transform.transform_point2(handle_start);
144+
let b = transform.transform_point2(handle_end);
145+
let c = transform.transform_point2(curve.end());
146+
let a = a.round() - DVec2::splat(0.5);
147+
let b = b.round() - DVec2::splat(0.5);
148+
let c = c.round() - DVec2::splat(0.5);
149+
150+
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
118151
}
119-
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(
120-
transform(handle_start).x,
121-
transform(handle_start).y,
122-
transform(handle_end).x,
123-
transform(handle_end).y,
124-
transform(curve.end()).x,
125-
transform(curve.end()).y,
126-
),
127152
}
128153
}
154+
129155
if subpath.closed() {
130156
self.render_context.close_path();
131157
}

editor/src/messages/tool/common_functionality/snapping.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod grid_snapper;
22
mod layer_snapper;
33
mod snap_results;
4+
use crate::consts::COLOR_OVERLAY_BLUE;
45
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
56
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
67
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
@@ -334,7 +335,7 @@ impl SnapManager {
334335
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
335336

336337
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
337-
overlay_context.square(viewport, true, None);
338+
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
338339
}
339340
}
340341

editor/src/messages/tool/common_functionality/transformation_cage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ impl BoundingBoxManager {
246246
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
247247

248248
for position in self.evaluate_transform_handle_positions() {
249-
overlay_context.square(position, false, None);
249+
overlay_context.square(position, Some(6.), None, None);
250250
}
251251
}
252252

editor/src/messages/tool/tool_messages/gradient_tool.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,15 @@ impl Fsm for GradientToolFsmState {
259259
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
260260

261261
overlay_context.line(start, end, None);
262-
overlay_context.handle(start, dragging == Some(GradientDragTarget::Start));
263-
overlay_context.handle(end, dragging == Some(GradientDragTarget::End));
262+
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
263+
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
264264

265265
for (index, (position, _)) in positions.into_iter().enumerate() {
266266
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
267267
continue;
268268
}
269269

270-
overlay_context.handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)));
270+
overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)));
271271
}
272272
}
273273

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ impl Fsm for PathToolFsmState {
427427
let state = tool_data.update_insertion(shape_editor, document, responses, input.mouse.position);
428428

429429
if let Some(closest_segment) = &tool_data.segment {
430-
overlay_context.square(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW));
430+
overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW));
431431
}
432432

433433
responses.add(PathToolMessage::SelectedPointUpdated);

frontend/src/components/panels/Document.svelte

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@
4141
let showTextInput: boolean;
4242
let textInputMatrix: number[];
4343
44-
// CSS properties
45-
let canvasSvgWidth: number | undefined = undefined;
46-
let canvasSvgHeight: number | undefined = undefined;
47-
let canvasCursor = "default";
48-
4944
// Scrollbars
5045
let scrollbarPos: XY = { x: 0.5, y: 0.5 };
5146
let scrollbarSize: XY = { x: 0.5, y: 0.5 };
@@ -64,6 +59,9 @@
6459
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
6560
let rasterizedContext: CanvasRenderingContext2D | undefined = undefined;
6661
62+
// Cursor icon to display while hovering over the canvas
63+
let canvasCursor = "default";
64+
6765
// Cursor position for cursor floating menus like the Eyedropper tool zoom
6866
let cursorLeft = 0;
6967
let cursorTop = 0;
@@ -73,8 +71,19 @@
7371
let cursorEyedropperPreviewColorPrimary = "";
7472
let cursorEyedropperPreviewColorSecondary = "";
7573
76-
$: canvasWidthCSS = canvasDimensionCSS(canvasSvgWidth);
77-
$: canvasHeightCSS = canvasDimensionCSS(canvasSvgHeight);
74+
// Canvas dimensions
75+
let canvasSvgWidth: number | undefined = undefined;
76+
let canvasSvgHeight: number | undefined = undefined;
77+
78+
// Used to set the canvas rendering dimensions.
79+
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
80+
$: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth);
81+
$: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight);
82+
// Used to set the canvas element size on the page.
83+
// The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived
84+
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
85+
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
86+
7887
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
7988
if (!isWidgetSpanRow(layoutGroup)) return undefined;
8089
@@ -345,15 +354,6 @@
345354
rulerVertical?.resize();
346355
}
347356
348-
function canvasDimensionCSS(dimension: number | undefined): string {
349-
// Temporary placeholder until the first actual value is populated
350-
// This at least gets close to the correct value but an actual number is required to prevent CSS from causing non-integer sizing making the SVG render with anti-aliasing
351-
if (dimension === undefined) return "100%";
352-
353-
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
354-
return `${dimension % 2 === 1 ? dimension + 1 : dimension}px`;
355-
}
356-
357357
onMount(() => {
358358
// Update rendered SVGs
359359
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => {
@@ -491,7 +491,8 @@
491491
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" />
492492
{/if}
493493
</div>
494-
<canvas class="overlays" style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas></canvas>
494+
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
495+
</canvas>
495496
</div>
496497
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork="80%" data-graph>
497498
<Graph />

0 commit comments

Comments
 (0)