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 #include "modules/skottie/src/text/TextAdapter.h"
8
9 #include "include/core/SkCanvas.h"
10 #include "include/core/SkColor.h"
11 #include "include/core/SkContourMeasure.h"
12 #include "include/core/SkFont.h"
13 #include "include/core/SkFontMgr.h"
14 #include "include/core/SkM44.h"
15 #include "include/core/SkMatrix.h"
16 #include "include/core/SkPaint.h"
17 #include "include/core/SkPath.h"
18 #include "include/core/SkRect.h"
19 #include "include/core/SkScalar.h"
20 #include "include/core/SkSpan.h"
21 #include "include/core/SkString.h"
22 #include "include/core/SkTypes.h"
23 #include "include/private/base/SkTPin.h"
24 #include "include/private/base/SkTo.h"
25 #include "include/utils/SkTextUtils.h"
26 #include "modules/skottie/include/Skottie.h"
27 #include "modules/skottie/include/SkottieProperty.h"
28 #include "modules/skottie/src/SkottieJson.h"
29 #include "modules/skottie/src/SkottiePriv.h"
30 #include "modules/skottie/src/text/RangeSelector.h" // IWYU pragma: keep
31 #include "modules/skottie/src/text/TextAnimator.h"
32 #include "modules/sksg/include/SkSGDraw.h"
33 #include "modules/sksg/include/SkSGGeometryNode.h"
34 #include "modules/sksg/include/SkSGGroup.h"
35 #include "modules/sksg/include/SkSGPaint.h"
36 #include "modules/sksg/include/SkSGPath.h"
37 #include "modules/sksg/include/SkSGRect.h"
38 #include "modules/sksg/include/SkSGRenderEffect.h"
39 #include "modules/sksg/include/SkSGRenderNode.h"
40 #include "modules/sksg/include/SkSGTransform.h"
41 #include "modules/sksg/src/SkSGTransformPriv.h"
42 #include "modules/skshaper/include/SkShaper_factory.h"
43 #include "src/utils/SkJSON.h"
44
45 #include <algorithm>
46 #include <cmath>
47 #include <cstddef>
48 #include <limits>
49 #include <tuple>
50 #include <utility>
51
52 namespace sksg {
53 class InvalidationController;
54 }
55
56 // Enable for text layout debugging.
57 #define SHOW_LAYOUT_BOXES 0
58
59 namespace skottie::internal {
60
61 namespace {
62
63 class GlyphTextNode final : public sksg::GeometryNode {
64 public:
GlyphTextNode(Shaper::ShapedGlyphs && glyphs)65 explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
66
67 ~GlyphTextNode() override = default;
68
glyphs() const69 const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }
70
71 protected:
onRevalidate(sksg::InvalidationController *,const SkMatrix &)72 SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
73 return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
74 }
75
onDraw(SkCanvas * canvas,const SkPaint & paint) const76 void onDraw(SkCanvas* canvas, const SkPaint& paint) const override {
77 fGlyphs.draw(canvas, {0,0}, paint);
78 }
79
onClip(SkCanvas * canvas,bool antiAlias) const80 void onClip(SkCanvas* canvas, bool antiAlias) const override {
81 canvas->clipPath(this->asPath(), antiAlias);
82 }
83
onContains(const SkPoint & p) const84 bool onContains(const SkPoint& p) const override {
85 return this->asPath().contains(p.x(), p.y());
86 }
87
onAsPath() const88 SkPath onAsPath() const override {
89 // TODO
90 return SkPath();
91 }
92
93 private:
94 const Shaper::ShapedGlyphs fGlyphs;
95 };
96
align_factor(SkTextUtils::Align a)97 static float align_factor(SkTextUtils::Align a) {
98 switch (a) {
99 case SkTextUtils::kLeft_Align : return 0.0f;
100 case SkTextUtils::kCenter_Align: return 0.5f;
101 case SkTextUtils::kRight_Align : return 1.0f;
102 }
103
104 SkUNREACHABLE;
105 }
106
107 } // namespace
108
109 class TextAdapter::GlyphDecoratorNode final : public sksg::Group {
110 public:
GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator,float scale)111 GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator, float scale)
112 : fDecorator(std::move(decorator))
113 , fScale(scale)
114 {}
115
116 ~GlyphDecoratorNode() override = default;
117
updateFragmentData(const std::vector<TextAdapter::FragmentRec> & recs)118 void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) {
119 fFragCount = recs.size();
120
121 SkASSERT(!fFragInfo);
122 fFragInfo = std::make_unique<FragmentInfo[]>(recs.size());
123
124 for (size_t i = 0; i < recs.size(); ++i) {
125 const auto& rec = recs[i];
126 fFragInfo[i] = {rec.fGlyphs, rec.fMatrixNode, rec.fAdvance};
127 }
128
129 SkASSERT(!fDecoratorInfo);
130 fDecoratorInfo = std::make_unique<GlyphDecorator::GlyphInfo[]>(recs.size());
131 }
132
onRevalidate(sksg::InvalidationController * ic,const SkMatrix & ctm)133 SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
134 const auto child_bounds = INHERITED::onRevalidate(ic, ctm);
135
136 for (size_t i = 0; i < fFragCount; ++i) {
137 const auto* glyphs = fFragInfo[i].fGlyphs;
138 fDecoratorInfo[i].fBounds =
139 glyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
140 fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As<SkMatrix>(fFragInfo[i].fMatrixNode);
141
142 fDecoratorInfo[i].fCluster = glyphs->fClusters.empty() ? 0 : glyphs->fClusters.front();
143 fDecoratorInfo[i].fAdvance = fFragInfo[i].fAdvance;
144 }
145
146 return child_bounds;
147 }
148
onRender(SkCanvas * canvas,const RenderContext * ctx) const149 void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
150 auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
151 canvas->getTotalMatrix(),
152 true);
153 this->INHERITED::onRender(canvas, local_ctx);
154
155 fDecorator->onDecorate(canvas, {
156 SkSpan(fDecoratorInfo.get(), fFragCount),
157 fScale
158 });
159 }
160
161 private:
162 struct FragmentInfo {
163 const Shaper::ShapedGlyphs* fGlyphs;
164 sk_sp<sksg::Matrix<SkM44>> fMatrixNode;
165 float fAdvance;
166 };
167
168 const sk_sp<GlyphDecorator> fDecorator;
169 const float fScale;
170
171 std::unique_ptr<FragmentInfo[]> fFragInfo;
172 std::unique_ptr<GlyphDecorator::GlyphInfo[]> fDecoratorInfo;
173 size_t fFragCount;
174
175 using INHERITED = Group;
176 };
177
178 // Text path semantics
179 //
180 // * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
181 // a distance along the path
182 //
183 // * horizontal alignment is applied relative to the path start/end points
184 //
185 // * "Reverse Path" allows reversing the path direction
186 //
187 // * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
188 // to the path tangent, or not (just positioned).
189 //
190 // * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
191 // depending on horizontal alignement:
192 // - left: offset = first margin
193 // - center: offset = first margin + last margin
194 // - right: offset = last margin
195 //
196 // * extranormal path positions (d < 0, d > path len) are allowed
197 // - closed path: the position wraps around in both directions
198 // - open path: extrapolates from extremes' positions/slopes
199 //
200 struct TextAdapter::PathInfo {
201 ShapeValue fPath;
202 ScalarValue fPathFMargin = 0,
203 fPathLMargin = 0,
204 fPathPerpendicular = 0,
205 fPathReverse = 0;
206
updateContourDataskottie::internal::TextAdapter::PathInfo207 void updateContourData() {
208 const auto reverse = fPathReverse != 0;
209
210 if (fPath != fCurrentPath || reverse != fCurrentReversed) {
211 // reinitialize cached contour data
212 auto path = static_cast<SkPath>(fPath);
213 if (reverse) {
214 SkPath reversed;
215 reversed.reverseAddPath(path);
216 path = reversed;
217 }
218
219 SkContourMeasureIter iter(path, /*forceClosed = */false);
220 fCurrentMeasure = iter.next();
221 fCurrentClosed = path.isLastContourClosed();
222 fCurrentReversed = reverse;
223 fCurrentPath = fPath;
224
225 // AE paths are always single-contour (no moves allowed).
226 SkASSERT(!iter.next());
227 }
228 }
229
pathLengthskottie::internal::TextAdapter::PathInfo230 float pathLength() const {
231 SkASSERT(fPath == fCurrentPath);
232 SkASSERT((fPathReverse != 0) == fCurrentReversed);
233
234 return fCurrentMeasure ? fCurrentMeasure->length() : 0;
235 }
236
getMatrixskottie::internal::TextAdapter::PathInfo237 SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
238 SkASSERT(fPath == fCurrentPath);
239 SkASSERT((fPathReverse != 0) == fCurrentReversed);
240
241 if (!fCurrentMeasure) {
242 return SkM44();
243 }
244
245 const auto path_len = fCurrentMeasure->length();
246
247 // First/last margin adjustment also depends on alignment.
248 switch (alignment) {
249 case SkTextUtils::Align::kLeft_Align: distance += fPathFMargin; break;
250 case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
251 fPathLMargin; break;
252 case SkTextUtils::Align::kRight_Align: distance += fPathLMargin; break;
253 }
254
255 // For closed paths, extranormal distances wrap around the contour.
256 if (fCurrentClosed) {
257 distance = std::fmod(distance, path_len);
258 if (distance < 0) {
259 distance += path_len;
260 }
261 SkASSERT(0 <= distance && distance <= path_len);
262 }
263
264 SkPoint pos;
265 SkVector tan;
266 if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
267 return SkM44();
268 }
269
270 // For open paths, extranormal distances are extrapolated from extremes.
271 // Note:
272 // - getPosTan above clamps to the extremes
273 // - the extrapolation below only kicks in for extranormal values
274 const auto underflow = std::min(0.0f, distance),
275 overflow = std::max(0.0f, distance - path_len);
276 pos += tan*(underflow + overflow);
277
278 auto m = SkM44::Translate(pos.x(), pos.y());
279
280 // The "perpendicular" flag controls whether fragments are positioned and rotated,
281 // or just positioned.
282 if (fPathPerpendicular != 0) {
283 m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
284 }
285
286 return m;
287 }
288
289 private:
290 // Cached contour data.
291 ShapeValue fCurrentPath;
292 sk_sp<SkContourMeasure> fCurrentMeasure;
293 bool fCurrentReversed = false,
294 fCurrentClosed = false;
295 };
296
Make(const skjson::ObjectValue & jlayer,const AnimationBuilder * abuilder,sk_sp<SkFontMgr> fontmgr,sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,sk_sp<Logger> logger,sk_sp<::SkShapers::Factory> factory)297 sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
298 const AnimationBuilder* abuilder,
299 sk_sp<SkFontMgr> fontmgr,
300 sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
301 sk_sp<Logger> logger,
302 sk_sp<::SkShapers::Factory> factory) {
303 // General text node format:
304 // "t": {
305 // "a": [], // animators (see TextAnimator)
306 // "d": {
307 // "k": [
308 // {
309 // "s": {
310 // "f": "Roboto-Regular",
311 // "fc": [
312 // 0.42,
313 // 0.15,
314 // 0.15
315 // ],
316 // "j": 1,
317 // "lh": 60,
318 // "ls": 0,
319 // "s": 50,
320 // "t": "text align right",
321 // "tr": 0
322 // },
323 // "t": 0
324 // }
325 // ],
326 // "sid": "optionalSlotID"
327 // },
328 // "m": { // more options
329 // "g": 1, // Anchor Point Grouping
330 // "a": {...} // Grouping Alignment
331 // },
332 // "p": { // path options
333 // "a": 0, // force alignment
334 // "f": {}, // first margin
335 // "l": {}, // last margin
336 // "m": 1, // mask index
337 // "p": 1, // perpendicular
338 // "r": 0 // reverse path
339 // }
340
341 // },
342
343 const skjson::ObjectValue* jt = jlayer["t"];
344 const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
345 : nullptr;
346 if (!jd) {
347 abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
348 return nullptr;
349 }
350
351 // "More options"
352 const skjson::ObjectValue* jm = (*jt)["m"];
353 static constexpr AnchorPointGrouping gGroupingMap[] = {
354 AnchorPointGrouping::kCharacter, // 'g': 1
355 AnchorPointGrouping::kWord, // 'g': 2
356 AnchorPointGrouping::kLine, // 'g': 3
357 AnchorPointGrouping::kAll, // 'g': 4
358 };
359 const auto apg = jm
360 ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, std::size(gGroupingMap))
361 : 1;
362
363 auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
364 std::move(custom_glyph_mapper),
365 std::move(logger),
366 std::move(factory),
367 gGroupingMap[SkToSizeT(apg - 1)]));
368
369 adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
370 if (jm) {
371 adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
372 }
373
374 // Animators
375 if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
376 adapter->fAnimators.reserve(janimators->size());
377
378 for (const skjson::ObjectValue* janimator : *janimators) {
379 if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
380 adapter->fHasBlurAnimator |= animator->hasBlur();
381 adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint();
382 adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();
383
384 adapter->fAnimators.push_back(std::move(animator));
385 }
386 }
387 }
388
389 // Optional text path
390 const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
391 if (!jpath) {
392 return nullptr;
393 }
394
395 // the actual path is identified as an index in the layer mask stack
396 const auto mask_index =
397 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
398 const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
399 if (!jmasks || mask_index >= jmasks->size()) {
400 return nullptr;
401 }
402
403 const skjson::ObjectValue* mask = (*jmasks)[mask_index];
404 if (!mask) {
405 return nullptr;
406 }
407
408 auto pinfo = std::make_unique<PathInfo>();
409 adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
410 adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
411 adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
412 adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
413 adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
414
415 // TODO: force align support
416
417 // Historically, these used to be exported as static properties.
418 // Attempt parsing both ways, for backward compat.
419 skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
420 skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
421
422 // Path positioning requires anchor point info.
423 adapter->fRequiresAnchorPoint = true;
424
425 return pinfo;
426 };
427
428 adapter->fPathInfo = attach_path((*jt)["p"]);
429 abuilder->dispatchTextProperty(adapter, jd);
430
431 return adapter;
432 }
433
TextAdapter(sk_sp<SkFontMgr> fontmgr,sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,sk_sp<Logger> logger,sk_sp<SkShapers::Factory> factory,AnchorPointGrouping apg)434 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
435 sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
436 sk_sp<Logger> logger,
437 sk_sp<SkShapers::Factory> factory,
438 AnchorPointGrouping apg)
439 : fRoot(sksg::Group::Make())
440 , fFontMgr(std::move(fontmgr))
441 , fCustomGlyphMapper(std::move(custom_glyph_mapper))
442 , fLogger(std::move(logger))
443 , fShapingFactory(std::move(factory))
444 , fAnchorPointGrouping(apg)
445 , fHasBlurAnimator(false)
446 , fRequiresAnchorPoint(false)
447 , fRequiresLineAdjustments(false) {}
448
449 TextAdapter::~TextAdapter() = default;
450
451 std::vector<sk_sp<sksg::RenderNode>>
buildGlyphCompNodes(Shaper::ShapedGlyphs & glyphs) const452 TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const {
453 std::vector<sk_sp<sksg::RenderNode>> draws;
454
455 if (fCustomGlyphMapper) {
456 size_t run_offset = 0;
457 for (auto& run : glyphs.fRuns) {
458 for (size_t i = 0; i < run.fSize; ++i) {
459 const size_t goffset = run_offset + i;
460 const SkGlyphID gid = glyphs.fGlyphIDs[goffset];
461
462 if (auto gcomp = fCustomGlyphMapper->getGlyphComp(run.fFont.getTypeface(), gid)) {
463 // Position and scale the "glyph".
464 const auto m = SkMatrix::Translate(glyphs.fGlyphPos[goffset])
465 * SkMatrix::Scale(fText->fTextSize*fTextShapingScale,
466 fText->fTextSize*fTextShapingScale);
467
468 draws.push_back(sksg::TransformEffect::Make(std::move(gcomp), m));
469
470 // Remove all related data from the fragment, so we don't attempt to render
471 // this as a regular glyph.
472 SkASSERT(glyphs.fGlyphIDs.size() > goffset);
473 glyphs.fGlyphIDs.erase(glyphs.fGlyphIDs.begin() + goffset);
474 SkASSERT(glyphs.fGlyphPos.size() > goffset);
475 glyphs.fGlyphPos.erase(glyphs.fGlyphPos.begin() + goffset);
476 if (!glyphs.fClusters.empty()) {
477 SkASSERT(glyphs.fClusters.size() > goffset);
478 glyphs.fClusters.erase(glyphs.fClusters.begin() + goffset);
479 }
480 i -= 1;
481 run.fSize -= 1;
482 }
483 }
484 run_offset += run.fSize;
485 }
486 }
487
488 return draws;
489 }
490
addFragment(Shaper::Fragment & frag,sksg::Group * container)491 void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) {
492 // For a given shaped fragment, build a corresponding SG fragment:
493 //
494 // [TransformEffect] -> [Transform]
495 // [Group]
496 // [Draw] -> [GlyphTextNode*] [FillPaint] // SkTypeface-based glyph.
497 // [Draw] -> [GlyphTextNode*] [StrokePaint] // SkTypeface-based glyph.
498 // [CompRenderTree] // Comp glyph.
499 // ...
500 //
501
502 FragmentRec rec;
503 rec.fOrigin = frag.fOrigin;
504 rec.fAdvance = frag.fAdvance;
505 rec.fAscent = frag.fAscent;
506 rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fOrigin.x(),
507 frag.fOrigin.y()));
508
509 // Start off substituting existing comp nodes for all composition-based glyphs.
510 std::vector<sk_sp<sksg::RenderNode>> draws = this->buildGlyphCompNodes(frag.fGlyphs);
511
512 // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface).
513 auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs));
514 rec.fGlyphs = text_node->glyphs();
515
516 draws.reserve(draws.size() +
517 static_cast<size_t>(fText->fHasFill) +
518 static_cast<size_t>(fText->fHasStroke));
519
520 SkASSERT(fText->fHasFill || fText->fHasStroke);
521
522 auto add_fill = [&]() {
523 if (fText->fHasFill) {
524 rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
525 rec.fFillColorNode->setAntiAlias(true);
526 draws.push_back(sksg::Draw::Make(text_node, rec.fFillColorNode));
527 }
528 };
529 auto add_stroke = [&] {
530 if (fText->fHasStroke) {
531 rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
532 rec.fStrokeColorNode->setAntiAlias(true);
533 rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
534 rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * fTextShapingScale);
535 rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
536 draws.push_back(sksg::Draw::Make(text_node, rec.fStrokeColorNode));
537 }
538 };
539
540 if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
541 add_fill();
542 add_stroke();
543 } else {
544 add_stroke();
545 add_fill();
546 }
547
548 SkASSERT(!draws.empty());
549
550 if (SHOW_LAYOUT_BOXES) {
551 // visualize fragment ascent boxes
552 auto box_color = sksg::Color::Make(0xff0000ff);
553 box_color->setStyle(SkPaint::kStroke_Style);
554 box_color->setStrokeWidth(1);
555 box_color->setAntiAlias(true);
556 auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
557 draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
558 }
559
560 draws.shrink_to_fit();
561
562 auto draws_node = (draws.size() > 1)
563 ? sksg::Group::Make(std::move(draws))
564 : std::move(draws[0]);
565
566 if (fHasBlurAnimator) {
567 // Optional blur effect.
568 rec.fBlur = sksg::BlurImageFilter::Make();
569 draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
570 }
571
572 container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
573 fFragments.push_back(std::move(rec));
574 }
575
buildDomainMaps(const Shaper::Result & shape_result)576 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
577 fMaps.fNonWhitespaceMap.clear();
578 fMaps.fWordsMap.clear();
579 fMaps.fLinesMap.clear();
580
581 size_t i = 0,
582 line = 0,
583 line_start = 0,
584 word_start = 0;
585
586 float word_advance = 0,
587 word_ascent = 0,
588 line_advance = 0,
589 line_ascent = 0;
590
591 bool in_word = false;
592
593 // TODO: use ICU for building the word map?
594 for (; i < shape_result.fFragments.size(); ++i) {
595 const auto& frag = shape_result.fFragments[i];
596 const bool is_new_line = frag.fLineIndex != line;
597
598 if (frag.fIsWhitespace || is_new_line) {
599 // Both whitespace and new lines break words.
600 if (in_word) {
601 in_word = false;
602 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
603 }
604 }
605
606 if (!frag.fIsWhitespace) {
607 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
608
609 if (!in_word) {
610 in_word = true;
611 word_start = i;
612 word_advance = word_ascent = 0;
613 }
614
615 word_advance += frag.fAdvance;
616 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
617 }
618
619 if (is_new_line) {
620 SkASSERT(frag.fLineIndex == line + 1);
621 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
622 line = frag.fLineIndex;
623 line_start = i;
624 line_advance = line_ascent = 0;
625 }
626
627 line_advance += frag.fAdvance;
628 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
629 }
630
631 if (i > word_start) {
632 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
633 }
634
635 if (i > line_start) {
636 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
637 }
638 }
639
setText(const TextValue & txt)640 void TextAdapter::setText(const TextValue& txt) {
641 fText.fCurrentValue = txt;
642 this->onSync();
643 }
644
shaperFlags() const645 uint32_t TextAdapter::shaperFlags() const {
646 uint32_t flags = Shaper::Flags::kNone;
647
648 // We need granular fragments (as opposed to consolidated blobs):
649 // - when animating
650 // - when positioning on a path
651 // - when clamping the number or lines (for accurate line count)
652 // - when a text decorator is present
653 if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
654 flags |= Shaper::Flags::kFragmentGlyphs;
655 }
656
657 if (fRequiresAnchorPoint || fText->fDecorator) {
658 flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
659 }
660
661 if (fText->fDecorator) {
662 flags |= Shaper::Flags::kClusters;
663 }
664
665 return flags;
666 }
667
reshape()668 void TextAdapter::reshape() {
669 // AE clamps the font size to a reasonable range.
670 // We do the same, since HB is susceptible to int overflows for degenerate values.
671 static constexpr float kMinSize = 0.1f,
672 kMaxSize = 1296.0f;
673 const Shaper::TextDesc text_desc = {
674 fText->fTypeface,
675 SkTPin(fText->fTextSize, kMinSize, kMaxSize),
676 SkTPin(fText->fMinTextSize, kMinSize, kMaxSize),
677 SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize),
678 fText->fLineHeight,
679 fText->fLineShift,
680 fText->fAscent,
681 fText->fHAlign,
682 fText->fVAlign,
683 fText->fResize,
684 fText->fLineBreak,
685 fText->fDirection,
686 fText->fCapitalization,
687 fText->fMaxLines,
688 this->shaperFlags(),
689 fText->fLocale.isEmpty() ? nullptr : fText->fLocale.c_str(),
690 fText->fFontFamily.isEmpty() ? nullptr : fText->fFontFamily.c_str(),
691 };
692 auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr,
693 fShapingFactory);
694
695 if (fLogger) {
696 if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
697 const auto msg = SkStringPrintf("Text layout failed for '%s'.",
698 fText->fText.c_str());
699 fLogger->log(Logger::Level::kError, msg.c_str());
700
701 // These may trigger repeatedly when the text is animating.
702 // To avoid spamming, only log once.
703 fLogger = nullptr;
704 }
705
706 if (shape_result.fMissingGlyphCount > 0) {
707 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
708 shape_result.fMissingGlyphCount,
709 fText->fText.c_str());
710 fLogger->log(Logger::Level::kWarning, msg.c_str());
711 fLogger = nullptr;
712 }
713 }
714
715 // Save the text shaping scale for later adjustments.
716 fTextShapingScale = shape_result.fScale;
717
718 // Rebuild all fragments.
719 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
720
721 fRoot->clear();
722 fFragments.clear();
723
724 if (SHOW_LAYOUT_BOXES) {
725 auto box_color = sksg::Color::Make(0xffff0000);
726 box_color->setStyle(SkPaint::kStroke_Style);
727 box_color->setStrokeWidth(1);
728 box_color->setAntiAlias(true);
729
730 auto bounds_color = sksg::Color::Make(0xff00ff00);
731 bounds_color->setStyle(SkPaint::kStroke_Style);
732 bounds_color->setStrokeWidth(1);
733 bounds_color->setAntiAlias(true);
734
735 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
736 std::move(box_color)));
737 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
738 std::move(bounds_color)));
739
740 if (fPathInfo) {
741 auto path_color = sksg::Color::Make(0xffffff00);
742 path_color->setStyle(SkPaint::kStroke_Style);
743 path_color->setStrokeWidth(1);
744 path_color->setAntiAlias(true);
745
746 fRoot->addChild(
747 sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
748 std::move(path_color)));
749 }
750 }
751
752 // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
753 // directly to the root group, or to an intermediate GlyphDecoratorNode container.
754 sksg::Group* container = fRoot.get();
755 sk_sp<GlyphDecoratorNode> decorator_node;
756 if (fText->fDecorator) {
757 decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator, fTextShapingScale);
758 container = decorator_node.get();
759 }
760
761 // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
762 // metrics are valid after this block.
763 for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
764 this->addFragment(shape_result.fFragments[i], container);
765 }
766
767 if (decorator_node) {
768 decorator_node->updateFragmentData(fFragments);
769 fRoot->addChild(std::move(decorator_node));
770 }
771
772 if (!fAnimators.empty() || fPathInfo) {
773 // Range selectors and text paths require fragment domain maps.
774 this->buildDomainMaps(shape_result);
775 }
776 }
777
onSync()778 void TextAdapter::onSync() {
779 if (!fText->fHasFill && !fText->fHasStroke) {
780 return;
781 }
782
783 if (fText.hasChanged()) {
784 this->reshape();
785 }
786
787 if (fFragments.empty()) {
788 return;
789 }
790
791 // Update the path contour measure, if needed.
792 if (fPathInfo) {
793 fPathInfo->updateContourData();
794 }
795
796 // Seed props from the current text value.
797 TextAnimator::ResolvedProps seed_props;
798 seed_props.fill_color = fText->fFillColor;
799 seed_props.stroke_color = fText->fStrokeColor;
800 seed_props.stroke_width = fText->fStrokeWidth;
801
802 TextAnimator::ModulatorBuffer buf;
803 buf.resize(fFragments.size(), { seed_props, 0 });
804
805 // Apply all animators to the modulator buffer.
806 for (const auto& animator : fAnimators) {
807 animator->modulateProps(fMaps, buf);
808 }
809
810 const TextAnimator::DomainMap* grouping_domain = nullptr;
811 switch (fAnchorPointGrouping) {
812 // for word/line grouping, we rely on domain map info
813 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
814 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
815 // remaining grouping modes (character/all) do not need (or have) domain map data
816 default: break;
817 }
818
819 size_t grouping_span_index = 0;
820 SkV2 current_line_offset = { 0, 0 }; // cumulative line spacing
821
822 auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
823 const TextAnimator::DomainSpan& line_span) {
824 SkV2 total_spacing = {0,0};
825 float total_tracking = 0;
826
827 // Only compute these when needed.
828 if (fRequiresLineAdjustments && line_span.fCount) {
829 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
830 const auto& props = buf[i].props;
831 total_spacing += props.line_spacing;
832 total_tracking += props.tracking;
833 }
834
835 // The first glyph does not contribute |before| tracking, and the last one does not
836 // contribute |after| tracking.
837 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
838 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
839 }
840
841 return std::make_tuple(total_spacing, total_tracking);
842 };
843
844 // Finally, push all props to their corresponding fragment.
845 for (const auto& line_span : fMaps.fLinesMap) {
846 const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
847 const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
848
849 // line spacing of the first line is ignored (nothing to "space" against)
850 if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
851 // For each line, the actual spacing is an average of individual fragment spacing
852 // (to preserve the "line").
853 current_line_offset += line_spacing / line_span.fCount;
854 }
855
856 float tracking_acc = 0;
857 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
858 // Track the grouping domain span in parallel.
859 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
860 (*grouping_domain)[grouping_span_index].fCount) {
861 grouping_span_index += 1;
862 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
863 (*grouping_domain)[grouping_span_index].fCount);
864 }
865
866 const auto& props = buf[i].props;
867 const auto& frag = fFragments[i];
868
869 // AE tracking is defined per glyph, based on two components: |before| and |after|.
870 // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
871 //
872 // Tracking is applied as a local glyph offset, and contributes to the line width for
873 // alignment purposes.
874 //
875 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
876 const auto track_before = i > line_span.fOffset
877 ? props.tracking * 0.5f : 0.0f,
878 track_after = i < line_span.fOffset + line_span.fCount - 1
879 ? props.tracking * 0.5f : 0.0f;
880
881 const auto frag_offset = current_line_offset +
882 SkV2{align_offset + tracking_acc + track_before, 0};
883
884 tracking_acc += track_before + track_after;
885
886 this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
887 grouping_domain ? &(*grouping_domain)[grouping_span_index]
888 : nullptr);
889 }
890 }
891 }
892
fragmentAnchorPoint(const FragmentRec & rec,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const893 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
894 const SkV2& grouping_alignment,
895 const TextAnimator::DomainSpan* grouping_span) const {
896 // Construct the following 2x ascent box:
897 //
898 // -------------
899 // | |
900 // | | ascent
901 // | |
902 // ----+-------------+---------- baseline
903 // (pos) |
904 // | | ascent
905 // | |
906 // -------------
907 // advance
908
909 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
910 // note: negative ascent
911 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
912 };
913
914 // Compute a grouping-dependent anchor point box.
915 // The default anchor point is at the center, and gets adjusted relative to the bounds
916 // based on |grouping_alignment|.
917 auto anchor_box = [&]() -> SkRect {
918 switch (fAnchorPointGrouping) {
919 case AnchorPointGrouping::kCharacter:
920 // Anchor box relative to each individual fragment.
921 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
922 case AnchorPointGrouping::kWord:
923 // Fall through
924 case AnchorPointGrouping::kLine: {
925 SkASSERT(grouping_span);
926 // Anchor box relative to the first fragment in the word/line.
927 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
928 return make_box(first_span_fragment.fOrigin,
929 grouping_span->fAdvance,
930 grouping_span->fAscent);
931 }
932 case AnchorPointGrouping::kAll:
933 // Anchor box is the same as the text box.
934 return fText->fBox;
935 }
936 SkUNREACHABLE;
937 };
938
939 const auto ab = anchor_box();
940
941 // Apply grouping alignment.
942 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
943 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
944
945 // The anchor point is relative to the fragment position.
946 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
947 }
948
fragmentMatrix(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & frag_offset) const949 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
950 const FragmentRec& rec, const SkV2& frag_offset) const {
951 const SkV3 pos = {
952 props.position.x + rec.fOrigin.fX + frag_offset.x,
953 props.position.y + rec.fOrigin.fY + frag_offset.y,
954 props.position.z
955 };
956
957 if (!fPathInfo) {
958 return SkM44::Translate(pos.x, pos.y, pos.z);
959 }
960
961 // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
962 const auto align_offset =
963 align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
964
965 // Path positioning is based on the fragment position relative to the paragraph box
966 // upper-left corner:
967 //
968 // - the horizontal component determines the distance on path
969 //
970 // - the vertical component is post-applied after orienting on path
971 //
972 // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
973 //
974 const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
975 const auto path_distance = rel_pos.x + align_offset;
976
977 return fPathInfo->getMatrix(path_distance, fText->fHAlign)
978 * SkM44::Translate(0, rel_pos.y, pos.z);
979 }
980
pushPropsToFragment(const TextAnimator::ResolvedProps & props,const FragmentRec & rec,const SkV2 & frag_offset,const SkV2 & grouping_alignment,const TextAnimator::DomainSpan * grouping_span) const981 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
982 const FragmentRec& rec,
983 const SkV2& frag_offset,
984 const SkV2& grouping_alignment,
985 const TextAnimator::DomainSpan* grouping_span) const {
986 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
987
988 rec.fMatrixNode->setMatrix(
989 this->fragmentMatrix(props, rec, anchor_point + frag_offset)
990 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
991 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
992 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
993 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
994 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
995
996 const auto scale_alpha = [](SkColor c, float o) {
997 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
998 };
999
1000 if (rec.fFillColorNode) {
1001 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
1002 }
1003 if (rec.fStrokeColorNode) {
1004 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
1005 rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale);
1006 }
1007 if (rec.fBlur) {
1008 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
1009 props.blur.y * kBlurSizeToSigma });
1010 }
1011 }
1012
1013 } // namespace skottie::internal
1014