Fix anti-aliasing in overlays by aligning everything with the pixel grid (#1603)

This commit is contained in:
Keavon Chambers 2024-02-13 20:47:16 -08:00 committed by GitHub
parent b18822b1b4
commit fc8b41914b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 92 additions and 66 deletions

View file

@ -9,7 +9,7 @@ on:
- master
env:
CARGO_TERM_COLOR: always
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>
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>
jobs:
build:

View file

@ -18,7 +18,7 @@ jobs:
RUSTC_WRAPPER: /usr/bin/sccache
CARGO_INCREMENTAL: 0
SCCACHE_DIR: /var/lib/github-actions/.cache
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>
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>
steps:
- name: 📥 Clone and checkout repository

View file

@ -42,7 +42,7 @@ pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
// Path tool
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.;
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;

View file

@ -25,8 +25,6 @@ impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> f
});
let size = ipp.viewport_bounds.size().as_uvec2();
canvas.set_width(size.x);
canvas.set_height(size.y);
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);

View file

@ -40,15 +40,15 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(in_handle);
overlay_context.line(handle_position, anchor_position, None);
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
}
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(out_handle);
overlay_context.line(handle_position, anchor_position, None);
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
}
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
}
}
}
@ -66,14 +66,14 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
let anchor = first_manipulator.anchor;
let anchor_position = transform.transform_point2(anchor);
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
};
if let Some(last_manipulator) = manipulator_groups.last() {
let anchor = last_manipulator.anchor;
let anchor_position = transform.transform_point2(anchor);
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
};
}
}

View file

@ -30,15 +30,18 @@ impl core::hash::Hash for OverlayContext {
impl OverlayContext {
pub fn quad(&mut self, quad: Quad) {
self.render_context.begin_path();
self.render_context.move_to(quad.0[3].x.round(), quad.0[3].y.round());
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);
for i in 0..4 {
self.render_context.line_to(quad.0[i].x.round(), quad.0[i].y.round());
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
}
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
}
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
self.render_context.begin_path();
self.render_context.move_to(start.x, start.y);
self.render_context.line_to(end.x, end.y);
@ -46,43 +49,53 @@ impl OverlayContext {
self.render_context.stroke();
}
pub fn handle(&mut self, position: DVec2, selected: bool) {
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) {
let position = position.round() - DVec2::splat(0.5);
self.render_context.begin_path();
let position = position.round();
self.render_context
.arc(position.x + 0.5, position.y + 0.5, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.)
.expect("draw circle");
self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.).expect("draw circle");
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
self.render_context.fill();
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.fill();
self.render_context.stroke();
}
pub fn square(&mut self, position: DVec2, selected: bool, color_selected: Option<&str>) {
let color_selected = color_selected.unwrap_or(COLOR_OVERLAY_BLUE);
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
self.square(position, None, Some(color_fill), Some(color_stroke));
}
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);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round() - DVec2::splat(0.5);
let corner = position - DVec2::splat(size) / 2.;
self.render_context.begin_path();
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
self.render_context
.rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE);
let fill = if selected { color_selected } else { COLOR_OVERLAY_WHITE };
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
self.render_context.rect(corner.x, corner.y, size, size);
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill));
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke));
self.render_context.fill();
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_selected));
self.render_context.stroke();
}
pub fn pivot(&mut self, pivot: DVec2) {
let x = pivot.x.round();
let y = pivot.y.round();
pub fn pivot(&mut self, position: DVec2) {
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
// Circle
self.render_context.begin_path();
self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., PI * 2.).expect("draw circle");
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW));
self.render_context.fill();
// Crosshair
// 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
let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
@ -101,31 +114,44 @@ impl OverlayContext {
}
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<ManipulatorGroupId>>, transform: DAffine2) {
let transform = |point| transform.transform_point2(point);
self.render_context.begin_path();
for subpath in subpaths {
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {
continue;
};
self.render_context.move_to(transform(first.start()).x, transform(first.start()).y);
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(transform(curve.end()).x, transform(curve.end()).y),
bezier_rs::BezierHandles::Quadratic { handle } => {
self.render_context
.quadratic_curve_to(transform(handle).x, transform(handle).y, transform(curve.end()).x, transform(curve.end()).y)
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
self.render_context.line_to(a.x, a.y)
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(
transform(handle_start).x,
transform(handle_start).y,
transform(handle_end).x,
transform(handle_end).y,
transform(curve.end()).x,
transform(curve.end()).y,
),
}
}
if subpath.closed() {
self.render_context.close_path();
}

View file

@ -1,6 +1,7 @@
mod grid_snapper;
mod layer_snapper;
mod snap_results;
use crate::consts::COLOR_OVERLAY_BLUE;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
@ -334,7 +335,7 @@ impl SnapManager {
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
overlay_context.square(viewport, true, None);
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
}
}

View file

@ -246,7 +246,7 @@ impl BoundingBoxManager {
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
for position in self.evaluate_transform_handle_positions() {
overlay_context.square(position, false, None);
overlay_context.square(position, Some(6.), None, None);
}
}

View file

@ -259,15 +259,15 @@ impl Fsm for GradientToolFsmState {
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end, None);
overlay_context.handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.handle(end, dragging == Some(GradientDragTarget::End));
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
for (index, (position, _)) in positions.into_iter().enumerate() {
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
continue;
}
overlay_context.handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)));
overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)));
}
}

View file

@ -427,7 +427,7 @@ impl Fsm for PathToolFsmState {
let state = tool_data.update_insertion(shape_editor, document, responses, input.mouse.position);
if let Some(closest_segment) = &tool_data.segment {
overlay_context.square(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW));
overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW));
}
responses.add(PathToolMessage::SelectedPointUpdated);

View file

@ -41,11 +41,6 @@
let showTextInput: boolean;
let textInputMatrix: number[];
// CSS properties
let canvasSvgWidth: number | undefined = undefined;
let canvasSvgHeight: number | undefined = undefined;
let canvasCursor = "default";
// Scrollbars
let scrollbarPos: XY = { x: 0.5, y: 0.5 };
let scrollbarSize: XY = { x: 0.5, y: 0.5 };
@ -64,6 +59,9 @@
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
let rasterizedContext: CanvasRenderingContext2D | undefined = undefined;
// Cursor icon to display while hovering over the canvas
let canvasCursor = "default";
// Cursor position for cursor floating menus like the Eyedropper tool zoom
let cursorLeft = 0;
let cursorTop = 0;
@ -73,8 +71,19 @@
let cursorEyedropperPreviewColorPrimary = "";
let cursorEyedropperPreviewColorSecondary = "";
$: canvasWidthCSS = canvasDimensionCSS(canvasSvgWidth);
$: canvasHeightCSS = canvasDimensionCSS(canvasSvgHeight);
// Canvas dimensions
let canvasSvgWidth: number | undefined = undefined;
let canvasSvgHeight: number | undefined = undefined;
// Used to set the canvas rendering dimensions.
// 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);
// Used to set the canvas element size on the page.
// The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
if (!isWidgetSpanRow(layoutGroup)) return undefined;
@ -345,15 +354,6 @@
rulerVertical?.resize();
}
function canvasDimensionCSS(dimension: number | undefined): string {
// Temporary placeholder until the first actual value is populated
// 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
if (dimension === undefined) return "100%";
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
return `${dimension % 2 === 1 ? dimension + 1 : dimension}px`;
}
onMount(() => {
// Update rendered SVGs
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => {
@ -491,7 +491,8 @@
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" />
{/if}
</div>
<canvas class="overlays" style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas></canvas>
<canvas class="overlays" width={canvasWidthRoundedToEven} height={canvasHeightRoundedToEven} style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas>
</canvas>
</div>
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork="80%" data-graph>
<Graph />