summaryrefslogtreecommitdiff
path: root/library/src/math/stretch.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-22 13:27:20 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-22 13:27:20 +0100
commita50cb588236a9258271d68b22b2c07fe71d19553 (patch)
tree90235d2509af221d5e35f7e01077941e2b9440de /library/src/math/stretch.rs
parent83b68581461df8968e408bec1b979ed2e3a8f0c5 (diff)
Glyph stretching
Diffstat (limited to 'library/src/math/stretch.rs')
-rw-r--r--library/src/math/stretch.rs191
1 files changed, 191 insertions, 0 deletions
diff --git a/library/src/math/stretch.rs b/library/src/math/stretch.rs
new file mode 100644
index 00000000..bd72a769
--- /dev/null
+++ b/library/src/math/stretch.rs
@@ -0,0 +1,191 @@
+use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
+use ttf_parser::LazyArray16;
+
+use super::*;
+
+/// Maximum number of times extenders can be repeated.
+const MAX_REPEATS: usize = 1024;
+
+impl GlyphFragment {
+ /// Try to stretch a glyph to a desired height.
+ pub fn stretch_vertical(
+ self,
+ ctx: &MathContext,
+ height: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, height, short_fall, false)
+ }
+
+ /// Try to stretch a glyph to a desired width.
+ pub fn stretch_horizontal(
+ self,
+ ctx: &MathContext,
+ width: Abs,
+ short_fall: Abs,
+ ) -> VariantFragment {
+ stretch_glyph(ctx, self, width, short_fall, true)
+ }
+}
+
+/// Try to stretch a glyph to a desired width or height.
+///
+/// The resulting frame may not have the exact desired width.
+fn stretch_glyph(
+ ctx: &MathContext,
+ base: GlyphFragment,
+ target: Abs,
+ short_fall: Abs,
+ horizontal: bool,
+) -> VariantFragment {
+ let short_target = target - short_fall;
+ let mut min_overlap = Abs::zero();
+ let construction = ctx
+ .table
+ .variants
+ .and_then(|variants| {
+ min_overlap = variants.min_connector_overlap.scaled(ctx);
+ if horizontal {
+ variants.horizontal_constructions
+ } else {
+ variants.vertical_constructions
+ }
+ .get(base.id)
+ })
+ .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
+
+ // If the base glyph is good enough, use it.
+ let advance = if horizontal { base.width } else { base.height() };
+ if short_target <= advance {
+ return base.to_variant(ctx);
+ }
+
+ // Search for a pre-made variant with a good advance.
+ let mut best_id = base.id;
+ let mut best_advance = base.width;
+ for variant in construction.variants {
+ best_id = variant.variant_glyph;
+ best_advance = variant.advance_measurement.scaled(ctx);
+ if short_target <= best_advance {
+ break;
+ }
+ }
+
+ // This is either good or the best we've got.
+ if short_target <= best_advance || construction.assembly.is_none() {
+ return GlyphFragment::with_id(ctx, base.c, best_id).to_variant(ctx);
+ }
+
+ // Assemble from parts.
+ let assembly = construction.assembly.unwrap();
+ assemble(ctx, base, assembly, min_overlap, target, horizontal)
+}
+
+/// Assemble a glyph from parts.
+fn assemble(
+ ctx: &MathContext,
+ base: GlyphFragment,
+ assembly: GlyphAssembly,
+ min_overlap: Abs,
+ target: Abs,
+ horizontal: bool,
+) -> VariantFragment {
+ // Determine the number of times the extenders need to be repeated as well
+ // as a ratio specifying how much to spread the parts apart
+ // (0 = maximal overlap, 1 = minimal overlap).
+ let mut full;
+ let mut ratio;
+ let mut repeat = 0;
+ loop {
+ full = Abs::zero();
+ ratio = 0.0;
+
+ let mut parts = parts(assembly, repeat).peekable();
+ let mut growable = Abs::zero();
+
+ while let Some(part) = parts.next() {
+ let mut advance = part.full_advance.scaled(ctx);
+ if let Some(next) = parts.peek() {
+ let max_overlap = part
+ .end_connector_length
+ .min(next.start_connector_length)
+ .scaled(ctx);
+
+ advance -= max_overlap;
+ growable += max_overlap - min_overlap;
+ }
+
+ full += advance;
+ }
+
+ if full < target {
+ let delta = target - full;
+ ratio = (delta / growable).min(1.0);
+ full += ratio * growable;
+ }
+
+ if target <= full || repeat >= MAX_REPEATS {
+ break;
+ }
+
+ repeat += 1;
+ }
+
+ let mut selected = vec![];
+ let mut parts = parts(assembly, repeat).peekable();
+ while let Some(part) = parts.next() {
+ let mut advance = part.full_advance.scaled(ctx);
+ if let Some(next) = parts.peek() {
+ let max_overlap =
+ part.end_connector_length.min(next.start_connector_length).scaled(ctx);
+ advance -= max_overlap;
+ advance += ratio * (max_overlap - min_overlap);
+ }
+
+ let fragment = GlyphFragment::with_id(ctx, base.c, part.glyph_id);
+ selected.push((fragment, advance));
+ }
+
+ let size;
+ let baseline;
+ if horizontal {
+ let height = base.ascent + base.descent;
+ size = Size::new(full, height);
+ baseline = base.ascent;
+ } else {
+ let axis = scaled!(ctx, axis_height);
+ let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
+ size = Size::new(width, full);
+ baseline = full / 2.0 + axis;
+ }
+
+ let mut frame = Frame::new(size);
+ let mut offset = Abs::zero();
+ frame.set_baseline(baseline);
+
+ for (fragment, advance) in selected {
+ let pos = if horizontal {
+ Point::new(offset, frame.baseline() - fragment.ascent)
+ } else {
+ Point::with_y(full - offset - fragment.height())
+ };
+ frame.push_frame(pos, fragment.to_frame(ctx));
+ offset += advance;
+ }
+
+ VariantFragment {
+ c: base.c,
+ id: None,
+ frame,
+ italics_correction: Abs::zero(),
+ }
+}
+
+/// Return an iterator over the assembly's parts with extenders repeated the
+/// specified number of times.
+fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
+ assembly.parts.into_iter().flat_map(move |part| {
+ let count = if part.part_flags.extender() { repeat } else { 1 };
+ std::iter::repeat(part).take(count)
+ })
+}