Geom: stars stretched by the standard's hf/vf factors

Five-, six-, seven- and ten-pointed stars used their own normalisation
and came out narrower or shifted against PowerPoint's. ECMA-376 places a
star's points on an ellipse around (hc, vc * vf) with radii half the box
times hf / vf (star5 1.05146 / 1.10557, star6 1.15470, star7 1.02572 /
1.05210, star10 1.05146), so the points touch the box's edges; now done
for all stars (checked exactly against PowerPoint's outlines at three
sizes and inner-radius adjusts).

Wild synthetic set: star galleries 4.08 -> 3.76, 3.77 -> 3.45,
4.62 -> 4.31 and the adjust galleries slightly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 07:22:32 -04:00
parent 5d5fa7d1dc
commit 3ec12412e0
1 changed files with 35 additions and 5 deletions

View File

@ -520,14 +520,25 @@ fn normalize(pts: &[(f64, f64)], w: f64, h: f64) -> Vec<(f64, f64)> {
pts.iter().map(|(x, y)| ((x - x0) * sx, (y - y0) * sy)).collect()
}
fn star(n: usize, w: f64, h: f64, inner: f64, normalized: bool) -> Vec<(f64, f64)> {
/// An n-pointed star as ECMA-376 draws it: points on an ellipse around (hc, vc · vf) with radii
/// half the box stretched by the preset's `hf` / `vf` (so a five-pointed star fills its box and
/// looks regular in a square), the inner corners `inner` of the way out.
fn star(n: usize, w: f64, h: f64, inner: f64) -> Vec<(f64, f64)> {
let (hf, vf) = match n {
5 => (1.05146, 1.10557),
6 => (1.15470, 1.0),
7 => (1.02572, 1.05210),
10 => (1.05146, 1.0),
_ => (1.0, 1.0),
};
let (rx, ry, cy) = (w / 2.0 * hf, h / 2.0 * vf, h / 2.0 * vf);
let mut pts = Vec::with_capacity(n * 2);
for i in 0..n * 2 {
let a = (-90.0 + 180.0 * i as f64 / n as f64).to_radians();
let r = if i % 2 == 0 { 1.0 } else { inner };
pts.push((r * a.cos(), r * a.sin()));
pts.push((w / 2.0 + r * rx * a.cos(), cy + r * ry * a.sin()));
}
if normalized { normalize(&pts, w, h) } else { pts.iter().map(|(x, y)| (w / 2.0 + x * w / 2.0, h / 2.0 + y * h / 2.0)).collect() }
pts
}
#[derive(Clone, Copy)]
@ -1576,8 +1587,7 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
let n: usize = name.trim_start_matches("star").parse().unwrap_or(5);
let v = a(0).clamp(0.0, 50000.0);
let inner = v / 50000.0;
let normalized = matches!(n, 5 | 7);
let mut g = simple(b.poly(&star(n, w, h, inner, normalized)).take(), full.inset(-ss * (0.5 - inner * 0.5) * 0.5), w, h);
let mut g = simple(b.poly(&star(n, w, h, inner)).take(), full.inset(-ss * (0.5 - inner * 0.5) * 0.5), w, h);
g.handles.push(Handle::new(0, Point::new(w / 2.0, h / 2.0), Vec2::new(0.0, -h / 2.0 / 50000.0), v, 0.0, 50000.0));
g
}
@ -2268,6 +2278,26 @@ mod tests {
}
}
#[test]
fn stars_are_stretched_like_the_standards() {
// star5 in a 216 pt square: top point at the top middle, side points at 18 degrees on an
// ellipse 5.146% wider and 10.557% taller, centred 10.557% below the middle; its bottom
// points reach the box's bottom edge.
let k = 216.0;
let g = build("star5", k, k, &[]).unwrap();
let (rx, ry, cy) = (k / 2.0 * 1.05146, k / 2.0 * 1.10557, k / 2.0 * 1.10557);
let a = 18f64.to_radians();
assert!(on_path(&g, Point::new(k / 2.0, 0.0), 1e-6));
assert!(on_path(&g, Point::new(k / 2.0 + rx * a.cos(), cy - ry * a.sin()), 1e-6));
use kurbo::Shape as _;
let bb = g.paths[0].path.bounding_box();
assert!((bb.x0).abs() < 0.5 && (bb.x1 - k).abs() < 0.5 && (bb.y1 - k).abs() < 0.5, "{bb:?}");
// star6 is 15.47% wider than its box's half-width allows (its points touch the sides).
let g = build("star6", k, k, &[]).unwrap();
let bb = g.paths[0].path.bounding_box();
assert!((bb.x0).abs() < 0.5 && (bb.x1 - k).abs() < 0.5, "{bb:?}");
}
#[test]
fn the_cloud_fills_its_box_at_any_aspect_and_has_creases() {
use kurbo::Shape as _;