1use deckcraft_geom::Xfrm;
6use deckcraft_model::resolve::Ctx;
7use deckcraft_model::{Shape, ShapeId};
8use deckcraft_text::{Fields, GlyphRun, Opts, TextLayout};
9use kurbo::{Affine, Point, Vec2};
10use vello_cpu::RenderContext;
11
12use crate::{color, draw_glyph, draw_layout, shape_geometry, text_transform};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct TextMorph {
17 pub old: ShapeId,
19 pub new: ShapeId,
20 pub from: Xfrm,
22 pub to: Xfrm,
23 pub chars: bool,
25 pub t: f64,
27}
28
29struct Token {
31 key: String,
32 glyphs: Vec<(usize, usize)>,
33 origin: Point,
34 run: usize,
35}
36
37fn tokens(l: &TextLayout, chars: bool) -> (Vec<Token>, Vec<(usize, usize)>) {
39 let mut cells: Vec<(usize, usize, char, usize, usize)> = Vec::new();
40 let mut loose = Vec::new();
41 for (ri, run) in l.runs.iter().enumerate() {
42 for gi in 0..run.glyphs.len() {
43 match run.cells.get(gi) {
44 Some((ci, ch)) => cells.push((run.para, *ci, *ch, ri, gi)),
45 None => loose.push((ri, gi)),
46 }
47 }
48 }
49 cells.sort_by_key(|c| (c.0, c.1, c.3, c.4));
50 let mut out: Vec<Token> = Vec::new();
51 let mut last: Option<(usize, usize)> = None;
52 for (para, ci, ch, ri, gi) in cells {
53 if ch.is_whitespace() {
54 last = None;
55 continue;
56 }
57 let same_char = last == Some((para, ci));
58 let next_char = last.is_some_and(|(p, c)| p == para && c.checked_add(1) == Some(ci));
59 let extend = same_char || (!chars && next_char);
60 match out.last_mut() {
61 Some(tok) if extend => {
62 if !same_char {
63 tok.key.push(ch);
64 }
65 tok.glyphs.push((ri, gi));
66 }
67 _ => {
68 let origin = l
69 .runs
70 .get(ri)
71 .and_then(|r| r.glyphs.get(gi))
72 .map(|g| Point::new(g.1, g.2))
73 .unwrap_or(Point::ZERO);
74 out.push(Token {
75 key: ch.to_string(),
76 glyphs: vec![(ri, gi)],
77 origin,
78 run: ri,
79 });
80 }
81 }
82 last = Some((para, ci));
83 }
84 (out, loose)
85}
86
87fn matches(a: &[Token], b: &[Token]) -> Vec<Option<usize>> {
89 let mut used = vec![false; a.len()];
90 b.iter()
91 .map(|tb| {
92 let i = a
93 .iter()
94 .enumerate()
95 .position(|(i, ta)| !used.get(i).copied().unwrap_or(true) && ta.key == tb.key)?;
96 if let Some(u) = used.get_mut(i) {
97 *u = true;
98 }
99 Some(i)
100 })
101 .collect()
102}
103
104fn layout(ctx: &Ctx, s: &Shape, x: Xfrm, fields: &dyn Fields) -> Option<TextLayout> {
105 let body = s.text.as_ref().filter(|b| !b.is_empty())?;
106 let rect = shape_geometry(s, x.w, x.h).text_rect;
107 Some(deckcraft_text::layout(
108 ctx,
109 s,
110 body,
111 &Opts {
112 rect,
113 fields,
114 prompt_color: None,
115 no_shrink: false,
116 },
117 ))
118}
119
120fn glyph_at(l: &TextLayout, (ri, gi): (usize, usize)) -> Option<(&GlyphRun, u32, Point)> {
121 let run = l.runs.get(ri)?;
122 let g = run.glyphs.get(gi)?;
123 Some((run, g.0, Point::new(g.1, g.2)))
124}
125
126fn same_look(a: &GlyphRun, b: &GlyphRun) -> bool {
127 a.face.id() == b.face.id()
128 && a.size == b.size
129 && a.color == b.color
130 && a.alpha == b.alpha
131 && a.outline == b.outline
132}
133
134fn lerp(a: f64, b: f64, t: f64) -> f64 {
135 a + (b - a) * t
136}
137
138pub(crate) fn draw(
141 ctx: &mut RenderContext,
142 (ca, a, fa): (&Ctx, &Shape, &dyn Fields),
143 (cb, b, fb): (&Ctx, &Shape, &dyn Fields),
144 m: &TextMorph,
145 now: Xfrm,
146 view: Affine,
147) {
148 let t = if m.t.is_finite() {
149 m.t.clamp(0.0, 1.0)
150 } else {
151 1.0
152 };
153 let la = layout(ca, a, m.from, fa);
154 let lb = layout(cb, b, m.to, fb);
155 let mt = view * now.affine();
156 let rotated = la.as_ref().is_some_and(|l| l.rotation != 0.0)
158 || lb.as_ref().is_some_and(|l| l.rotation != 0.0);
159 if rotated || la.is_none() || lb.is_none() {
160 for (l, s, alpha, x) in [(la, a, 1.0 - t, m.from), (lb, b, t, m.to)] {
161 if let Some(l) = l
162 && alpha > 0.001
163 {
164 ctx.push_layer(None, None, Some(alpha as f32), None, None);
165 draw_layout(ctx, &l, shape_geometry(s, x.w, x.h).text_rect, mt);
166 ctx.pop_layer();
167 }
168 }
169 return;
170 }
171 let (Some(la), Some(lb)) = (la, lb) else {
172 return;
173 };
174 let (ta, loose_a) = tokens(&la, m.chars);
175 let (tb, loose_b) = tokens(&lb, m.chars);
176 let pairs = matches(&ta, &tb);
177 let (tra, trb) = (
179 shape_geometry(a, m.from.w, m.from.h).text_rect,
180 shape_geometry(b, m.to.w, m.to.h).text_rect,
181 );
182 let (ma, mb) = (
183 text_transform(view * m.from.affine(), tra, 0.0),
184 text_transform(view * m.to.affine(), trb, 0.0),
185 );
186 let (mta, mtb) = (text_transform(mt, tra, 0.0), text_transform(mt, trb, 0.0));
187 let c = mta.as_coeffs();
188 let linear = Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]);
189 let mut moved_a = vec![false; ta.len()];
190 for (j, i) in pairs.iter().enumerate() {
192 let (Some(i), Some(tok_b)) = (*i, tb.get(j)) else {
193 continue;
194 };
195 let Some(tok_a) = ta.get(i) else { continue };
196 if let Some(f) = moved_a.get_mut(i) {
197 *f = true;
198 }
199 let (Some(ra), Some(rb)) = (la.runs.get(tok_a.run), lb.runs.get(tok_b.run)) else {
200 continue;
201 };
202 let p = (ma * tok_a.origin).lerp(mb * tok_b.origin, t);
203 let ratio = if ra.size > 0.0 && rb.size > 0.0 {
204 rb.size / ra.size
205 } else {
206 1.0
207 };
208 let same = same_look(ra, rb);
209 let alpha_a = if same { 1.0 } else { 1.0 - t };
210 let sa = lerp(1.0, ratio, t);
211 for &g in &tok_a.glyphs {
212 if let Some((run, gid, pos)) = glyph_at(&la, g) {
213 let at = Affine::translate(p.to_vec2())
214 * linear
215 * Affine::scale(sa)
216 * Affine::translate(pos - tok_a.origin);
217 draw_glyph(ctx, run, gid, at, alpha_a);
218 }
219 }
220 if !same {
221 let sb = lerp(1.0 / ratio, 1.0, t);
222 for &g in &tok_b.glyphs {
223 if let Some((run, gid, pos)) = glyph_at(&lb, g) {
224 let at = Affine::translate(p.to_vec2())
225 * linear
226 * Affine::scale(sb)
227 * Affine::translate(pos - tok_b.origin);
228 draw_glyph(ctx, run, gid, at, t);
229 }
230 }
231 }
232 }
233 let shared: Vec<(Point, Point, f64)> = pairs
237 .iter()
238 .enumerate()
239 .filter_map(|(j, i)| {
240 let (tok_a, tok_b) = (ta.get((*i)?)?, tb.get(j)?);
241 let (ra, rb) = (la.runs.get(tok_a.run)?, lb.runs.get(tok_b.run)?);
242 let ratio = if ra.size > 0.0 && rb.size > 0.0 {
243 rb.size / ra.size
244 } else {
245 1.0
246 };
247 Some((ma * tok_a.origin, mb * tok_b.origin, ratio))
248 })
249 .collect();
250 let n = shared.len() as f64;
251 let s = shared.iter().map(|x| x.2).sum::<f64>() / n;
252 let c = shared.iter().fold(Vec2::ZERO, |acc, (pa, pb, _)| {
253 acc + (pb.to_vec2() - pa.to_vec2() * s)
254 }) / n;
255 let (place_a, place_b) = if !shared.is_empty() && s.is_finite() && s > 0.0 && c.is_finite() {
256 let a = Affine::translate(c * t) * Affine::scale(lerp(1.0, s, t));
258 let b = Affine::translate(-c * ((1.0 - t) / s)) * Affine::scale((1.0 - t) / s + t);
259 (a * ma, b * mb)
260 } else {
261 (mta, mtb)
262 };
263 let unmatched_a = ta
264 .iter()
265 .enumerate()
266 .filter(|(i, _)| !moved_a.get(*i).copied().unwrap_or(false))
267 .flat_map(|(_, tok)| tok.glyphs.iter().copied());
268 for g in unmatched_a.chain(loose_a) {
269 if let Some((run, gid, pos)) = glyph_at(&la, g) {
270 draw_glyph(
271 ctx,
272 run,
273 gid,
274 place_a * Affine::translate(pos.to_vec2()),
275 1.0 - t,
276 );
277 }
278 }
279 let unmatched_b = tb
280 .iter()
281 .enumerate()
282 .filter(|(j, _)| pairs.get(*j).is_none_or(|p| p.is_none()))
283 .flat_map(|(_, tok)| tok.glyphs.iter().copied());
284 for g in unmatched_b.chain(loose_b) {
285 if let Some((run, gid, pos)) = glyph_at(&lb, g) {
286 draw_glyph(ctx, run, gid, place_b * Affine::translate(pos.to_vec2()), t);
287 }
288 }
289 for (l, alpha, place) in [(&la, 1.0 - t, place_a), (&lb, t, place_b)] {
291 for d in &l.decos {
292 ctx.set_transform(place);
293 ctx.set_paint(color(d.color, alpha));
294 ctx.fill_rect(&d.rect);
295 }
296 }
297}
298
299#[cfg(test)]
300pub(crate) fn token_keys(l: &TextLayout, chars: bool) -> Vec<String> {
301 tokens(l, chars).0.into_iter().map(|t| t.key).collect()
302}