1/*
2 * Copyright 2019 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "modules/skottie/src/text/TextAdapter.h"
9
10#include "include/core/SkFontMgr.h"
11#include "include/core/SkM44.h"
12#include "modules/skottie/src/SkottieJson.h"
13#include "modules/skottie/src/text/RangeSelector.h"
14#include "modules/skottie/src/text/TextAnimator.h"
15#include "modules/sksg/include/SkSGDraw.h"
16#include "modules/sksg/include/SkSGGroup.h"
17#include "modules/sksg/include/SkSGPaint.h"
18#include "modules/sksg/include/SkSGRect.h"
19#include "modules/sksg/include/SkSGRenderEffect.h"
20#include "modules/sksg/include/SkSGText.h"
21#include "modules/sksg/include/SkSGTransform.h"
22
23namespace skottie {
24namespace internal {
25
26sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
27 const AnimationBuilder* abuilder,
28 sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) {
29 // General text node format:
30 // "t": {
31 // "a": [], // animators (see TextAnimator)
32 // "d": {
33 // "k": [
34 // {
35 // "s": {
36 // "f": "Roboto-Regular",
37 // "fc": [
38 // 0.42,
39 // 0.15,
40 // 0.15
41 // ],
42 // "j": 1,
43 // "lh": 60,
44 // "ls": 0,
45 // "s": 50,
46 // "t": "text align right",
47 // "tr": 0
48 // },
49 // "t": 0
50 // }
51 // ]
52 // },
53 // "m": { // "more options"
54 // "g": 1, // Anchor Point Grouping
55 // "a": {...} // Grouping Alignment
56 // },
57 // "p": {} // "path options" (TODO)
58 // },
59
60 const skjson::ObjectValue* jt = jlayer["t"];
61 const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
62 : nullptr;
63 if (!jd) {
64 abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
65 return nullptr;
66 }
67
68 // "More options"
69 const skjson::ObjectValue* jm = (*jt)["m"];
70 static constexpr AnchorPointGrouping gGroupingMap[] = {
71 AnchorPointGrouping::kCharacter, // 'g': 1
72 AnchorPointGrouping::kWord, // 'g': 2
73 AnchorPointGrouping::kLine, // 'g': 3
74 AnchorPointGrouping::kAll, // 'g': 4
75 };
76 const auto apg = jm
77 ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, SK_ARRAY_COUNT(gGroupingMap))
78 : 1;
79
80 auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
81 std::move(logger),
82 gGroupingMap[SkToSizeT(apg - 1)]));
83
84 adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
85 if (jm) {
86 adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
87 }
88
89 // Animators
90 if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
91 adapter->fAnimators.reserve(janimators->size());
92
93 for (const skjson::ObjectValue* janimator : *janimators) {
94 if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
95 adapter->fHasBlurAnimator |= animator->hasBlur();
96 adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint();
97
98 adapter->fAnimators.push_back(std::move(animator));
99 }
100 }
101 }
102
103 abuilder->dispatchTextProperty(adapter);
104
105 return adapter;
106}
107
108TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
109 : fRoot(sksg::Group::Make())
110 , fFontMgr(std::move(fontmgr))
111 , fLogger(std::move(logger))
112 , fAnchorPointGrouping(apg)
113 , fHasBlurAnimator(false)
114 , fRequiresAnchorPoint(false) {}
115
116TextAdapter::~TextAdapter() = default;
117
118void TextAdapter::addFragment(const Shaper::Fragment& frag) {
119 // For a given shaped fragment, build a corresponding SG fragment:
120 //
121 // [TransformEffect] -> [Transform]
122 // [Group]
123 // [Draw] -> [TextBlob*] [FillPaint]
124 // [Draw] -> [TextBlob*] [StrokePaint]
125 //
126 // * where the blob node is shared
127
128 auto blob_node = sksg::TextBlob::Make(frag.fBlob);
129
130 FragmentRec rec;
131 rec.fOrigin = frag.fPos;
132 rec.fAdvance = frag.fAdvance;
133 rec.fAscent = frag.fAscent;
134 rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y()));
135
136 std::vector<sk_sp<sksg::RenderNode>> draws;
137 draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
138
139 SkASSERT(fText->fHasFill || fText->fHasStroke);
140
141 auto add_fill = [&]() {
142 if (fText->fHasFill) {
143 rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
144 rec.fFillColorNode->setAntiAlias(true);
145 draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
146 }
147 };
148 auto add_stroke = [&] {
149 if (fText->fHasStroke) {
150 rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
151 rec.fStrokeColorNode->setAntiAlias(true);
152 rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
153 rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth);
154 draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
155 }
156 };
157
158 if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
159 add_fill();
160 add_stroke();
161 } else {
162 add_stroke();
163 add_fill();
164 }
165
166 SkASSERT(!draws.empty());
167
168 if (0) {
169 // enable to visualize fragment ascent boxes
170 auto box_color = sksg::Color::Make(0xff0000ff);
171 box_color->setStyle(SkPaint::kStroke_Style);
172 box_color->setStrokeWidth(1);
173 box_color->setAntiAlias(true);
174 auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
175 draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
176 }
177
178 auto draws_node = (draws.size() > 1)
179 ? sksg::Group::Make(std::move(draws))
180 : std::move(draws[0]);
181
182 if (fHasBlurAnimator) {
183 // Optional blur effect.
184 rec.fBlur = sksg::BlurImageFilter::Make();
185 draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
186 }
187
188 fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
189 fFragments.push_back(std::move(rec));
190}
191
192void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
193 fMaps.fNonWhitespaceMap.clear();
194 fMaps.fWordsMap.clear();
195 fMaps.fLinesMap.clear();
196
197 size_t i = 0,
198 line = 0,
199 line_start = 0,
200 word_start = 0;
201
202 float word_advance = 0,
203 word_ascent = 0,
204 line_advance = 0,
205 line_ascent = 0;
206
207 bool in_word = false;
208
209 // TODO: use ICU for building the word map?
210 for (; i < shape_result.fFragments.size(); ++i) {
211 const auto& frag = shape_result.fFragments[i];
212
213 if (frag.fIsWhitespace) {
214 if (in_word) {
215 in_word = false;
216 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
217 }
218 } else {
219 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
220
221 if (!in_word) {
222 in_word = true;
223 word_start = i;
224 word_advance = word_ascent = 0;
225 }
226
227 word_advance += frag.fAdvance;
228 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
229 }
230
231 if (frag.fLineIndex != line) {
232 SkASSERT(frag.fLineIndex == line + 1);
233 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
234 line = frag.fLineIndex;
235 line_start = i;
236 line_advance = line_ascent = 0;
237 }
238
239 line_advance += frag.fAdvance;
240 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
241 }
242
243 if (i > word_start) {
244 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
245 }
246
247 if (i > line_start) {
248 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
249 }
250}
251
252void TextAdapter::setText(const TextValue& txt) {
253 fText.fCurrentValue = txt;
254 this->onSync();
255}
256
257uint32_t TextAdapter::shaperFlags() const {
258 uint32_t flags = Shaper::Flags::kNone;
259
260 SkASSERT(!(fRequiresAnchorPoint && fAnimators.empty()));
261 if (!fAnimators.empty() ) flags |= Shaper::Flags::kFragmentGlyphs;
262 if (fRequiresAnchorPoint) flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
263
264 return flags;
265}
266
267void TextAdapter::reshape() {
268 const Shaper::TextDesc text_desc = {
269 fText->fTypeface,
270 fText->fTextSize,
271 fText->fLineHeight,
272 fText->fAscent,
273 fText->fHAlign,
274 fText->fVAlign,
275 fText->fResize,
276 this->shaperFlags(),
277 };
278 const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
279
280 if (fLogger && shape_result.fMissingGlyphCount > 0) {
281 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
282 shape_result.fMissingGlyphCount,
283 fText->fText.c_str());
284 fLogger->log(Logger::Level::kWarning, msg.c_str());
285
286 // This may trigger repeatedly when the text is animating.
287 // To avoid spamming, only log once.
288 fLogger = nullptr;
289 }
290
291 // Rebuild all fragments.
292 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
293
294 fRoot->clear();
295 fFragments.clear();
296
297 for (const auto& frag : shape_result.fFragments) {
298 this->addFragment(frag);
299 }
300
301 if (!fAnimators.empty()) {
302 // Range selectors require fragment domain maps.
303 this->buildDomainMaps(shape_result);
304 }
305
306#if (0)
307 // Enable for text box debugging/visualization.
308 auto box_color = sksg::Color::Make(0xffff0000);
309 box_color->setStyle(SkPaint::kStroke_Style);
310 box_color->setStrokeWidth(1);
311 box_color->setAntiAlias(true);
312
313 auto bounds_color = sksg::Color::Make(0xff00ff00);
314 bounds_color->setStyle(SkPaint::kStroke_Style);
315 bounds_color->setStrokeWidth(1);
316 bounds_color->setAntiAlias(true);
317
318 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText.fBox),
319 std::move(box_color)));
320 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeBounds()),
321 std::move(bounds_color)));
322#endif
323}
324
325void TextAdapter::onSync() {
326 if (!fText->fHasFill && !fText->fHasStroke) {
327 return;
328 }
329
330 if (fText.hasChanged()) {
331 this->reshape();
332 }
333
334 if (fFragments.empty()) {
335 return;
336 }
337
338 // Seed props from the current text value.
339 TextAnimator::ResolvedProps seed_props;
340 seed_props.fill_color = fText->fFillColor;
341 seed_props.stroke_color = fText->fStrokeColor;
342
343 TextAnimator::ModulatorBuffer buf;
344 buf.resize(fFragments.size(), { seed_props, 0 });
345
346 // Apply all animators to the modulator buffer.
347 for (const auto& animator : fAnimators) {
348 animator->modulateProps(fMaps, buf);
349 }
350
351 const TextAnimator::DomainMap* grouping_domain = nullptr;
352 switch (fAnchorPointGrouping) {
353 // for word/line grouping, we rely on domain map info
354 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
355 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
356 // remaining grouping modes (character/all) do not need (or have) domain map data
357 default: break;
358 }
359
360 size_t grouping_span_index = 0;
361 SkV2 line_offset = { 0, 0 }; // cumulative line spacing
362
363 // Finally, push all props to their corresponding fragment.
364 for (const auto& line_span : fMaps.fLinesMap) {
365 SkV2 line_spacing = { 0, 0 };
366 float line_tracking = 0;
367 bool line_has_tracking = false;
368
369 // Tracking requires special treatment: unlike other props, its effect is not localized
370 // to a single fragment, but requires re-alignment of the whole line.
371 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
372 // Track the grouping domain span in parallel.
373 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
374 (*grouping_domain)[grouping_span_index].fCount) {
375 grouping_span_index += 1;
376 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
377 (*grouping_domain)[grouping_span_index].fCount);
378 }
379
380 const auto& props = buf[i].props;
381 const auto& frag = fFragments[i];
382 this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
383 grouping_domain ? &(*grouping_domain)[grouping_span_index]
384 : nullptr);
385
386 line_tracking += props.tracking;
387 line_has_tracking |= !SkScalarNearlyZero(props.tracking);
388
389 line_spacing += props.line_spacing;
390 }
391
392 // line spacing of the first line is ignored (nothing to "space" against)
393 if (&line_span != &fMaps.fLinesMap.front()) {
394 // For each line, the actual spacing is an average of individual fragment spacing
395 // (to preserve the "line").
396 line_offset += line_spacing / line_span.fCount;
397 }
398
399 if (line_offset != SkV2{0, 0} || line_has_tracking) {
400 this->adjustLineProps(buf, line_span, line_offset, line_tracking);
401 }
402
403 }
404}
405
406SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
407 const SkV2& grouping_alignment,
408 const TextAnimator::DomainSpan* grouping_span) const {
409 // Construct the following 2x ascent box:
410 //
411 // -------------
412 // | |
413 // | | ascent
414 // | |
415 // ----+-------------+---------- baseline
416 // (pos) |
417 // | | ascent
418 // | |
419 // -------------
420 // advance
421
422 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
423 // note: negative ascent
424 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
425 };
426
427 // Compute a grouping-dependent anchor point box.
428 // The default anchor point is at the center, and gets adjusted relative to the bounds
429 // based on |grouping_alignment|.
430 auto anchor_box = [&]() -> SkRect {
431 switch (fAnchorPointGrouping) {
432 case AnchorPointGrouping::kCharacter:
433 // Anchor box relative to each individual fragment.
434 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
435 case AnchorPointGrouping::kWord:
436 // Fall through
437 case AnchorPointGrouping::kLine: {
438 SkASSERT(grouping_span);
439 // Anchor box relative to the first fragment in the word/line.
440 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
441 return make_box(first_span_fragment.fOrigin,
442 grouping_span->fAdvance,
443 grouping_span->fAscent);
444 }
445 case AnchorPointGrouping::kAll:
446 // Anchor box is the same as the text box.
447 return fText->fBox;
448 }
449 SkUNREACHABLE;
450 };
451
452 const auto ab = anchor_box();
453
454 // Apply grouping alignment.
455 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
456 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
457
458 // The anchor point is relative to the fragment position.
459 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
460}
461
462void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
463 const FragmentRec& rec,
464 const SkV2& grouping_alignment,
465 const TextAnimator::DomainSpan* grouping_span) const {
466 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
467
468 rec.fMatrixNode->setMatrix(
469 SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x,
470 props.position.y + rec.fOrigin.y() + anchor_point.y,
471 props.position.z)
472 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
473 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
474 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
475 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
476 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
477
478 const auto scale_alpha = [](SkColor c, float o) {
479 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
480 };
481
482 if (rec.fFillColorNode) {
483 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
484 }
485 if (rec.fStrokeColorNode) {
486 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
487 }
488 if (rec.fBlur) {
489 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
490 props.blur.y * kBlurSizeToSigma });
491 }
492}
493
494void TextAdapter::adjustLineProps(const TextAnimator::ModulatorBuffer& buf,
495 const TextAnimator::DomainSpan& line_span,
496 const SkV2& line_offset,
497 float total_tracking) const {
498 SkASSERT(line_span.fCount > 0);
499
500 // AE tracking is defined per glyph, based on two components: |before| and |after|.
501 // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
502 //
503 // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
504 // purposes.
505
506 // The first glyph does not contribute |before| tracking, and the last one does not contribute
507 // |after| tracking. Rather than spill this logic into applyAnimators, post-adjust here.
508 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
509 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
510
511 static const auto align_factor = [](SkTextUtils::Align a) {
512 switch (a) {
513 case SkTextUtils::kLeft_Align : return 0.0f;
514 case SkTextUtils::kCenter_Align: return -0.5f;
515 case SkTextUtils::kRight_Align : return -1.0f;
516 }
517
518 SkASSERT(false);
519 return 0.0f;
520 };
521
522 const auto align_offset = total_tracking * align_factor(fText->fHAlign);
523
524 float tracking_acc = 0;
525 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
526 const auto& props = buf[i].props;
527
528 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
529 const auto track_before = i > line_span.fOffset
530 ? props.tracking * 0.5f : 0.0f,
531 track_after = i < line_span.fOffset + line_span.fCount - 1
532 ? props.tracking * 0.5f : 0.0f,
533 fragment_offset = align_offset + tracking_acc + track_before;
534
535 const auto& frag = fFragments[i];
536 const auto m = SkM44::Translate(line_offset.x + fragment_offset,
537 line_offset.y) *
538 frag.fMatrixNode->getMatrix();
539 frag.fMatrixNode->setMatrix(m);
540
541 tracking_acc += track_before + track_after;
542 }
543}
544
545} // namespace internal
546} // namespace skottie
547