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 if (fText->fHasFill) {
142 rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
143 rec.fFillColorNode->setAntiAlias(true);
144 draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
145 }
146 if (fText->fHasStroke) {
147 rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
148 rec.fStrokeColorNode->setAntiAlias(true);
149 rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
150 draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
151 }
152
153 SkASSERT(!draws.empty());
154
155 if (0) {
156 // enable to visualize fragment ascent boxes
157 auto box_color = sksg::Color::Make(0xff0000ff);
158 box_color->setStyle(SkPaint::kStroke_Style);
159 box_color->setStrokeWidth(1);
160 box_color->setAntiAlias(true);
161 auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
162 draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
163 }
164
165 auto draws_node = (draws.size() > 1)
166 ? sksg::Group::Make(std::move(draws))
167 : std::move(draws[0]);
168
169 if (fHasBlurAnimator) {
170 // Optional blur effect.
171 rec.fBlur = sksg::BlurImageFilter::Make();
172 draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
173 }
174
175 fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
176 fFragments.push_back(std::move(rec));
177}
178
179void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
180 fMaps.fNonWhitespaceMap.clear();
181 fMaps.fWordsMap.clear();
182 fMaps.fLinesMap.clear();
183
184 size_t i = 0,
185 line = 0,
186 line_start = 0,
187 word_start = 0;
188
189 float word_advance = 0,
190 word_ascent = 0,
191 line_advance = 0,
192 line_ascent = 0;
193
194 bool in_word = false;
195
196 // TODO: use ICU for building the word map?
197 for (; i < shape_result.fFragments.size(); ++i) {
198 const auto& frag = shape_result.fFragments[i];
199
200 if (frag.fIsWhitespace) {
201 if (in_word) {
202 in_word = false;
203 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
204 }
205 } else {
206 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
207
208 if (!in_word) {
209 in_word = true;
210 word_start = i;
211 word_advance = word_ascent = 0;
212 }
213
214 word_advance += frag.fAdvance;
215 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
216 }
217
218 if (frag.fLineIndex != line) {
219 SkASSERT(frag.fLineIndex == line + 1);
220 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
221 line = frag.fLineIndex;
222 line_start = i;
223 line_advance = line_ascent = 0;
224 }
225
226 line_advance += frag.fAdvance;
227 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
228 }
229
230 if (i > word_start) {
231 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
232 }
233
234 if (i > line_start) {
235 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
236 }
237}
238
239void TextAdapter::setText(const TextValue& txt) {
240 fText.fCurrentValue = txt;
241 this->onSync();
242}
243
244uint32_t TextAdapter::shaperFlags() const {
245 uint32_t flags = Shaper::Flags::kNone;
246
247 SkASSERT(!(fRequiresAnchorPoint && fAnimators.empty()));
248 if (!fAnimators.empty() ) flags |= Shaper::Flags::kFragmentGlyphs;
249 if (fRequiresAnchorPoint) flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
250
251 return flags;
252}
253
254void TextAdapter::reshape() {
255 const Shaper::TextDesc text_desc = {
256 fText->fTypeface,
257 fText->fTextSize,
258 fText->fLineHeight,
259 fText->fAscent,
260 fText->fHAlign,
261 fText->fVAlign,
262 fText->fResize,
263 this->shaperFlags(),
264 };
265 const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
266
267 if (fLogger && shape_result.fMissingGlyphCount > 0) {
268 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
269 shape_result.fMissingGlyphCount,
270 fText->fText.c_str());
271 fLogger->log(Logger::Level::kWarning, msg.c_str());
272
273 // This may trigger repeatedly when the text is animating.
274 // To avoid spamming, only log once.
275 fLogger = nullptr;
276 }
277
278 // Rebuild all fragments.
279 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
280
281 fRoot->clear();
282 fFragments.clear();
283
284 for (const auto& frag : shape_result.fFragments) {
285 this->addFragment(frag);
286 }
287
288 if (!fAnimators.empty()) {
289 // Range selectors require fragment domain maps.
290 this->buildDomainMaps(shape_result);
291 }
292
293#if (0)
294 // Enable for text box debugging/visualization.
295 auto box_color = sksg::Color::Make(0xffff0000);
296 box_color->setStyle(SkPaint::kStroke_Style);
297 box_color->setStrokeWidth(1);
298 box_color->setAntiAlias(true);
299
300 auto bounds_color = sksg::Color::Make(0xff00ff00);
301 bounds_color->setStyle(SkPaint::kStroke_Style);
302 bounds_color->setStrokeWidth(1);
303 bounds_color->setAntiAlias(true);
304
305 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText.fBox),
306 std::move(box_color)));
307 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeBounds()),
308 std::move(bounds_color)));
309#endif
310}
311
312void TextAdapter::onSync() {
313 if (!fText->fHasFill && !fText->fHasStroke) {
314 return;
315 }
316
317 if (fText.hasChanged()) {
318 this->reshape();
319 }
320
321 if (fFragments.empty()) {
322 return;
323 }
324
325 // Seed props from the current text value.
326 TextAnimator::ResolvedProps seed_props;
327 seed_props.fill_color = fText->fFillColor;
328 seed_props.stroke_color = fText->fStrokeColor;
329
330 TextAnimator::ModulatorBuffer buf;
331 buf.resize(fFragments.size(), { seed_props, 0 });
332
333 // Apply all animators to the modulator buffer.
334 for (const auto& animator : fAnimators) {
335 animator->modulateProps(fMaps, buf);
336 }
337
338 const TextAnimator::DomainMap* grouping_domain = nullptr;
339 switch (fAnchorPointGrouping) {
340 // for word/line grouping, we rely on domain map info
341 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
342 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
343 // remaining grouping modes (character/all) do not need (or have) domain map data
344 default: break;
345 }
346
347 size_t grouping_span_index = 0;
348
349 // Finally, push all props to their corresponding fragment.
350 for (const auto& line_span : fMaps.fLinesMap) {
351 float line_tracking = 0;
352 bool line_has_tracking = false;
353
354 // Tracking requires special treatment: unlike other props, its effect is not localized
355 // to a single fragment, but requires re-alignment of the whole line.
356 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
357 // Track the grouping domain span in parallel.
358 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
359 (*grouping_domain)[grouping_span_index].fCount) {
360 grouping_span_index += 1;
361 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
362 (*grouping_domain)[grouping_span_index].fCount);
363 }
364
365 const auto& props = buf[i].props;
366 const auto& frag = fFragments[i];
367 this->pushPropsToFragment(props, frag, fGroupingAlignment * .01f, // percentage
368 grouping_domain ? &(*grouping_domain)[grouping_span_index]
369 : nullptr);
370
371 line_tracking += props.tracking;
372 line_has_tracking |= !SkScalarNearlyZero(props.tracking);
373 }
374
375 if (line_has_tracking) {
376 this->adjustLineTracking(buf, line_span, line_tracking);
377 }
378 }
379}
380
381SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
382 const SkV2& grouping_alignment,
383 const TextAnimator::DomainSpan* grouping_span) const {
384 // Construct the following 2x ascent box:
385 //
386 // -------------
387 // | |
388 // | | ascent
389 // | |
390 // ----+-------------+---------- baseline
391 // (pos) |
392 // | | ascent
393 // | |
394 // -------------
395 // advance
396
397 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
398 // note: negative ascent
399 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
400 };
401
402 // Compute a grouping-dependent anchor point box.
403 // The default anchor point is at the center, and gets adjusted relative to the bounds
404 // based on |grouping_alignment|.
405 auto anchor_box = [&]() -> SkRect {
406 switch (fAnchorPointGrouping) {
407 case AnchorPointGrouping::kCharacter:
408 // Anchor box relative to each individual fragment.
409 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
410 case AnchorPointGrouping::kWord:
411 // Fall through
412 case AnchorPointGrouping::kLine: {
413 SkASSERT(grouping_span);
414 // Anchor box relative to the first fragment in the word/line.
415 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
416 return make_box(first_span_fragment.fOrigin,
417 grouping_span->fAdvance,
418 grouping_span->fAscent);
419 }
420 case AnchorPointGrouping::kAll:
421 // Anchor box is the same as the text box.
422 return fText->fBox;
423 }
424 SkUNREACHABLE;
425 };
426
427 const auto ab = anchor_box();
428
429 // Apply grouping alignment.
430 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
431 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
432
433 // The anchor point is relative to the fragment position.
434 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
435}
436
437void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
438 const FragmentRec& rec,
439 const SkV2& grouping_alignment,
440 const TextAnimator::DomainSpan* grouping_span) const {
441 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
442
443 rec.fMatrixNode->setMatrix(
444 SkM44::Translate(props.position.x + rec.fOrigin.x() + anchor_point.x,
445 props.position.y + rec.fOrigin.y() + anchor_point.y,
446 props.position.z)
447 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
448 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
449 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
450 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
451 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
452
453 const auto scale_alpha = [](SkColor c, float o) {
454 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
455 };
456
457 if (rec.fFillColorNode) {
458 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
459 }
460 if (rec.fStrokeColorNode) {
461 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
462 }
463 if (rec.fBlur) {
464 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
465 props.blur.y * kBlurSizeToSigma });
466 }
467}
468
469void TextAdapter::adjustLineTracking(const TextAnimator::ModulatorBuffer& buf,
470 const TextAnimator::DomainSpan& line_span,
471 float total_tracking) const {
472 SkASSERT(line_span.fCount > 0);
473
474 // AE tracking is defined per glyph, based on two components: |before| and |after|.
475 // BodyMovin only exports "balanced" tracking values, where before == after == tracking / 2.
476 //
477 // Tracking is applied as a local glyph offset, and contributes to the line width for alignment
478 // purposes.
479
480 // The first glyph does not contribute |before| tracking, and the last one does not contribute
481 // |after| tracking. Rather than spill this logic into applyAnimators, post-adjust here.
482 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
483 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
484
485 static const auto align_factor = [](SkTextUtils::Align a) {
486 switch (a) {
487 case SkTextUtils::kLeft_Align : return 0.0f;
488 case SkTextUtils::kCenter_Align: return -0.5f;
489 case SkTextUtils::kRight_Align : return -1.0f;
490 }
491
492 SkASSERT(false);
493 return 0.0f;
494 };
495
496 const auto align_offset = total_tracking * align_factor(fText->fHAlign);
497
498 float tracking_acc = 0;
499 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
500 const auto& props = buf[i].props;
501
502 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
503 const auto track_before = i > line_span.fOffset
504 ? props.tracking * 0.5f : 0.0f,
505 track_after = i < line_span.fOffset + line_span.fCount - 1
506 ? props.tracking * 0.5f : 0.0f,
507 fragment_offset = align_offset + tracking_acc + track_before;
508
509 const auto& frag = fFragments[i];
510 const auto m = SkM44::Translate(fragment_offset, 0) * frag.fMatrixNode->getMatrix();
511 frag.fMatrixNode->setMatrix(m);
512
513 tracking_acc += track_before + track_after;
514 }
515}
516
517} // namespace internal
518} // namespace skottie
519