mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Canvas: a pan by whole pixels copies the pixels that stay on screen
Squashed from #6070 onto main.
This commit is contained in:
@@ -137,6 +137,13 @@ pub struct FrameStats {
|
||||
/// The filtered contents, shadows and glows the frame drew at a drag's lower resolution
|
||||
/// ([`RenderOptions::dragging`]). [`Self::moved`] counts them too.
|
||||
pub previewed: usize,
|
||||
/// What the frame drew in a way that depends on the frame's size: effect rasters and filter
|
||||
/// layers cropped to it or made at a lower resolution, blending groups drawn as layers past its
|
||||
/// offscreen budget, gradient meshes drawn quad by quad past the mesh buffer, and every filter
|
||||
/// layer drawn on a single-threaded context: on a single-threaded renderer, or offscreen in a
|
||||
/// group or an effect's raster. A part of the frame rendered on its own can draw that art with
|
||||
/// other pixels than the whole frame does.
|
||||
pub view_sized: usize,
|
||||
/// How many threads composed the frame from its two ink planes, in a CMYK document that shows
|
||||
/// transparency; 0 for other frames.
|
||||
pub ink_threads: usize,
|
||||
|
||||
@@ -100,6 +100,7 @@ impl Renderer {
|
||||
// The cached raster: of the whole content at full size at this zoom, else of this view (and
|
||||
// the margin around it).
|
||||
let whole = fits && k >= 1.0;
|
||||
self.stats.view_sized += usize::from(!whole);
|
||||
let in_view = (!whole).then(|| ViewCrop::of(f.view, room));
|
||||
let stamp = self.stamp;
|
||||
let channels = match f.ink.base() {
|
||||
@@ -574,10 +575,15 @@ impl Renderer {
|
||||
composite: Option<(vectorcraft_doc::color::BlendMode, f32)>,
|
||||
draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame, Option<(vectorcraft_doc::color::BlendMode, f32)>),
|
||||
) {
|
||||
// A filter layer's coarser grid starts where its context does: at the corner of a context
|
||||
// drawn single-threaded, and at the frame's edge for a region cropped to the frame.
|
||||
let screen = Rect::new(0.0, 0.0, f64::from(ctx.width()), f64::from(ctx.height()));
|
||||
self.stats.view_sized += usize::from(!f.mt || f.view.transform_rect_bbox(reach).union(screen) != screen);
|
||||
if !f.mt {
|
||||
return self.inside_layer(|r| draw(r, ctx, f, composite));
|
||||
}
|
||||
let Some((x0, y0, w, h, k)) = filter_region(f.view.transform_rect_bbox(reach), (ctx.width(), ctx.height()), blur / f.px) else { return };
|
||||
self.stats.view_sized += usize::from(k < 1.0);
|
||||
// During a drag, the region holds at most `DRAG_PIXEL_BUDGET` pixels (#2558).
|
||||
let drag = || filter_region_within(f.view.transform_rect_bbox(reach), (ctx.width(), ctx.height()), blur / f.px, DRAG_PIXEL_BUDGET);
|
||||
let (x0, y0, w, h, k) = match f.opts.dragging.then(drag).flatten().filter(|r| r.4 < k) {
|
||||
|
||||
@@ -183,6 +183,7 @@ impl Renderer {
|
||||
(image, x0, y0, scale, moved, cropped)
|
||||
}
|
||||
};
|
||||
self.stats.view_sized += usize::from(cropped.is_some() || scale != 1.0);
|
||||
Some(Shadow { image, at: Affine::translate((x, y)) * Affine::scale(scale), mode, moved, cropped })
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ impl Renderer {
|
||||
if c.mask.is_some() || self.offscreen_fits(ctx, r, c.isolated) {
|
||||
return self.group_offscreen(ctx, f, c, r, draw);
|
||||
}
|
||||
self.stats.view_sized += 1;
|
||||
}
|
||||
if c.mask.is_some() {
|
||||
// Its backdrop out of reach (or not needed), a masked group starts from transparency.
|
||||
|
||||
@@ -582,6 +582,7 @@ impl Renderer {
|
||||
ctx.set_transform(f.view);
|
||||
return;
|
||||
}
|
||||
self.stats.view_sized += 1;
|
||||
ctx.set_transform(f.view);
|
||||
// Where a patch narrows to slivers, many quads share each pixel, each covering a little of it: their edges
|
||||
// composite to part coverage, and hard edges drop each one. An opaque patch is backed by its own outline in
|
||||
|
||||
@@ -498,4 +498,13 @@ fn nested_isolated_blending_groups_stay_within_the_offscreen_budget() {
|
||||
assert!(images <= budget, "{images} pixels drawn from offscreen groups, budget {budget}");
|
||||
assert_near(frame.pixel(50, 50), blend_rgb(Blend::Multiply, B, S), "inside the groups");
|
||||
assert_near(frame.pixel(10, 10), B, "outside the groups");
|
||||
let mut r = Renderer::new();
|
||||
render_with(&mut r, &d, &RenderOptions::default());
|
||||
assert!(r.stats.view_sized > 0, "levels past the budget depend on the frame's size");
|
||||
let within = doc(|d| {
|
||||
let n = multiply(d);
|
||||
group(d, vec![n], 1.0, Blend::Multiply, true)
|
||||
});
|
||||
render_with(&mut r, &within, &RenderOptions::default());
|
||||
assert_eq!(r.stats.view_sized, 0, "a group within the budget");
|
||||
}
|
||||
|
||||
@@ -180,6 +180,72 @@ fn pans_within_a_cropped_rasters_margin_draw_it_kept() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame counts the art it draws in a way that depends on its size: an effect raster cropped to
|
||||
/// it or made at a lower resolution, drawn from the raster kept as well as made; a filter layer
|
||||
/// cropped to it or made at a lower resolution; any filter layer drawn on a single-threaded context,
|
||||
/// as on a single-threaded renderer or in a group drawn offscreen; and a mesh past the mesh buffer.
|
||||
/// Art with a raster of its own whole, and a filter layer that stays inside the frame, are not
|
||||
/// counted.
|
||||
#[test]
|
||||
fn a_frame_counts_the_art_it_draws_by_its_size() {
|
||||
let effect = |id: &str, params: serde_json::Value| backdrop(&[(id, params)]).0;
|
||||
let mesh = |side: f64| {
|
||||
let mut d = Document::new(595.0, 842.0);
|
||||
let m = vectorcraft_doc::GradientMesh::from_surface(2, 2, &|u, v| vectorcraft_geom::Point::new(u * side, v * side), &|u, v| {
|
||||
Color::rgb(u as f32, v as f32, 0.5)
|
||||
});
|
||||
let layer = d.layers[0].id;
|
||||
let node = Node::new(d.alloc_id(), NodeKind::Mesh(m));
|
||||
d.insert(Some(layer), 0, node).unwrap();
|
||||
d
|
||||
};
|
||||
let blur = effect("blur.gaussian", json!({"radius": 8}));
|
||||
let shadow = effect("stylize.dropShadow", json!({"blur": 8, "x": 4, "y": 4}));
|
||||
let outer_glow = effect("stylize.outerGlow", json!({"blur": 8}));
|
||||
let glow = effect("stylize.innerGlow", json!({"blur": 8}));
|
||||
let wide_glow = effect("stylize.innerGlow", json!({"blur": 2000}));
|
||||
let group_glow = {
|
||||
let mut d = Document::new(595.0, 842.0);
|
||||
let basic = |c: Color| Appearance::basic(Paint::solid(c), Paint::None, 0.0);
|
||||
let wide = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 595.0, 842.0)), basic(Color::rgb(0.9, 0.8, 0.3)));
|
||||
let mut child = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(200.0, 200.0, 330.0, 500.0)), basic(Color::rgb(0.1, 0.3, 0.9)));
|
||||
child.appearance.effects = vec![Effect { id: "stylize.innerGlow".into(), params: json!({"blur": 40}), visible: true }];
|
||||
child.blend = vectorcraft_doc::color::BlendMode::Multiply;
|
||||
let mut group = Node::group(d.alloc_id(), vec![std::sync::Arc::new(wide), std::sync::Arc::new(child)]);
|
||||
group.isolate = true;
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, group).unwrap();
|
||||
d
|
||||
};
|
||||
let cases = [
|
||||
("a whole blur raster", &blur, 1.0, (640, 900), 2, false),
|
||||
("a blur raster cropped to the frame", &blur, 10.0, (256, 128), 2, true),
|
||||
("a blur raster at a lower resolution", &blur, 5.0, (3200, 4400), 2, true),
|
||||
("a whole shadow raster", &shadow, 1.0, (640, 900), 2, false),
|
||||
("a shadow raster cropped to the frame", &shadow, 40.0, (256, 128), 2, true),
|
||||
("an outer glow raster at a lower resolution", &outer_glow, 5.0, (3200, 4400), 2, true),
|
||||
("a filter layer inside the frame", &glow, 1.0, (640, 900), 2, false),
|
||||
("a filter layer past the frame's edge", &glow, 1.0, (256, 128), 2, true),
|
||||
("a filter layer at a lower resolution", &wide_glow, 1.0, (640, 900), 2, true),
|
||||
("a filter layer on a single-threaded renderer", &glow, 1.0, (640, 900), 0, true),
|
||||
("a filter layer in a group drawn offscreen", &group_glow, 1.0, (640, 900), 2, true),
|
||||
];
|
||||
for (name, d, zoom, size, threads, counted) in cases {
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
r.threads = threads;
|
||||
r.render(d, size.0, size.1, view(zoom, size), &opts);
|
||||
assert_eq!(r.stats.view_sized > 0, counted, "{name}");
|
||||
r.render(d, size.0, size.1, view(zoom, size), &opts);
|
||||
assert_eq!(r.stats.view_sized > 0, counted, "{name}, drawn again");
|
||||
}
|
||||
let mut r = Renderer::new();
|
||||
r.render(&mesh(100.0), 2100, 2100, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert_eq!(r.stats.view_sized, 0, "a mesh within the mesh buffer");
|
||||
r.render(&mesh(3000.0), 2100, 2100, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert!(r.stats.view_sized > 0, "a mesh past the mesh buffer");
|
||||
}
|
||||
|
||||
/// Scrolls by amounts that are not whole pixels, as the wheel and a trackpad give them, moved in whole pixels with the part left
|
||||
/// over carried to the next, as the canvas moves them: after the scroll that makes the Drop Shadow's cropped raster
|
||||
/// again with its margin, every later scroll draws it kept. The same scrolls moved as given make it again in every
|
||||
|
||||
Reference in New Issue
Block a user