mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
M7.3: Type → Type on a Path effects (Rainbow, Skew, 3D Ribbon, Stair Step, Gravity): TextObject::path_effect, type.pathOptions {effect}, checked menu items
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8434ef4404
commit
0ae572be93
@@ -3,7 +3,7 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, Rect, Shape, Vec2};
|
||||
use vectorcraft_doc::{CharStyle, Justify, ParaStyle, TextKind, TextObject};
|
||||
use vectorcraft_doc::{CharStyle, Justify, ParaStyle, PathEffect, TextKind, TextObject};
|
||||
|
||||
use crate::composer::{Breakpoint, compose};
|
||||
use crate::fontdb::FontDb;
|
||||
@@ -93,7 +93,7 @@ pub fn layout_with(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLay
|
||||
cx.out.frames = regions.iter().map(|r| r.cell).collect();
|
||||
flow(&mut cx, ¶s, &t.para, Some(®ions));
|
||||
}
|
||||
TextKind::OnPath { path, start } => on_path(&mut cx, ¶s, &t.para, &path.to_bezpath(), *start, path.is_closed()),
|
||||
TextKind::OnPath { path, start } => on_path(&mut cx, ¶s, &t.para, &path.to_bezpath(), *start, path.is_closed(), t.path_effect),
|
||||
}
|
||||
finish_bounds(&mut cx.out);
|
||||
cx.out
|
||||
@@ -624,7 +624,8 @@ impl ArcPath {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, path: &BezPath, start: f64, closed: bool) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, path: &BezPath, start: f64, closed: bool, effect: PathEffect) {
|
||||
cx.out.on_path = true;
|
||||
let mut sg = Vec::new();
|
||||
for pr in paras {
|
||||
@@ -652,6 +653,7 @@ fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, path: &Be
|
||||
});
|
||||
return;
|
||||
}
|
||||
let centre = path.bounding_box().center();
|
||||
let s_start = start.clamp(0.0, 1.0) * ap.len;
|
||||
let avail = if closed { ap.len } else { ap.len - s_start };
|
||||
let w: f64 = sg.iter().map(|g| g.adv).sum();
|
||||
@@ -673,7 +675,37 @@ fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, path: &Be
|
||||
}
|
||||
let (p, dir) = ap.at(mid);
|
||||
let angle = dir.y.atan2(dir.x);
|
||||
let pre = Affine::translate(p.to_vec2()) * Affine::rotate(angle) * Affine::translate((-g.adv * 0.5, 0.0));
|
||||
let half = Affine::translate((-g.adv * 0.5, 0.0));
|
||||
// Glyph space: x along the advance, y down from the baseline; `pre` maps it onto the path.
|
||||
let pre = match effect {
|
||||
PathEffect::Rainbow => Affine::translate(p.to_vec2()) * Affine::rotate(angle) * half,
|
||||
// x axis along the tangent, y axis stays vertical.
|
||||
PathEffect::Skew => Affine::translate(p.to_vec2()) * Affine::new([dir.x, dir.y, 0.0, 1.0, 0.0, 0.0]) * half,
|
||||
// x axis stays horizontal (facing the path's direction), y axis perpendicular to the path.
|
||||
PathEffect::Ribbon3d => {
|
||||
let sx = if dir.x < 0.0 { -1.0 } else { 1.0 };
|
||||
Affine::translate(p.to_vec2()) * Affine::new([sx, 0.0, -dir.y * sx, dir.x * sx, 0.0, 0.0]) * half
|
||||
}
|
||||
PathEffect::StairStep => {
|
||||
let s_left = if closed { s.rem_euclid(ap.len) } else { s };
|
||||
Affine::translate(ap.at(s_left).0.to_vec2())
|
||||
}
|
||||
// x axis along the tangent; vertical edges point at the path's centre (kept on the glyph's
|
||||
// up side, and never closer than ~17° to the baseline so glyphs stay legible).
|
||||
PathEffect::Gravity => {
|
||||
let n = Vec2::new(dir.y, -dir.x);
|
||||
let mut up = p - centre;
|
||||
up = if up.hypot() < 1e-9 { n } else { up / up.hypot() };
|
||||
if up.dot(n) < 0.0 {
|
||||
up = -up;
|
||||
}
|
||||
if up.dot(n) < 0.3 {
|
||||
let t = up - n * up.dot(n);
|
||||
up = n * 0.3 + t / t.hypot().max(1e-9) * (1.0 - 0.09f64).sqrt();
|
||||
}
|
||||
Affine::translate(p.to_vec2()) * Affine::new([dir.x, dir.y, -up.x, -up.y, 0.0, 0.0]) * half
|
||||
}
|
||||
};
|
||||
cx.emit(g, pre, p - dir * (g.adv * 0.5), angle, g.adv, 0);
|
||||
x += g.adv;
|
||||
}
|
||||
|
||||
@@ -293,6 +293,36 @@ fn on_path_placement() {
|
||||
assert_eq!(hit_byte(&l, mid), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_path_effects_orient_glyphs() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
let mut diag = BezPath::new();
|
||||
diag.move_to((0.0, 0.0));
|
||||
diag.line_to((400.0, 400.0));
|
||||
let mut t = point("H", style(40.0));
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&diag), start: 0.1 };
|
||||
let bbox = |t: &TextObject| layout(db(), t).glyphs[0].outline.bounding_box();
|
||||
let rainbow = bbox(&t);
|
||||
t.path_effect = PathEffect::StairStep;
|
||||
let stair = bbox(&t);
|
||||
// Unrotated: as tall as the cap height, narrower than the rotated glyph's box.
|
||||
assert!(stair.height() < rainbow.height() && stair.height() > 20.0, "{stair:?} {rainbow:?}");
|
||||
t.path_effect = PathEffect::Skew;
|
||||
let skew = bbox(&t);
|
||||
// Vertical stems stay vertical: the box spans the cap height plus the slant of the baseline.
|
||||
assert!(skew.height() > stair.height() && skew.width() < rainbow.width() + 1e-6, "{skew:?}");
|
||||
// Gravity on a circle: glyphs point away from the centre (same as Rainbow on a circle).
|
||||
let circle = kurbo::Circle::new((0.0, 0.0), 100.0).to_path(0.1);
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&circle), start: 0.0 };
|
||||
t.path_effect = PathEffect::Rainbow;
|
||||
let a = layout(db(), &t).glyphs[0].outline.bounding_box();
|
||||
t.path_effect = PathEffect::Gravity;
|
||||
let b = layout(db(), &t).glyphs[0].outline.bounding_box();
|
||||
assert!((a.center() - b.center()).hypot() < 2.0, "{a:?} {b:?}");
|
||||
assert_eq!(PathEffect::parse("3D Ribbon"), Some(PathEffect::Ribbon3d));
|
||||
assert_eq!(PathEffect::parse("stair step"), Some(PathEffect::StairStep));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_path_circle_and_overflow() {
|
||||
let circle = kurbo::Circle::new((0.0, 0.0), 100.0).to_path(0.1);
|
||||
|
||||
Reference in New Issue
Block a user