- geom.rs, RS1: the levels and the closed forms in i64, and the 32-bit quotient-remainder identity behind cfg(test, kani) and the i32-forms feature; - cover.rs, RS2-RS6: the row split, the cell split walked left to right with the columns left of the canvas folded into one contribution per row piece, the delta buffer over the fill's canvas-clipped box, the scan, the quantisers and the run's glyph-wise sum; a debug build asserts D_r <= 256 p <= 2^22 - 256 before accumulating; - paint.rs, RS7-RS9: p' = div255(p o), then div255(p' alpha) and source-over; the group pop div255(m o_g) then div255(G k); RGBA8S; - frame.rs: R(P, S, z, r, g), with a run's outlines placed and flattened once per glyph ID; render_request's size_unlisted, rate_unlisted and frame_range. Vectors: flatten (levels and vertices), fills, runs (a set drawn here from formulas), composite (with RGBA8S), motion, clock and slicing, each frame's SHA-256 and each (size, rate)'s clip digest in its expected file and in sha256.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
199 lines
8.9 KiB
Rust
199 lines
8.9 KiB
Rust
//! The frame procedure `R(P, S, z, r, g)` (§3.10): a loaded payload, a listed size
|
|
//! and rate, and a frame in range give the bytes of `RGBA8P` (§2.9).
|
|
//!
|
|
//! A run's glyph outlines are placed and flattened once per glyph ID, then moved to
|
|
//! each record's origin: placement offsets do not depend on the origin, and the
|
|
//! flattened vertices of a translated curve are the translated vertices (§3.11 lets an
|
|
//! implementation cache what the definition makes equal).
|
|
|
|
use alloc::collections::BTreeMap;
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::bounds::NONE;
|
|
use crate::clock;
|
|
use crate::cover::{self, Cov, RunSum};
|
|
use crate::geom::{self, Pt};
|
|
use crate::paint::{self, Target};
|
|
use crate::place::{SLOT_DEFAULT, linear_map, origin, point_offset};
|
|
use crate::refuse::{Code, Refusal, refuse};
|
|
use crate::track;
|
|
use crate::wos1::Point;
|
|
use crate::wvg1::{Item, KIND_BEGIN, KIND_END, KIND_PATH, KIND_RUN, Loaded, M00, M01, M10, M11, OP, TX, TY};
|
|
|
|
/// The value of an item's slot at tick `tau`: its track's value, or the slot's default.
|
|
fn slot(l: &Loaded, it: &Item, s: usize, tau: i64) -> i64 {
|
|
match it.slots[s] {
|
|
NONE => SLOT_DEFAULT[s],
|
|
t => track::eval(l.doc.track_keys(t as usize), &l.doc.tables, tau),
|
|
}
|
|
}
|
|
|
|
/// An outline's contours, placed: each point at `t + offset(L, p)`, flattened (RS1).
|
|
fn place_outline(pts: &[Point], ends: &[u16], l: [i64; 4], t: Pt, out: &mut Vec<Vec<Pt>>) {
|
|
let mut start = 0usize;
|
|
let mut placed: Vec<(Pt, u8)> = Vec::new();
|
|
for &e in ends {
|
|
let end = e as usize + 1;
|
|
placed.clear();
|
|
for p in &pts[start..end] {
|
|
let o = point_offset(l, [i64::from(p.x), i64::from(p.y)]);
|
|
placed.push(([t[0] + o[0], t[1] + o[1]], p.tag));
|
|
}
|
|
let mut poly = Vec::new();
|
|
geom::flatten_contour(&placed, &mut poly);
|
|
out.push(poly);
|
|
start = end;
|
|
}
|
|
}
|
|
|
|
/// The items' matching group ends.
|
|
fn group_ends(items: &[Item]) -> Vec<usize> {
|
|
let mut m = alloc::vec![0usize; items.len()];
|
|
let mut stack = Vec::new();
|
|
for (i, it) in items.iter().enumerate() {
|
|
match it.kind {
|
|
KIND_BEGIN => stack.push(i),
|
|
KIND_END => {
|
|
if let Some(b) = stack.pop() {
|
|
m[b] = i;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
m
|
|
}
|
|
|
|
/// A path fill's or a clip's values: the path placed with matrix `m`, origin sum `o`
|
|
/// and `unit_scale` `s`, at width `w`.
|
|
fn path_cov(l: &Loaded, path: usize, m: [i64; 4], o: Pt, s: i64, w: i64, h: i64) -> Cov {
|
|
let d = &l.doc;
|
|
let dw = i64::from(d.header.design_w);
|
|
let lm = linear_map(m, s, w, dw);
|
|
let t = origin(o, m, [0, 0], w, dw);
|
|
let mut polys = Vec::new();
|
|
place_outline(d.path_points_of(path), d.path_ends_of(path), lm, t, &mut polys);
|
|
cover::accumulate(&polys, w, h, u64::from(d.paths[path].n_points))
|
|
}
|
|
|
|
/// `R(P, S, z, r, g)` for size index `z`, rate index `r` and frame `g`, which must be in
|
|
/// range (`frame_range` otherwise).
|
|
pub fn render(l: &Loaded, z: usize, r: usize, g: u64) -> Result<Vec<u8>, Refusal> {
|
|
let d = &l.doc;
|
|
let p = d.rates[r].ticks_per_frame(d.header.tb);
|
|
if !clock::in_range(g, d.header.t0, d.header.dur, p) {
|
|
return refuse(Code::FrameRange, "render", "frame out of range", g);
|
|
}
|
|
// 1. τ = g·P.
|
|
let tau = clock::tick(g, p);
|
|
let (w, h) = (i64::from(d.sizes[z].w), i64::from(d.sizes[z].h));
|
|
let dw = i64::from(d.header.design_w);
|
|
let ends = group_ends(&d.items);
|
|
// 2. The root, cleared; a stack of (target, the group's item, its origin sum).
|
|
let mut stack: Vec<(Target, usize, Pt)> = Vec::new();
|
|
let mut root = Target::clear(w as usize, h as usize);
|
|
let mut o_sum: Pt = [0, 0];
|
|
let active = |it: &Item| i64::from(it.t_on) <= tau && tau < i64::from(it.t_off);
|
|
// 3. Each item in order.
|
|
let mut i = 0;
|
|
while i < d.items.len() {
|
|
let it = &d.items[i];
|
|
match it.kind {
|
|
KIND_BEGIN => {
|
|
if !active(it) {
|
|
i = ends[i] + 1;
|
|
continue;
|
|
}
|
|
let og = [o_sum[0] + slot(l, it, TX, tau), o_sum[1] + slot(l, it, TY, tau)];
|
|
stack.push((Target::clear(w as usize, h as usize), i, o_sum));
|
|
o_sum = og;
|
|
}
|
|
KIND_END => {
|
|
let (gt, gi, parent_o) = stack.pop().expect("groups balance");
|
|
let git = &d.items[gi];
|
|
let o_g = slot(l, git, OP, tau) as u8;
|
|
let target = match stack.last_mut() {
|
|
Some(t) => &mut t.0,
|
|
None => &mut root,
|
|
};
|
|
if git.reference == NONE {
|
|
paint::group_pop(target, >, o_g, |_, _| 255);
|
|
} else {
|
|
let cov = path_cov(l, git.reference as usize, crate::place::IDENTITY, o_sum, i64::from(git.unit_scale), w, h);
|
|
let rule = git.rule;
|
|
paint::group_pop(target, >, o_g, |x, y| cover::alpha(cov.at(x as i64, y as i64), rule));
|
|
}
|
|
o_sum = parent_o;
|
|
}
|
|
KIND_PATH | KIND_RUN if active(it) => {
|
|
let m = [slot(l, it, M00, tau), slot(l, it, M01, tau), slot(l, it, M10, tau), slot(l, it, M11, tau)];
|
|
let o = [o_sum[0] + slot(l, it, TX, tau), o_sum[1] + slot(l, it, TY, tau)];
|
|
let s = i64::from(it.unit_scale);
|
|
let paint = paint::paint_opacity(d.paints[it.paint as usize].rgba, slot(l, it, OP, tau) as u8);
|
|
let target = match stack.last_mut() {
|
|
Some(t) => &mut t.0,
|
|
None => &mut root,
|
|
};
|
|
if it.kind == KIND_PATH {
|
|
let cov = path_cov(l, it.reference as usize, m, o, s, w, h);
|
|
let rule = it.rule;
|
|
paint::composite(target, paint, cov.c0, cov.r0, cov.bw, cov.bh, |c, r| cover::alpha(cov.v[r * cov.bw + c], rule));
|
|
} else {
|
|
let set = &l.sets[it.reference as usize];
|
|
let lm = linear_map(m, s, w, dw);
|
|
// Each glyph ID placed at origin (0, 0) and flattened once.
|
|
let mut shapes: BTreeMap<u16, (Vec<Vec<Pt>>, u64)> = BTreeMap::new();
|
|
let mut placed: Vec<(u16, Pt)> = Vec::new();
|
|
for gr in d.run_glyphs(i) {
|
|
let t = origin(o, m, [i64::from(gr.x), i64::from(gr.y)], w, dw);
|
|
placed.push((gr.gid, t));
|
|
shapes.entry(gr.gid).or_insert_with(|| {
|
|
let gl = set.glyph(gr.gid).expect("step 7");
|
|
let mut polys = Vec::new();
|
|
place_outline(set.glyph_points(gl), set.glyph_ends(gl), lm, [0, 0], &mut polys);
|
|
(polys, u64::from(gl.n_points))
|
|
});
|
|
}
|
|
// Each record's fill, and the run's box.
|
|
let mut covs: Vec<Cov> = Vec::with_capacity(placed.len());
|
|
for (gid, t) in &placed {
|
|
let (polys, n) = &shapes[gid];
|
|
let moved: Vec<Vec<Pt>> = polys.iter().map(|p| p.iter().map(|q| [q[0] + t[0], q[1] + t[1]]).collect()).collect();
|
|
let c = cover::accumulate(&moved, w, h, *n);
|
|
if c.bw > 0 && c.bh > 0 {
|
|
covs.push(c);
|
|
}
|
|
}
|
|
if !covs.is_empty() {
|
|
let c0 = covs.iter().map(|c| c.c0).min().unwrap_or(0);
|
|
let r0 = covs.iter().map(|c| c.r0).min().unwrap_or(0);
|
|
let c1 = covs.iter().map(|c| c.c0 + c.bw as i64).max().unwrap_or(0);
|
|
let r1 = covs.iter().map(|c| c.r0 + c.bh as i64).max().unwrap_or(0);
|
|
let mut sum = RunSum::new(c0, r0, (c1 - c0) as usize, (r1 - r0) as usize);
|
|
for c in &covs {
|
|
sum.add(c);
|
|
}
|
|
paint::composite(target, paint, sum.c0, sum.r0, sum.bw, sum.bh, |c, r| crate::arith::alpha_sum(sum.s[r * sum.bw + c]));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
i += 1;
|
|
}
|
|
// 4. The root is the frame's RGBA8P.
|
|
Ok(root.px)
|
|
}
|
|
|
|
/// A render request by size, rate and frame: `size_unlisted`, `rate_unlisted` and
|
|
/// `frame_range` are request errors, not refusals of the payload (§2.7, *Render*).
|
|
pub fn render_request(l: &Loaded, w: u16, h: u16, num: u32, den: u32, g: u64) -> Result<Vec<u8>, Refusal> {
|
|
let d = &l.doc;
|
|
let Some(z) = d.sizes.iter().position(|s| s.w == w && s.h == h) else {
|
|
return refuse(Code::SizeUnlisted, "render", "no SIZE lists it", 0);
|
|
};
|
|
let Some(r) = d.rates.iter().position(|q| q.num == num && q.den == den) else {
|
|
return refuse(Code::RateUnlisted, "render", "no RATE lists it", 0);
|
|
};
|
|
render(l, z, r, g)
|
|
}
|