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