mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
M13.6: variable font axes in the character style
CharStyle/TextDoc variations (layer.setText variations), shaped with harfrust instances so advances follow HVAR/gvar, outlines drawn at the design-space position, animator deltas relative to the style's values, Character panel Variable Font Axes fields (character.axis.<tag>). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b0a449c5f
commit
f89af341a6
@@ -12,6 +12,32 @@ use skrifa::{GlyphId, MetadataProvider, Tag};
|
||||
use crate::fonts::{self, FaceId};
|
||||
use crate::{CharGlyph, Pen};
|
||||
|
||||
/// Intern a set of axis values (0 for none), so `Copy` glyphs can carry them.
|
||||
pub fn intern(coords: &[(String, f32)]) -> u32 {
|
||||
if coords.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(i) = t.iter().position(|c| c.as_slice() == coords) {
|
||||
return i as u32 + 1;
|
||||
}
|
||||
t.push(Arc::new(coords.to_vec()));
|
||||
t.len() as u32
|
||||
}
|
||||
|
||||
/// The axis values of an interned id (empty for 0).
|
||||
pub fn coords(id: u32) -> Arc<Vec<(String, f32)>> {
|
||||
if id == 0 {
|
||||
return Arc::new(Vec::new());
|
||||
}
|
||||
table().lock().unwrap_or_else(|e| e.into_inner()).get(id as usize - 1).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn table() -> &'static Mutex<Vec<Arc<Vec<(String, f32)>>>> {
|
||||
static T: OnceLock<Mutex<Vec<Arc<Vec<(String, f32)>>>>> = OnceLock::new();
|
||||
T.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// One variation axis of a face, in user units.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FontAxis {
|
||||
@@ -38,7 +64,7 @@ pub fn font_axes(face: FaceId) -> Vec<FontAxis> {
|
||||
}
|
||||
|
||||
/// A tag string as a [`Tag`] (padded with spaces, at most four characters).
|
||||
fn tag_of(s: &str) -> Tag {
|
||||
pub(crate) fn tag_of(s: &str) -> Tag {
|
||||
let mut b = [b' '; 4];
|
||||
for (i, c) in s.bytes().take(4).enumerate() {
|
||||
b[i] = c;
|
||||
@@ -78,14 +104,21 @@ pub fn outline_units_at(face: FaceId, gid: u32, coords: &[(String, f32)]) -> Opt
|
||||
}
|
||||
|
||||
/// A laid-out character's outline (same placement as [`CharGlyph::path`]) with its face's axes
|
||||
/// moved by `deltas` (tag, user-unit offset from the axis default).
|
||||
/// moved by `deltas` (tag, user-unit offset from the character's own axis value — its style's
|
||||
/// Variable Font Axes setting, else the axis default).
|
||||
pub fn char_outline_varied(g: &CharGlyph, deltas: &[(String, f32)]) -> Option<BezPath> {
|
||||
let axes = font_axes(g.face);
|
||||
let coords: Vec<(String, f32)> =
|
||||
deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (a.default + d).clamp(a.min, a.max)))).collect();
|
||||
let base = |t: &str, d: f32| g.variations.iter().find(|(x, _)| x.trim_end() == t.trim_end()).map(|(_, v)| *v).unwrap_or(d);
|
||||
let mut coords: Vec<(String, f32)> =
|
||||
deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (base(&a.tag, a.default) + d).clamp(a.min, a.max)))).collect();
|
||||
if coords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for (t, v) in &g.variations {
|
||||
if !coords.iter().any(|(c, _)| c == t) {
|
||||
coords.push((t.clone(), *v));
|
||||
}
|
||||
}
|
||||
let units = outline_units_at(g.face, g.gid, &coords)?;
|
||||
Some(g.outline_xf * (*units).clone())
|
||||
}
|
||||
@@ -122,4 +155,37 @@ mod tests {
|
||||
let s = crate::outline_units(face, gid).unwrap();
|
||||
assert!((d.area() - s.area()).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_variations_change_advances_and_outlines() {
|
||||
assert_eq!(intern(&[]), 0);
|
||||
let id = intern(&[("wght".into(), 700.0)]);
|
||||
assert_eq!(intern(&[("wght".into(), 700.0)]), id);
|
||||
assert_eq!(coords(id).as_slice(), &[("wght".to_string(), 700.0)]);
|
||||
let Some((face, axes)) = find_variable_face() else {
|
||||
eprintln!("no variable font installed: skipped");
|
||||
return;
|
||||
};
|
||||
// An axis that changes widths (wdth or wght, else the first that moves).
|
||||
let a = axes.iter().find(|a| a.tag == "wdth" && a.max > a.min).or_else(|| axes.iter().find(|a| a.tag == "wght" && a.max > a.min)).unwrap_or(&axes[0]);
|
||||
let info = crate::fonts::face(face).info.clone();
|
||||
let st = |v: f32| crate::TextStyle {
|
||||
family: info.family.clone(),
|
||||
style: info.style.clone(),
|
||||
size: 100.0,
|
||||
variations: vec![(a.tag.clone(), v)],
|
||||
..Default::default()
|
||||
};
|
||||
let (lo, hi) = (crate::layout::measure("Hamburgefonts", &st(a.min)), crate::layout::measure("Hamburgefonts", &st(a.max)));
|
||||
if crate::resolve(&info.family, &info.style).face != face {
|
||||
eprintln!("variable face not reachable by family/style: skipped");
|
||||
return;
|
||||
}
|
||||
assert!((lo - hi).abs() > 1.0, "{} {}..{}: advances {lo} vs {hi}", a.tag, a.min, a.max);
|
||||
let ink = |v: f32| {
|
||||
let l = crate::layout::layout("H", &st(v), &crate::ParagraphStyle::default());
|
||||
l.glyphs.iter().map(|g| crate::glyph_outline(g).area().abs()).sum::<f64>()
|
||||
};
|
||||
assert!((ink(a.min) - ink(a.max)).abs() > 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user