mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
A Drop Shadow, Gaussian Blur, Feather or Photoshop-style filter raster too large to keep whole is cropped to the view. It was drawn again only while a later frame's rect, panned back, lay inside the frame it was made in, so every pan that showed new pixels made it again. For a Drop Shadow on a 6000 x 4000 pt image in a 2560 x 1440 view at 100% to 400%, a pan took 60 to 84 ms; with Gaussian Blur or Feather, a pan took 158 to 217 ms. When such a raster is made again at the zoom and rotation of the one kept for the object, it now also covers a margin around the view: up to half the view's longer side, while the raster stays within the 8.4 million pixel budget at full size (441 pixels on each side of a 2560 x 1440 view for a 20 pt blur at 100%). Later pans that keep the view inside the margin draw that raster. The first frame at a zoom, a zoom step and the frames of a drag make the raster without the margin, as before. A shadow made over an earlier cropped shadow covers only the rect the earlier one was made for. A kept raster is drawn again only after a pan by whole device pixels, but the wheel, a trackpad and the scroll bars scrolled the view by amounts that are not whole pixels. The canvas now moves the view by whole device pixels when it scrolls or is dragged. A scroll by the wheel, a trackpad, a touch screen or a scroll bar is rounded, and the part of a pixel left over is added to the next scroll of the same view; it is dropped after a zoom, a turn, Fit or a document switch. A Hand tool, Space or middle-button drag and a scroll bar thumb drag round the distance from where the drag began. The silhouette of a shadow or glow raster is now drawn with art culled against the raster's own area instead of the view, as filtered content already is. Before, a group member outside the view cast no shadow into a raster kept whole, so a pan that showed the member drew it without its shadow. A cropped raster kept across the margin would have done the same. The moved silhouette that a drag compares is drawn the same way. Docs: docs/target-app-parity.md.
366 lines
19 KiB
Rust
366 lines
19 KiB
Rust
//! Effect rasters cropped to the view, or made at a lower resolution, are kept while the view
|
||
//! stays as it is: dragging other art over an object whose effects reach far past the view (a wide
|
||
//! blur, or any blur zoomed in) draws its raster from the last frame. A cropped raster made again
|
||
//! after a pan is kept while later pans stay within the margin it is made with.
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_doc::color::{Color, Paint};
|
||
use vectorcraft_doc::{Appearance, Document, Effect, ImageBlob, ImageObject, Node, NodeId, NodeKind};
|
||
use vectorcraft_geom::{Affine, Rect, shapes};
|
||
|
||
use crate::{RenderOptions, Renderer};
|
||
|
||
/// An A4 page filled by a blue rectangle under `effects`, and a small orange square over it
|
||
/// (returned with the document): the square is the art a drag moves.
|
||
fn backdrop(effects: &[(&str, serde_json::Value)]) -> (Document, NodeId) {
|
||
let mut d = Document::new(595.0, 842.0);
|
||
let layer = d.layers[0].id;
|
||
let mut back = Node::path(
|
||
d.alloc_id(),
|
||
shapes::rectangle(Rect::new(0.0, 0.0, 595.0, 842.0)),
|
||
Appearance::basic(Paint::solid(Color::rgb(0.1, 0.3, 0.9)), Paint::None, 0.0),
|
||
);
|
||
back.appearance.effects = effects.iter().map(|(id, p)| Effect { id: (*id).into(), params: p.clone(), visible: true }).collect();
|
||
d.insert(Some(layer), 0, back).unwrap();
|
||
let id = d.alloc_id();
|
||
let square = Node::path(
|
||
id,
|
||
shapes::rectangle(Rect::new(280.0, 400.0, 300.0, 420.0)),
|
||
Appearance::basic(Paint::solid(Color::rgb(0.9, 0.4, 0.1)), Paint::None, 0.0),
|
||
);
|
||
d.insert(Some(layer), 1, square).unwrap();
|
||
(d, id)
|
||
}
|
||
|
||
/// A `w` × `h` view at `zoom`, centered on the page.
|
||
fn view(zoom: f64, (w, h): (u32, u32)) -> Affine {
|
||
Affine::translate((f64::from(w) / 2.0, f64::from(h) / 2.0)) * Affine::scale(zoom) * Affine::translate((-297.5, -421.0))
|
||
}
|
||
|
||
/// `base` with the square moved by `step` steps of a drag.
|
||
fn moved(base: &Document, id: NodeId, step: u32, zoom: f64) -> Document {
|
||
let mut d = base.clone();
|
||
let by = (f64::from(step) * 3.0 / zoom, f64::from(step) * 2.0 / zoom);
|
||
d.node_mut(id).unwrap().transform(Affine::translate(by), false);
|
||
d
|
||
}
|
||
|
||
/// A case's name, its effects (id and params) and its zoom.
|
||
type Case = (&'static str, Vec<(&'static str, serde_json::Value)>, f64);
|
||
|
||
/// Every effect whose raster can be cropped to the view or made coarser, with a zoom at which it
|
||
/// is: wide blurs at the fit zoom, narrow ones zoomed in, a Photoshop-style filter, a shadow and a
|
||
/// glow.
|
||
fn cases() -> Vec<Case> {
|
||
vec![
|
||
("Gaussian Blur 500 pt", vec![("blur.gaussian", json!({"radius": 500}))], 1.6),
|
||
("Gaussian Blur 100 pt", vec![("blur.gaussian", json!({"radius": 100}))], 1.6),
|
||
("Gaussian Blur 20 pt", vec![("blur.gaussian", json!({"radius": 20}))], 4.0),
|
||
("Gaussian Blur 8 pt", vec![("blur.gaussian", json!({"radius": 8}))], 16.0),
|
||
("Feather 20 pt", vec![("stylize.feather", json!({"radius": 20}))], 4.0),
|
||
("Craquelure", vec![("texture.craquelure", json!({}))], 4.0),
|
||
("Drop Shadow blur 100 pt", vec![("stylize.dropShadow", json!({"blur": 100, "x": 20, "y": 20}))], 1.6),
|
||
("Drop Shadow blur 10 pt", vec![("stylize.dropShadow", json!({"blur": 10, "x": 5, "y": 5}))], 8.0),
|
||
("Outer Glow blur 100 pt", vec![("stylize.outerGlow", json!({"blur": 100}))], 1.6),
|
||
]
|
||
}
|
||
|
||
/// A drag of the square over the backdrop draws the backdrop's effects from the rasters of the
|
||
/// frame before the drag, at full resolution: no frame of the drag previews them at a drag's lower
|
||
/// resolution, and each draws as a fresh renderer draws its still frame.
|
||
#[test]
|
||
fn a_drag_over_art_with_effects_past_the_view_draws_their_rasters_kept() {
|
||
let size = (640, 360);
|
||
for (name, effects, zoom) in cases() {
|
||
let (base, id) = backdrop(&effects);
|
||
let xf = view(zoom, size);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let live = RenderOptions { interacting: true, dragging: true, ..opts.clone() };
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&base, size.0, size.1, xf, &opts);
|
||
for step in 1..4 {
|
||
let d = moved(&base, id, step, zoom);
|
||
let drag = r.render(&d, size.0, size.1, xf, &live);
|
||
assert_eq!(r.stats.previewed, 0, "{name}: drag frame {step} drew the backdrop's effects from the rasters kept");
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
let still = fresh.render(&d, size.0, size.1, xf, &opts);
|
||
assert!(drag.pixels == still.pixels, "{name}: drag frame {step} draws as a still frame");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A pan by whole pixels or a zoom makes the rasters cropped to the view again: the frame after it draws as a
|
||
/// fresh renderer draws it.
|
||
#[test]
|
||
fn rasters_cropped_to_the_view_follow_a_pan_and_a_zoom() {
|
||
let size = (640, 360);
|
||
for (name, effects, zoom) in cases() {
|
||
let (d, _) = backdrop(&effects);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&d, size.0, size.1, view(zoom, size), &opts);
|
||
for xf in [Affine::translate((37.0, -21.0)) * view(zoom, size), view(zoom * 1.25, size)] {
|
||
let image = r.render(&d, size.0, size.1, xf, &opts);
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
assert!(image.pixels == fresh.render(&d, size.0, size.1, xf, &opts).pixels, "{name}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A 6000 × 4000 pt image (300 × 200 pixels, 20 pt a pixel) under `effects`.
|
||
fn large_image(effects: &[(&str, serde_json::Value)]) -> Document {
|
||
let img = image::RgbaImage::from_fn(300, 200, |x, y| image::Rgba([(x * 7) as u8, (y * 13) as u8, ((x + y) * 3) as u8, 255]));
|
||
let mut png = vec![];
|
||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
let mut d = Document::new(6000.0, 4000.0);
|
||
d.images.insert("viewraster-pan".into(), ImageBlob::new("image/png", png));
|
||
let im = ImageObject {
|
||
key: "viewraster-pan".into(),
|
||
width: 300,
|
||
height: 200,
|
||
xf: Affine::scale(20.0),
|
||
link: None,
|
||
placement: Default::default(),
|
||
pixels: None,
|
||
};
|
||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||
n.appearance.effects = effects.iter().map(|(id, p)| Effect { id: (*id).into(), params: p.clone(), visible: true }).collect();
|
||
let layer = d.layers[0].id;
|
||
d.insert(Some(layer), 0, n).unwrap();
|
||
d
|
||
}
|
||
|
||
/// At 100 %, the Drop Shadow, Gaussian Blur and Feather rasters of a large image are cropped to the view. The pan
|
||
/// that makes one again at the same zoom makes it with a margin around the view, and later pans within the margin
|
||
/// (by up to 259 pixels from the frame that made them, and back) draw it without making any offscreen raster: each
|
||
/// such frame makes as many offscreen pixels as the same view drawn again, and draws as a fresh renderer draws it,
|
||
/// within a level a channel: an image sampled into a raster that starts at another pixel rounds by up to a level, as it
|
||
/// does in the frames of two fresh renderers whose views differ in size.
|
||
#[test]
|
||
fn pans_within_a_cropped_rasters_margin_draw_it_kept() {
|
||
let size = (640, 360);
|
||
let cases: Vec<Case> = vec![
|
||
("Drop Shadow", vec![("stylize.dropShadow", json!({"blur": 20, "x": 7, "y": 7}))], 1.0),
|
||
("Gaussian Blur", vec![("blur.gaussian", json!({"radius": 20}))], 1.0),
|
||
("Feather", vec![("stylize.feather", json!({"radius": 20}))], 1.0),
|
||
(
|
||
"Drop Shadow over a Drop Shadow",
|
||
vec![("stylize.dropShadow", json!({"blur": 20, "x": 7, "y": 7})), ("stylize.dropShadow", json!({"blur": 10, "x": -5, "y": 9}))],
|
||
1.0,
|
||
),
|
||
];
|
||
let counted = |r: &mut Renderer, d: &Document, xf: Affine, opts: &RenderOptions| {
|
||
crate::OFFSCREEN_PIXELS.with(|n| n.set(0));
|
||
let image = r.render(d, size.0, size.1, xf, opts);
|
||
(image, crate::OFFSCREEN_PIXELS.with(|n| n.get()))
|
||
};
|
||
for (name, effects, zoom) in cases {
|
||
let d = large_image(&effects);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let at = |dx: f64, dy: f64| Affine::translate((320.0 + dx, 180.0 + dy)) * Affine::scale(zoom) * Affine::translate((-3000.0, -2000.0));
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&d, size.0, size.1, at(0.0, 0.0), &opts);
|
||
// The pan that makes the rasters again, and the same view drawn again.
|
||
r.render(&d, size.0, size.1, at(37.0, 0.0), &opts);
|
||
let (_, kept) = counted(&mut r, &d, at(37.0, 0.0), &opts);
|
||
for (dx, dy) in [(74.0, 0.0), (74.0, -37.0), (296.0, -37.0), (0.0, 0.0), (-222.0, 37.0)] {
|
||
let (image, made) = counted(&mut r, &d, at(dx, dy), &opts);
|
||
assert_eq!(made, kept, "{name}: the pan to ({dx}, {dy}) drew the rasters kept");
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
let still = fresh.render(&d, size.0, size.1, at(dx, dy), &opts);
|
||
let worst = image.pixels.iter().zip(&still.pixels).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
|
||
assert!(worst <= 1, "{name}: ({dx}, {dy}): a channel differs by {worst}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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
|
||
/// frame.
|
||
#[test]
|
||
fn fractional_scrolls_moved_in_whole_pixels_draw_a_cropped_raster_kept() {
|
||
let size = (640, 360);
|
||
let d = large_image(&[("stylize.dropShadow", json!({"blur": 20, "x": 7, "y": 7}))]);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let at = |dx: f64, dy: f64| Affine::translate((320.0 + dx, 180.0 + dy)) * Affine::translate((-3000.0, -2000.0));
|
||
let counted = |r: &mut Renderer, xf: Affine| {
|
||
crate::OFFSCREEN_PIXELS.with(|n| n.set(0));
|
||
r.render(&d, size.0, size.1, xf, &opts);
|
||
crate::OFFSCREEN_PIXELS.with(|n| n.get())
|
||
};
|
||
let scrolls = [(0.37, 1.13), (-2.71, 0.0), (0.0, 0.45), (1.9, -0.83), (0.21, 0.21), (13.6, -4.4), (-0.5, 0.5), (7.77, 2.22)];
|
||
for whole in [true, false] {
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&d, size.0, size.1, at(0.0, 0.0), &opts);
|
||
// The scroll that makes the raster again with its margin, and the same view drawn again.
|
||
r.render(&d, size.0, size.1, at(37.0, 0.0), &opts);
|
||
let kept = counted(&mut r, at(37.0, 0.0));
|
||
let (mut given, mut moved): ((f64, f64), (f64, f64)) = ((37.0, 0.0), (37.0, 0.0));
|
||
let mut made_again = 0;
|
||
for (k, (x, y)) in scrolls.into_iter().enumerate() {
|
||
given = (given.0 + x, given.1 + y);
|
||
moved = if whole { (given.0.round(), given.1.round()) } else { given };
|
||
let made = counted(&mut r, at(moved.0, moved.1));
|
||
if whole {
|
||
assert_eq!(made, kept, "scroll {k} to {moved:?} drew the raster kept");
|
||
} else if made > kept {
|
||
made_again += 1;
|
||
}
|
||
}
|
||
assert!((moved.0 - given.0).abs() <= 0.5 && (moved.1 - given.1).abs() <= 0.5, "moved {moved:?} for {given:?}");
|
||
if !whole {
|
||
assert_eq!(made_again, scrolls.len(), "scrolls by amounts that are not whole pixels make the raster again");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A group's shadow raster holds the shadows of the members outside the view: a pan that brings a member into view
|
||
/// draws its shadow from the raster kept as a fresh renderer draws it, for a raster of the group's whole reach (two
|
||
/// 100 pt squares) and for one cropped to the view and the margin a pan adds (two 3000 × 2000 pt rectangles).
|
||
#[test]
|
||
fn a_groups_shadow_raster_holds_the_members_outside_the_view() {
|
||
let size = (640, 360);
|
||
let blue = || Appearance::basic(Paint::solid(Color::rgb(0.1, 0.3, 0.9)), Paint::None, 0.0);
|
||
// The two members, the point the view is centered on, and the pans after the first frame.
|
||
let cases = [
|
||
("whole", [Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(600.0, 0.0, 700.0, 100.0)], (220.0, 50.0), vec![-300.0]),
|
||
("cropped", [Rect::new(0.0, 0.0, 3000.0, 2000.0), Rect::new(3400.0, 0.0, 6400.0, 2000.0)], (3000.0, 1000.0), vec![-37.0, -237.0]),
|
||
];
|
||
let mut wrong = vec![];
|
||
for (name, members, (cx, cy), pans) in cases {
|
||
let mut d = Document::new(6400.0, 2000.0);
|
||
let members = members.iter().map(|r| std::sync::Arc::new(Node::path(d.alloc_id(), shapes::rectangle(*r), blue()))).collect();
|
||
let mut group = Node::group(d.alloc_id(), members);
|
||
group.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params: json!({"blur": 20, "x": 7, "y": 7}), visible: true });
|
||
let layer = d.layers[0].id;
|
||
d.insert(Some(layer), 0, group).unwrap();
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let at = |dx: f64| Affine::translate((320.0 + dx, 180.0)) * Affine::translate((-cx, -cy));
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&d, size.0, size.1, at(0.0), &opts);
|
||
for dx in pans {
|
||
let image = r.render(&d, size.0, size.1, at(dx), &opts);
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
if image.pixels != fresh.render(&d, size.0, size.1, at(dx), &opts).pixels {
|
||
wrong.push(format!("{name}: panned by {dx}"));
|
||
}
|
||
}
|
||
}
|
||
assert!(wrong.is_empty(), "{wrong:?}");
|
||
}
|
||
|
||
/// While interacting, a group with a member outside the view that only moved draws its shadow from the raster of the
|
||
/// frame before, where a fresh renderer draws it: the moved silhouette holds that member too.
|
||
#[test]
|
||
fn a_moved_group_with_a_member_outside_the_view_draws_its_shadow_kept() {
|
||
let size = (640, 360);
|
||
let blue = || Appearance::basic(Paint::solid(Color::rgb(0.1, 0.3, 0.9)), Paint::None, 0.0);
|
||
let mut base = Document::new(700.0, 100.0);
|
||
let members = [Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(600.0, 0.0, 700.0, 100.0)]
|
||
.iter()
|
||
.map(|r| std::sync::Arc::new(Node::path(base.alloc_id(), shapes::rectangle(*r), blue())))
|
||
.collect();
|
||
let mut group = Node::group(base.alloc_id(), members);
|
||
group.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params: json!({"blur": 20, "x": 7, "y": 7}), visible: true });
|
||
let (id, layer) = (group.id, base.layers[0].id);
|
||
base.insert(Some(layer), 0, group).unwrap();
|
||
let view = Affine::translate((100.0, 130.0));
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let live = RenderOptions { interacting: true, ..opts.clone() };
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&base, size.0, size.1, view, &opts);
|
||
let mut moved = base.clone();
|
||
moved.node_mut(id).unwrap().transform(Affine::translate((3.0, 2.0)), false);
|
||
let frame = r.render(&moved, size.0, size.1, view, &live);
|
||
assert!(r.stats.moved > 0, "drawn from the raster of the frame before");
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
assert!(frame.pixels == fresh.render(&moved, size.0, size.1, view, &opts).pixels);
|
||
}
|
||
|
||
/// A shadow made over an earlier cropped shadow covers only the rect the earlier one was made for. In a 2000 × 1200
|
||
/// view of the image's right edge, the pixel budget gives the earlier shadow, which also covers what the later one
|
||
/// reads 60 pixels below, a margin of 548 pixels and the later one 643: a pan of 600 pixels past the first makes both
|
||
/// again, and the frame draws as a fresh renderer draws it.
|
||
#[test]
|
||
fn a_shadow_over_a_cropped_shadow_keeps_to_the_earlier_ones_margin() {
|
||
let size = (2000, 1200);
|
||
let effects = [("stylize.dropShadow", json!({"blur": 20, "x": 7, "y": 7})), ("stylize.dropShadow", json!({"blur": 10, "x": -5, "y": -60}))];
|
||
let d = large_image(&effects);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let at = |dy: f64| Affine::translate((1000.0, 600.0 + dy)) * Affine::translate((-6000.0, -2000.0));
|
||
let mut r = Renderer::new();
|
||
r.threads = 2;
|
||
r.render(&d, size.0, size.1, at(0.0), &opts);
|
||
r.render(&d, size.0, size.1, at(37.0), &opts);
|
||
for dy in [37.0 - 600.0, 37.0 + 600.0] {
|
||
let image = r.render(&d, size.0, size.1, at(dy), &opts);
|
||
let mut fresh = Renderer::new();
|
||
fresh.threads = 2;
|
||
let still = fresh.render(&d, size.0, size.1, at(dy), &opts);
|
||
let worst = image.pixels.iter().zip(&still.pixels).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
|
||
assert_eq!(worst, 0, "panned to {dy}");
|
||
}
|
||
}
|
||
|
||
/// Time a drag of the square over each backdrop in a 2560 × 1440 view: the first frame renders as
|
||
/// before a drag, the next eight as a drag's frames (`interacting` and `dragging`, as the canvas
|
||
/// renders them), then the frame after the drag. The hashes of that frame and of a fresh renderer's
|
||
/// frame compare builds.
|
||
/// Run with `cargo test --release -p vectorcraft-render backdrop_drag_perf -- --ignored --nocapture`.
|
||
#[test]
|
||
#[ignore = "manual timing probe; wall-clock performance depends on machine load"]
|
||
fn backdrop_drag_perf() {
|
||
let hash = |px: &[u8]| {
|
||
use std::hash::{Hash, Hasher};
|
||
let mut h = std::hash::DefaultHasher::new();
|
||
px.hash(&mut h);
|
||
h.finish()
|
||
};
|
||
let size = (2560, 1440);
|
||
for (name, effects, zoom) in cases() {
|
||
let (base, id) = backdrop(&effects);
|
||
let xf = view(zoom, size);
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let live = RenderOptions { interacting: true, dragging: true, ..opts.clone() };
|
||
let mut r = Renderer::new();
|
||
r.threads = crate::default_threads();
|
||
let start = std::time::Instant::now();
|
||
std::hint::black_box(r.render(&base, size.0, size.1, xf, &opts));
|
||
let first = start.elapsed().as_secs_f64() * 1000.0;
|
||
let (mut ms, mut previewed, mut doc) = (vec![], 0, base.clone());
|
||
for step in 1..9 {
|
||
doc = moved(&base, id, step, zoom);
|
||
let start = std::time::Instant::now();
|
||
std::hint::black_box(r.render(&doc, size.0, size.1, xf, &live));
|
||
ms.push(start.elapsed().as_secs_f64() * 1000.0);
|
||
previewed += r.stats.previewed;
|
||
}
|
||
let start = std::time::Instant::now();
|
||
let still = r.render(&doc, size.0, size.1, xf, &opts);
|
||
let after = start.elapsed().as_secs_f64() * 1000.0;
|
||
let fresh = Renderer::new().render(&doc, size.0, size.1, xf, &opts);
|
||
ms.sort_by(f64::total_cmp);
|
||
println!(
|
||
"{name} at {:.0}%: first frame {first:.1} ms; drag frames median {:.1} ms (min {:.1}, max {:.1}), {previewed} previewed; the frame after the drag {after:.1} ms; hashes {:016x} {:016x}",
|
||
zoom * 100.0,
|
||
(ms[3] + ms[4]) * 0.5,
|
||
ms[0],
|
||
ms[7],
|
||
hash(&still.pixels),
|
||
hash(&fresh.pixels)
|
||
);
|
||
}
|
||
}
|