mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Renderer: character strokes are expanded once, not every frame
The outlines of a run's character stroke are kept with the glyph geometry the renderer keeps for the type, in a byte-limited cache swept by frame, so type that changes in every frame (an animation) frees them within a few frames.
This commit is contained in:
@@ -2396,7 +2396,7 @@ impl Renderer {
|
||||
if st.brush.is_some() && self.draw_brush(ctx, f, n, ig, st) {
|
||||
return;
|
||||
}
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, ig, rule, st, ib);
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, ig, rule, st, None, ib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
-35
@@ -395,6 +395,10 @@ impl std::hash::Hasher for PtrHasher {
|
||||
|
||||
type PtrMap<K, V> = HashMap<K, V, std::hash::BuildHasherDefault<PtrHasher>>;
|
||||
|
||||
/// The key of a cached stroke outline: ([`Renderer::stroke_owner`] of what owns the stroke, the
|
||||
/// stroke's place in it, width bits, tolerance level).
|
||||
type StrokeKey = (usize, usize, u64, i32);
|
||||
|
||||
/// A stroke expanded to a fill outline: (owning node, outline, last frame used).
|
||||
struct StrokeEntry {
|
||||
node: Arc<Node>,
|
||||
@@ -470,7 +474,13 @@ pub struct Renderer {
|
||||
plugin_revision: u64,
|
||||
/// Expanded stroke outlines keyed by ([`Self::stroke_owner`], stroke layer address, width bits,
|
||||
/// tolerance level): panning re-renders reuse them instead of re-expanding every stroke.
|
||||
strokes: PtrMap<(usize, usize, u64, i32), StrokeEntry>,
|
||||
strokes: PtrMap<StrokeKey, StrokeEntry>,
|
||||
/// Expanded character stroke outlines, with the glyph geometry kept in `texts` they belong to,
|
||||
/// keyed by ([`Self::stroke_owner`] of that geometry, run, width bits, tolerance level): type
|
||||
/// that doesn't change draws them again instead of expanding its strokes in every frame. Within
|
||||
/// a byte budget, and swept by frame, so the outlines of type that changes in every frame go
|
||||
/// within a few frames.
|
||||
char_strokes: FrameCache<StrokeKey, (Arc<TextGeom>, Arc<BezPath>)>,
|
||||
/// The node being drawn on the plain-path fast path, or the path of brush art being drawn
|
||||
/// ([`Self::draw_brush_pieces`]): the owner of cached stroke outlines.
|
||||
cur: Option<Arc<Node>>,
|
||||
@@ -648,6 +658,7 @@ impl Renderer {
|
||||
symbols: vec![],
|
||||
plugin_revision: 0,
|
||||
strokes: PtrMap::default(),
|
||||
char_strokes: FrameCache::new(lru::CHAR_STROKE_BUDGET, 0),
|
||||
cur: None,
|
||||
made_again: false,
|
||||
fx_owner: None,
|
||||
@@ -789,6 +800,7 @@ impl Renderer {
|
||||
// Filtered rasters are large: only those of the last frame stay.
|
||||
self.pixel_fx.retain(|_, e| e.stamp == g);
|
||||
self.texts.sweep(g);
|
||||
self.char_strokes.sweep(g);
|
||||
self.clips.sweep(g);
|
||||
self.clip_paints.sweep(g);
|
||||
self.fx_arts.sweep(g);
|
||||
@@ -1173,11 +1185,11 @@ impl Renderer {
|
||||
&& !fx::has_fx(a)
|
||||
{
|
||||
let t = &*f.doc.inline_resolved(t);
|
||||
let g = match f.text_snap(t) {
|
||||
Some(xf) => Arc::new(text_geom_snapped(t, Some(xf))),
|
||||
let (g, kept) = match f.text_snap(t) {
|
||||
Some(xf) => (Arc::new(text_geom_snapped(t, Some(xf))), false),
|
||||
None => self.text_geom_of(a, t),
|
||||
};
|
||||
self.draw_text_geom(ctx, f, a, t, &g);
|
||||
self.draw_text_geom(ctx, f, a, t, &g, kept);
|
||||
self.stats.drawn += 1;
|
||||
return;
|
||||
}
|
||||
@@ -1609,7 +1621,7 @@ impl Renderer {
|
||||
if st.brush.is_some() && self.draw_brush(ctx, f, n, bp, st) {
|
||||
continue;
|
||||
}
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, bp, rule, st, bounds);
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, bp, rule, st, None, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1626,8 +1638,20 @@ impl Renderer {
|
||||
|
||||
/// Paint stroke `st` of the shape `bp` (in the space `xf` maps to the document; `bounds`: its
|
||||
/// geometric bounds there) with its alignment, dashes, profile, arrowheads, opacity and blend.
|
||||
/// `chars` is the glyph geometry kept in `texts` and the run that `st` is the character stroke
|
||||
/// of, when it is one.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_stroke(&mut self, ctx: &mut RenderContext, f: &Frame, xf: Affine, bp: &BezPath, rule: FillRule, st: &StrokeLayer, bounds: Rect) {
|
||||
fn draw_stroke(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
xf: Affine,
|
||||
bp: &BezPath,
|
||||
rule: FillRule,
|
||||
st: &StrokeLayer,
|
||||
chars: Option<(&Arc<TextGeom>, usize)>,
|
||||
bounds: Rect,
|
||||
) {
|
||||
let pieces = effects::stroke::stroke_pieces(bp, st);
|
||||
// The line and its heads overlap: they take the opacity once, in one layer, instead of
|
||||
// each folding it into its paint.
|
||||
@@ -1650,11 +1674,11 @@ impl Renderer {
|
||||
// One paint box for the line and the heads, so a gradient runs on into the heads.
|
||||
let paint_bounds = bounds.inflate(st.width / 2.0, st.width / 2.0);
|
||||
if let Some(g) = st.path_gradient() {
|
||||
self.fill_path_gradient(ctx, f, bp, rule, st, &pieces, width, &g.gradient);
|
||||
self.fill_path_gradient(ctx, f, bp, rule, st, chars, &pieces, width, &g.gradient);
|
||||
} else if paint::set_paint(ctx, &st.paint, paint_bounds, f) {
|
||||
self.fold_alpha(ctx, f.ink, &st.paint);
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
match self.cached_stroke(f, &pieces.line, st, width) {
|
||||
match self.cached_stroke(f, chars, &pieces.line, st, width) {
|
||||
Some(o) => ctx.fill_path(&o),
|
||||
None => ctx.fill_path(&effects::stroke::line_outline(&pieces.line, st, width, f.px * 0.25)),
|
||||
}
|
||||
@@ -1693,6 +1717,7 @@ impl Renderer {
|
||||
bp: &BezPath,
|
||||
rule: FillRule,
|
||||
st: &StrokeLayer,
|
||||
chars: Option<(&Arc<TextGeom>, usize)>,
|
||||
pieces: &effects::stroke::StrokePieces,
|
||||
width: f64,
|
||||
g: &vectorcraft_color::Gradient,
|
||||
@@ -1714,7 +1739,7 @@ impl Renderer {
|
||||
};
|
||||
let slices = match self.cur.clone().filter(|n| owns_stroke(n, st)) {
|
||||
Some(node) => {
|
||||
let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, level);
|
||||
let key = (self.stroke_owner(Arc::as_ptr(&node) as usize), st as *const StrokeLayer as usize, level);
|
||||
match self.stroke_slices.get_mut(&key) {
|
||||
Some(e) if Arc::ptr_eq(&e.node, &node) => {
|
||||
e.stamp = self.stamp;
|
||||
@@ -1729,7 +1754,7 @@ impl Renderer {
|
||||
}
|
||||
None => make(),
|
||||
};
|
||||
let line = match self.cached_stroke(f, &pieces.line, st, width) {
|
||||
let line = match self.cached_stroke(f, chars, &pieces.line, st, width) {
|
||||
Some(o) => o,
|
||||
None => Arc::new(effects::stroke::line_outline(&pieces.line, st, width, f.px * 0.25)),
|
||||
};
|
||||
@@ -1751,30 +1776,51 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// The line part of a stroke of `self.cur` (see [`effects::stroke::line_outline`]), cached
|
||||
/// across frames under [`Self::stroke_owner`] and the stroke layer. The tolerance (vello's 0.25
|
||||
/// device px) is bucketed to powers of two, never coarser than needed, so zooming reuses
|
||||
/// outlines until the level changes. Only `self.cur`'s own strokes are cached: for other
|
||||
/// strokes, this returns `None`.
|
||||
fn cached_stroke(&mut self, f: &Frame, line: &BezPath, st: &StrokeLayer, width: f64) -> Option<Arc<BezPath>> {
|
||||
let node = self.cur.clone().filter(|n| owns_stroke(n, st))?;
|
||||
/// The line part of a stroke (see [`effects::stroke::line_outline`]), cached across frames: a
|
||||
/// character stroke of run `chars.1` of kept glyph geometry `chars.0` in `char_strokes`, a
|
||||
/// stroke of `self.cur` in `strokes` (under [`Self::stroke_owner`] and the stroke layer). The
|
||||
/// tolerance (vello's 0.25 device px) is bucketed to powers of two, never coarser than needed,
|
||||
/// so zooming reuses outlines until the level changes. Other strokes aren't cached: for them
|
||||
/// this returns `None`.
|
||||
fn cached_stroke(
|
||||
&mut self,
|
||||
f: &Frame,
|
||||
chars: Option<(&Arc<TextGeom>, usize)>,
|
||||
line: &BezPath,
|
||||
st: &StrokeLayer,
|
||||
width: f64,
|
||||
) -> Option<Arc<BezPath>> {
|
||||
let level = (f.px * 0.25).log2().floor() as i32;
|
||||
let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, width.to_bits(), level);
|
||||
let expand = || Arc::new(effects::stroke::line_outline(line, st, width, 2f64.powi(level)));
|
||||
if let Some((g, run)) = chars {
|
||||
let key = (self.stroke_owner(Arc::as_ptr(g) as usize), run, width.to_bits(), level);
|
||||
if let Some((kept, outline)) = self.char_strokes.get(&key, self.stamp)
|
||||
&& Arc::ptr_eq(kept, g)
|
||||
{
|
||||
return Some(outline.clone());
|
||||
}
|
||||
let outline = expand();
|
||||
self.char_strokes.insert(key, (g.clone(), outline.clone()), lru::path_bytes(&outline), self.stamp);
|
||||
return Some(outline);
|
||||
}
|
||||
let node = self.cur.clone().filter(|n| owns_stroke(n, st))?;
|
||||
let key = (self.stroke_owner(Arc::as_ptr(&node) as usize), st as *const StrokeLayer as usize, width.to_bits(), level);
|
||||
if let Some(e) = self.strokes.get_mut(&key)
|
||||
&& Arc::ptr_eq(&e.node, &node)
|
||||
{
|
||||
e.stamp = self.stamp;
|
||||
return Some(e.outline.clone());
|
||||
}
|
||||
let outline = Arc::new(effects::stroke::line_outline(line, st, width, 2f64.powi(level)));
|
||||
let outline = expand();
|
||||
self.strokes.insert(key, StrokeEntry { node, outline: outline.clone(), stamp: self.stamp });
|
||||
Some(outline)
|
||||
}
|
||||
|
||||
/// What the `strokes` and `stroke_slices` keys hold for `node` (`self.cur`): its address,
|
||||
/// or 0 while art made again in every frame is drawn ([`Self::made_again`]).
|
||||
fn stroke_owner(&self, node: &Arc<Node>) -> usize {
|
||||
if self.made_again { 0 } else { Arc::as_ptr(node) as usize }
|
||||
/// What the `strokes`, `char_strokes` and `stroke_slices` keys hold for a stroke owner at
|
||||
/// `address` (`self.cur` or kept glyph geometry): the address, or 0 while art made again in
|
||||
/// every frame is drawn ([`Self::made_again`]).
|
||||
fn stroke_owner(&self, address: usize) -> usize {
|
||||
if self.made_again { 0 } else { address }
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
|
||||
@@ -1783,11 +1829,11 @@ impl Renderer {
|
||||
// keeps, see `draw_fx_art`) takes its glyph geometry from `texts`, as on the fast path,
|
||||
// except in frames that snap type to pixels.
|
||||
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned();
|
||||
let g = match (f.text_snap(t), owner) {
|
||||
let (g, kept) = match (f.text_snap(t), owner) {
|
||||
(None, Some(a)) => self.text_geom_of(&a, t),
|
||||
(snap, _) => Arc::new(text_geom_snapped(t, snap)),
|
||||
(snap, _) => (Arc::new(text_geom_snapped(t, snap)), false),
|
||||
};
|
||||
self.draw_text_geom(ctx, f, n, t, &g);
|
||||
self.draw_text_geom(ctx, f, n, t, &g, kept);
|
||||
}
|
||||
|
||||
/// Draw the inline graphics of type `t` (laid out in `g`): each symbol's art through the
|
||||
@@ -1854,28 +1900,32 @@ impl Renderer {
|
||||
|
||||
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
|
||||
/// Text `t` that is not the node's own, such as the copy whose inline graphics
|
||||
/// `Document::inline_resolved` sized again, is laid out without the cache.
|
||||
fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> Arc<TextGeom> {
|
||||
/// `Document::inline_resolved` sized again, is laid out without the cache. Also returns
|
||||
/// whether the geometry is kept in the cache.
|
||||
fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> (Arc<TextGeom>, bool) {
|
||||
if !matches!(&a.kind, NodeKind::Text(own) if std::ptr::eq(&**own, t)) {
|
||||
return Arc::new(text_geom(t));
|
||||
return (Arc::new(text_geom(t)), false);
|
||||
}
|
||||
let fonts = vectorcraft_text::FontDb::global().generation();
|
||||
if fonts != self.text_fonts {
|
||||
self.texts.clear();
|
||||
self.char_strokes.clear();
|
||||
self.text_fonts = fonts;
|
||||
}
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
if let Some((node, g)) = self.texts.get(&key, self.stamp)
|
||||
&& Arc::ptr_eq(node, a)
|
||||
{
|
||||
return g.clone();
|
||||
return (g.clone(), true);
|
||||
}
|
||||
let g = Arc::new(text_geom(t));
|
||||
self.texts.insert(key, (a.clone(), g.clone()), g.bytes(), self.stamp);
|
||||
g
|
||||
let kept = self.texts.insert(key, (a.clone(), g.clone()), g.bytes(), self.stamp);
|
||||
(g, kept)
|
||||
}
|
||||
|
||||
fn draw_text_geom(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &TextGeom) {
|
||||
/// Draw type `n` (`t`, laid out in `g`). The outlines of its character strokes are cached
|
||||
/// when `g` is kept in `texts` (`kept`).
|
||||
fn draw_text_geom(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &Arc<TextGeom>, kept: bool) {
|
||||
let xf = f.view * t.xf;
|
||||
if f.opts.outline {
|
||||
let mut p = g.all.clone();
|
||||
@@ -1958,7 +2008,8 @@ impl Renderer {
|
||||
if run.style.has_stroke() {
|
||||
// Character strokes are drawn in text space, with their cap, join and dashes.
|
||||
overprint(ctx, run.style.overprint_stroke);
|
||||
self.draw_stroke(ctx, f, t.xf, path, FillRule::NonZero, &run.style.stroke_layer(), g.bounds);
|
||||
let chars = kept.then_some((g, i));
|
||||
self.draw_stroke(ctx, f, t.xf, path, FillRule::NonZero, &run.style.stroke_layer(), chars, g.bounds);
|
||||
}
|
||||
}
|
||||
overprint(ctx, false);
|
||||
@@ -1991,7 +2042,7 @@ impl Renderer {
|
||||
}
|
||||
AppearanceItem::Stroke(st) if st.width > 0.0 => {
|
||||
if st.brush.is_none() || !self.draw_brush(ctx, f, n, all, st) {
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, all, FillRule::NonZero, st, tb);
|
||||
self.draw_stroke(ctx, f, Affine::IDENTITY, all, FillRule::NonZero, st, None, tb);
|
||||
}
|
||||
}
|
||||
AppearanceItem::Stroke(_) => {}
|
||||
|
||||
@@ -25,6 +25,9 @@ use crate::PtrHasher;
|
||||
|
||||
/// Glyph outlines per text object (`Renderer::text_geom_of`).
|
||||
pub(crate) const TEXT_BUDGET: usize = 128 << 20;
|
||||
/// Expanded character strokes per run of the glyph outlines kept in `texts`
|
||||
/// (`Renderer::cached_stroke`).
|
||||
pub(crate) const CHAR_STROKE_BUDGET: usize = 128 << 20;
|
||||
/// Clip regions per clipping path (`Renderer::clip_of`), and separately what clipping paths
|
||||
/// paint (`Renderer::clip_paint_of`).
|
||||
pub(crate) const CLIP_BUDGET: usize = 32 << 20;
|
||||
@@ -176,6 +179,12 @@ impl<K: Hash + Eq + Clone, V, S: BuildHasher + Default> FrameCache<K, V, S> {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
/// The values of the entries frame `stamp` made or used.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn used_in(&self, stamp: u64) -> impl Iterator<Item = &V> {
|
||||
self.map.values().filter(move |s| s.stamp == stamp).map(|s| &s.value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
|
||||
@@ -7,6 +7,7 @@ use vectorcraft_doc::{AppearanceItem, Dash, LineJoin, Node, NodeKind};
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use super::*;
|
||||
use crate::tests_fontchange::fonts_steady;
|
||||
|
||||
const SIZE: u32 = 200;
|
||||
|
||||
@@ -103,3 +104,86 @@ fn aligned_character_strokes_keep_to_their_side_of_the_glyphs() {
|
||||
assert!(!dark(&outside, mid.0, mid.1), "an outside stroke leaves the glyph itself unpainted");
|
||||
assert!(dark(&inside, mid.0, mid.1), "an inside stroke paints into the glyph");
|
||||
}
|
||||
|
||||
/// A document with 60 pt `text` without a fill and with a 1 pt character stroke, and the type's id.
|
||||
fn stroked_text_doc(text: &str) -> (Document, NodeId) {
|
||||
let mut d = Document::new(SIZE as f64, SIZE as f64);
|
||||
let style = CharStyle { size: 60.0, fill: Paint::None, stroke: Paint::solid(Color::BLACK), stroke_width: 1.0, ..Default::default() };
|
||||
let mut n = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), text, style))));
|
||||
n.id = d.alloc_id();
|
||||
let id = n.id;
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
(d, id)
|
||||
}
|
||||
|
||||
/// The character stroke outlines that `r` keeps and that its last frame made or used.
|
||||
fn character_outlines(r: &Renderer) -> Vec<Arc<BezPath>> {
|
||||
r.char_strokes.used_in(r.stamp).map(|(_, outline)| outline.clone()).collect()
|
||||
}
|
||||
|
||||
/// The outlines of type that changes in every frame (an animation) go within a few frames: the
|
||||
/// renderer keeps no more than the last few frames' strokes, however long it runs.
|
||||
#[test]
|
||||
fn the_character_strokes_of_type_that_changes_in_every_frame_go() {
|
||||
let _fonts = fonts_steady();
|
||||
let (mut d, id) = stroked_text_doc("Type");
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
for i in 0..40 {
|
||||
if let Some(Node { kind: NodeKind::Text(t), .. }) = d.node_mut(id) {
|
||||
t.xf = Affine::translate((f64::from(i), 0.0));
|
||||
}
|
||||
r.render(&d, SIZE, SIZE, Affine::IDENTITY, &opts);
|
||||
}
|
||||
assert!(r.char_strokes.len() <= 4, "{} outlines kept", r.char_strokes.len());
|
||||
}
|
||||
|
||||
/// A character stroke is expanded once: the next frame of the same view draws the outline the
|
||||
/// first made. A change of the zoom level or of the text expands it again.
|
||||
#[test]
|
||||
fn character_stroke_outlines_are_kept_between_frames() {
|
||||
let _fonts = fonts_steady();
|
||||
let (mut d, id) = stroked_text_doc("Type");
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
r.render(&d, SIZE, SIZE, Affine::IDENTITY, &opts);
|
||||
let first = character_outlines(&r);
|
||||
assert_eq!(first.len(), 1, "the outline of the one run's stroke");
|
||||
r.render(&d, SIZE, SIZE, Affine::IDENTITY, &opts);
|
||||
let again = character_outlines(&r);
|
||||
assert!(again.len() == 1 && Arc::ptr_eq(&again[0], &first[0]), "the second frame draws the outline the first made");
|
||||
r.render(&d, SIZE, SIZE, Affine::translate((-40.0, -300.0)) * Affine::scale(4.0), &opts);
|
||||
let zoomed = character_outlines(&r);
|
||||
assert!(zoomed.len() == 1 && !Arc::ptr_eq(&zoomed[0], &first[0]), "zoomed in 4×, it is expanded again");
|
||||
if let Some(Node { kind: NodeKind::Text(t), .. }) = d.node_mut(id) {
|
||||
t.runs[0].text = "Typo".into();
|
||||
}
|
||||
r.render(&d, SIZE, SIZE, Affine::IDENTITY, &opts);
|
||||
let changed = character_outlines(&r);
|
||||
assert!(changed.len() == 1 && !Arc::ptr_eq(&changed[0], &first[0]), "the changed text is expanded again");
|
||||
}
|
||||
|
||||
/// Frames that draw character strokes from the cache draw the same pixels, and what a renderer
|
||||
/// draws whose glyph cache has no room, so that it expands them in every frame: exactly at 100%,
|
||||
/// where both take a tolerance of 1/4 device pixel, and within antialiasing at 105% and 150%,
|
||||
/// where the cache takes 1/8 device pixel and the frames without it about 1/4 and 1/6.
|
||||
#[test]
|
||||
fn cached_character_strokes_draw_what_uncached_ones_draw() {
|
||||
let _fonts = fonts_steady();
|
||||
let (d, _) = stroked_text_doc("Type");
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
for (zoom, most) in [(1.0, 0), (1.05, 24), (1.5, 24)] {
|
||||
let view = Affine::scale(zoom);
|
||||
let mut r = Renderer::new();
|
||||
let first = r.render(&d, SIZE, SIZE, view, &opts).pixels;
|
||||
assert!(r.render(&d, SIZE, SIZE, view, &opts).pixels == first, "{zoom}×: the cached frames match");
|
||||
let mut uncached = Renderer::new();
|
||||
uncached.texts = FrameCache::new(0, 0);
|
||||
let plain = uncached.render(&d, SIZE, SIZE, view, &opts).pixels;
|
||||
assert!(character_outlines(&uncached).is_empty());
|
||||
let diff = first.iter().zip(&plain).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
|
||||
assert!(diff <= most, "{zoom}×: cached and uncached frames differ by {diff}");
|
||||
assert!(plain.iter().any(|&c| c < 128), "{zoom}×: the strokes show");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user