mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Fix anti-aliasing in overlays by aligning everything with the pixel grid (#1603)
This commit is contained in:
parent
b18822b1b4
commit
fc8b41914b
11 changed files with 92 additions and 66 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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.;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue