2 * Copyright 2019 Google Inc.
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
8 #include "modules/skottie/src/text/TextAdapter.h"
10 #include "include/core/SkContourMeasure.h"
11 #include "include/core/SkFontMgr.h"
12 #include "include/core/SkM44.h"
13 #include "include/private/SkTPin.h"
14 #include "modules/skottie/src/SkottieJson.h"
15 #include "modules/skottie/src/text/RangeSelector.h"
16 #include "modules/skottie/src/text/TextAnimator.h"
17 #include "modules/sksg/include/SkSGDraw.h"
18 #include "modules/sksg/include/SkSGGroup.h"
19 #include "modules/sksg/include/SkSGPaint.h"
20 #include "modules/sksg/include/SkSGPath.h"
21 #include "modules/sksg/include/SkSGRect.h"
22 #include "modules/sksg/include/SkSGRenderEffect.h"
23 #include "modules/sksg/include/SkSGText.h"
24 #include "modules/sksg/include/SkSGTransform.h"
26 // Enable for text layout debugging.
27 #define SHOW_LAYOUT_BOXES 0
29 namespace skottie::internal {
31 static float align_factor(SkTextUtils::Align a) {
33 case SkTextUtils::kLeft_Align : return 0.0f;
34 case SkTextUtils::kCenter_Align: return 0.5f;
35 case SkTextUtils::kRight_Align : return 1.0f;
41 // Text path semantics
43 // * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
44 // a distance along the path
46 // * horizontal alignment is applied relative to the path start/end points
48 // * "Reverse Path" allows reversing the path direction
50 // * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
51 // to the path tangent, or not (just positioned).
53 // * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
54 // depending on horizontal alignement:
55 // - left: offset = first margin
56 // - center: offset = first margin + last margin
57 // - right: offset = last margin
59 // * extranormal path positions (d < 0, d > path len) are allowed
60 // - closed path: the position wraps around in both directions
61 // - open path: extrapolates from extremes' positions/slopes
63 struct TextAdapter::PathInfo {
65 ScalarValue fPathFMargin = 0,
67 fPathPerpendicular = 0,
70 void updateContourData() {
71 const auto reverse = fPathReverse != 0;
73 if (fPath != fCurrentPath || reverse != fCurrentReversed) {
74 // reinitialize cached contour data
75 auto path = static_cast<SkPath>(fPath);
78 reversed.reverseAddPath(path);
82 SkContourMeasureIter iter(path, /*forceClosed = */false);
83 fCurrentMeasure = iter.next();
84 fCurrentClosed = path.isLastContourClosed();
85 fCurrentReversed = reverse;
88 // AE paths are always single-contour (no moves allowed).
89 SkASSERT(!iter.next());
93 float pathLength() const {
94 SkASSERT(fPath == fCurrentPath);
95 SkASSERT((fPathReverse != 0) == fCurrentReversed);
97 return fCurrentMeasure ? fCurrentMeasure->length() : 0;
100 SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
101 SkASSERT(fPath == fCurrentPath);
102 SkASSERT((fPathReverse != 0) == fCurrentReversed);
104 if (!fCurrentMeasure) {
108 const auto path_len = fCurrentMeasure->length();
110 // First/last margin adjustment also depends on alignment.
112 case SkTextUtils::Align::kLeft_Align: distance += fPathFMargin; break;
113 case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
115 case SkTextUtils::Align::kRight_Align: distance += fPathLMargin; break;
118 // For closed paths, extranormal distances wrap around the contour.
119 if (fCurrentClosed) {
120 distance = std::fmod(distance, path_len);
122 distance += path_len;
124 SkASSERT(0 <= distance && distance <= path_len);
129 if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
133 // For open paths, extranormal distances are extrapolated from extremes.
135 // - getPosTan above clamps to the extremes
136 // - the extrapolation below only kicks in for extranormal values
137 const auto underflow = std::min(0.0f, distance),
138 overflow = std::max(0.0f, distance - path_len);
139 pos += tan*(underflow + overflow);
141 auto m = SkM44::Translate(pos.x(), pos.y());
143 // The "perpendicular" flag controls whether fragments are positioned and rotated,
144 // or just positioned.
145 if (fPathPerpendicular != 0) {
146 m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
153 // Cached contour data.
154 ShapeValue fCurrentPath;
155 sk_sp<SkContourMeasure> fCurrentMeasure;
156 bool fCurrentReversed = false,
157 fCurrentClosed = false;
160 sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
161 const AnimationBuilder* abuilder,
162 sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger) {
163 // General text node format:
165 // "a": [], // animators (see TextAnimator)
170 // "f": "Roboto-Regular",
180 // "t": "text align right",
187 // "m": { // more options
188 // "g": 1, // Anchor Point Grouping
189 // "a": {...} // Grouping Alignment
191 // "p": { // path options
192 // "a": 0, // force alignment
193 // "f": {}, // first margin
194 // "l": {}, // last margin
195 // "m": 1, // mask index
196 // "p": 1, // perpendicular
197 // "r": 0 // reverse path
202 const skjson::ObjectValue* jt = jlayer["t"];
203 const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
206 abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
211 const skjson::ObjectValue* jm = (*jt)["m"];
212 static constexpr AnchorPointGrouping gGroupingMap[] = {
213 AnchorPointGrouping::kCharacter, // 'g': 1
214 AnchorPointGrouping::kWord, // 'g': 2
215 AnchorPointGrouping::kLine, // 'g': 3
216 AnchorPointGrouping::kAll, // 'g': 4
219 ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, SK_ARRAY_COUNT(gGroupingMap))
222 auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
224 gGroupingMap[SkToSizeT(apg - 1)]));
226 adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
228 adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
232 if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
233 adapter->fAnimators.reserve(janimators->size());
235 for (const skjson::ObjectValue* janimator : *janimators) {
236 if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
237 adapter->fHasBlurAnimator |= animator->hasBlur();
238 adapter->fRequiresAnchorPoint |= animator->requiresAnchorPoint();
239 adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();
241 adapter->fAnimators.push_back(std::move(animator));
246 // Optional text path
247 const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
252 // the actual path is identified as an index in the layer mask stack
253 const auto mask_index =
254 ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
255 const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
256 if (!jmasks || mask_index >= jmasks->size()) {
260 const skjson::ObjectValue* mask = (*jmasks)[mask_index];
265 auto pinfo = std::make_unique<PathInfo>();
266 adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
267 adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
268 adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
269 adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
270 adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
272 // TODO: force align support
274 // Historically, these used to be exported as static properties.
275 // Attempt parsing both ways, for backward compat.
276 skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
277 skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
279 // Path positioning requires anchor point info.
280 adapter->fRequiresAnchorPoint = true;
285 adapter->fPathInfo = attach_path((*jt)["p"]);
287 abuilder->dispatchTextProperty(adapter);
292 TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger, AnchorPointGrouping apg)
293 : fRoot(sksg::Group::Make())
294 , fFontMgr(std::move(fontmgr))
295 , fLogger(std::move(logger))
296 , fAnchorPointGrouping(apg)
297 , fHasBlurAnimator(false)
298 , fRequiresAnchorPoint(false)
299 , fRequiresLineAdjustments(false) {}
301 TextAdapter::~TextAdapter() = default;
303 void TextAdapter::addFragment(const Shaper::Fragment& frag, float scale) {
304 // For a given shaped fragment, build a corresponding SG fragment:
306 // [TransformEffect] -> [Transform]
308 // [Draw] -> [TextBlob*] [FillPaint]
309 // [Draw] -> [TextBlob*] [StrokePaint]
311 // * where the blob node is shared
313 auto blob_node = sksg::TextBlob::Make(frag.fBlob);
316 rec.fOrigin = frag.fPos;
317 rec.fAdvance = frag.fAdvance;
318 rec.fAscent = frag.fAscent;
319 rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fPos.x(), frag.fPos.y()));
321 std::vector<sk_sp<sksg::RenderNode>> draws;
322 draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
324 SkASSERT(fText->fHasFill || fText->fHasStroke);
326 auto add_fill = [&]() {
327 if (fText->fHasFill) {
328 rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
329 rec.fFillColorNode->setAntiAlias(true);
330 draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
333 auto add_stroke = [&] {
334 if (fText->fHasStroke) {
335 rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
336 rec.fStrokeColorNode->setAntiAlias(true);
337 rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
338 rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * scale);
339 rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
340 draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
344 if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
352 SkASSERT(!draws.empty());
354 if (SHOW_LAYOUT_BOXES) {
355 // visualize fragment ascent boxes
356 auto box_color = sksg::Color::Make(0xff0000ff);
357 box_color->setStyle(SkPaint::kStroke_Style);
358 box_color->setStrokeWidth(1);
359 box_color->setAntiAlias(true);
360 auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
361 draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
364 auto draws_node = (draws.size() > 1)
365 ? sksg::Group::Make(std::move(draws))
366 : std::move(draws[0]);
368 if (fHasBlurAnimator) {
369 // Optional blur effect.
370 rec.fBlur = sksg::BlurImageFilter::Make();
371 draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
374 fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
375 fFragments.push_back(std::move(rec));
378 void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
379 fMaps.fNonWhitespaceMap.clear();
380 fMaps.fWordsMap.clear();
381 fMaps.fLinesMap.clear();
388 float word_advance = 0,
393 bool in_word = false;
395 // TODO: use ICU for building the word map?
396 for (; i < shape_result.fFragments.size(); ++i) {
397 const auto& frag = shape_result.fFragments[i];
399 if (frag.fIsWhitespace) {
402 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
405 fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
410 word_advance = word_ascent = 0;
413 word_advance += frag.fAdvance;
414 word_ascent = std::min(word_ascent, frag.fAscent); // negative ascent
417 if (frag.fLineIndex != line) {
418 SkASSERT(frag.fLineIndex == line + 1);
419 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
420 line = frag.fLineIndex;
422 line_advance = line_ascent = 0;
425 line_advance += frag.fAdvance;
426 line_ascent = std::min(line_ascent, frag.fAscent); // negative ascent
429 if (i > word_start) {
430 fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
433 if (i > line_start) {
434 fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
438 void TextAdapter::setText(const TextValue& txt) {
439 fText.fCurrentValue = txt;
443 uint32_t TextAdapter::shaperFlags() const {
444 uint32_t flags = Shaper::Flags::kNone;
446 // We need granular fragments (as opposed to consolidated blobs):
448 // - when positioning on a path
449 // - when clamping the number or lines (for accurate line count)
450 if (!fAnimators.empty() || fPathInfo || fText->fMaxLines) {
451 flags |= Shaper::Flags::kFragmentGlyphs;
454 if (fRequiresAnchorPoint) {
455 flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
461 void TextAdapter::reshape() {
462 const Shaper::TextDesc text_desc = {
475 fText->fCapitalization,
479 const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
482 if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
483 const auto msg = SkStringPrintf("Text layout failed for '%s'.",
484 fText->fText.c_str());
485 fLogger->log(Logger::Level::kError, msg.c_str());
487 // These may trigger repeatedly when the text is animating.
488 // To avoid spamming, only log once.
492 if (shape_result.fMissingGlyphCount > 0) {
493 const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
494 shape_result.fMissingGlyphCount,
495 fText->fText.c_str());
496 fLogger->log(Logger::Level::kWarning, msg.c_str());
501 // Rebuild all fragments.
502 // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
507 for (const auto& frag : shape_result.fFragments) {
508 this->addFragment(frag, shape_result.fScale);
511 if (!fAnimators.empty() || fPathInfo) {
512 // Range selectors and text paths require fragment domain maps.
513 this->buildDomainMaps(shape_result);
516 if (SHOW_LAYOUT_BOXES) {
517 auto box_color = sksg::Color::Make(0xffff0000);
518 box_color->setStyle(SkPaint::kStroke_Style);
519 box_color->setStrokeWidth(1);
520 box_color->setAntiAlias(true);
522 auto bounds_color = sksg::Color::Make(0xff00ff00);
523 bounds_color->setStyle(SkPaint::kStroke_Style);
524 bounds_color->setStrokeWidth(1);
525 bounds_color->setAntiAlias(true);
527 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
528 std::move(box_color)));
529 fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
530 std::move(bounds_color)));
533 auto path_color = sksg::Color::Make(0xffffff00);
534 path_color->setStyle(SkPaint::kStroke_Style);
535 path_color->setStrokeWidth(1);
536 path_color->setAntiAlias(true);
539 sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
540 std::move(path_color)));
545 void TextAdapter::onSync() {
546 if (!fText->fHasFill && !fText->fHasStroke) {
550 if (fText.hasChanged()) {
554 if (fFragments.empty()) {
558 // Update the path contour measure, if needed.
560 fPathInfo->updateContourData();
563 // Seed props from the current text value.
564 TextAnimator::ResolvedProps seed_props;
565 seed_props.fill_color = fText->fFillColor;
566 seed_props.stroke_color = fText->fStrokeColor;
568 TextAnimator::ModulatorBuffer buf;
569 buf.resize(fFragments.size(), { seed_props, 0 });
571 // Apply all animators to the modulator buffer.
572 for (const auto& animator : fAnimators) {
573 animator->modulateProps(fMaps, buf);
576 const TextAnimator::DomainMap* grouping_domain = nullptr;
577 switch (fAnchorPointGrouping) {
578 // for word/line grouping, we rely on domain map info
579 case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
580 case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
581 // remaining grouping modes (character/all) do not need (or have) domain map data
585 size_t grouping_span_index = 0;
586 SkV2 current_line_offset = { 0, 0 }; // cumulative line spacing
588 auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
589 const TextAnimator::DomainSpan& line_span) {
590 SkV2 total_spacing = {0,0};
591 float total_tracking = 0;
593 // Only compute these when needed.
594 if (fRequiresLineAdjustments) {
595 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
596 const auto& props = buf[i].props;
597 total_spacing += props.line_spacing;
598 total_tracking += props.tracking;
601 // The first glyph does not contribute |before| tracking, and the last one does not
602 // contribute |after| tracking.
603 total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
604 buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
607 return std::make_tuple(total_spacing, total_tracking);
610 // Finally, push all props to their corresponding fragment.
611 for (const auto& line_span : fMaps.fLinesMap) {
612 const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
613 const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
615 // line spacing of the first line is ignored (nothing to "space" against)
616 if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
617 // For each line, the actual spacing is an average of individual fragment spacing
618 // (to preserve the "line").
619 current_line_offset += line_spacing / line_span.fCount;
622 float tracking_acc = 0;
623 for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
624 // Track the grouping domain span in parallel.
625 if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
626 (*grouping_domain)[grouping_span_index].fCount) {
627 grouping_span_index += 1;
628 SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
629 (*grouping_domain)[grouping_span_index].fCount);
632 const auto& props = buf[i].props;
633 const auto& frag = fFragments[i];
635 // AE tracking is defined per glyph, based on two components: |before| and |after|.
636 // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
638 // Tracking is applied as a local glyph offset, and contributes to the line width for
639 // alignment purposes.
641 // No |before| tracking for the first glyph, nor |after| tracking for the last one.
642 const auto track_before = i > line_span.fOffset
643 ? props.tracking * 0.5f : 0.0f,
644 track_after = i < line_span.fOffset + line_span.fCount - 1
645 ? props.tracking * 0.5f : 0.0f;
647 const auto frag_offset = current_line_offset +
648 SkV2{align_offset + tracking_acc + track_before, 0};
650 tracking_acc += track_before + track_after;
652 this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
653 grouping_domain ? &(*grouping_domain)[grouping_span_index]
659 SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
660 const SkV2& grouping_alignment,
661 const TextAnimator::DomainSpan* grouping_span) const {
662 // Construct the following 2x ascent box:
668 // ----+-------------+---------- baseline
675 auto make_box = [](const SkPoint& pos, float advance, float ascent) {
676 // note: negative ascent
677 return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
680 // Compute a grouping-dependent anchor point box.
681 // The default anchor point is at the center, and gets adjusted relative to the bounds
682 // based on |grouping_alignment|.
683 auto anchor_box = [&]() -> SkRect {
684 switch (fAnchorPointGrouping) {
685 case AnchorPointGrouping::kCharacter:
686 // Anchor box relative to each individual fragment.
687 return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
688 case AnchorPointGrouping::kWord:
690 case AnchorPointGrouping::kLine: {
691 SkASSERT(grouping_span);
692 // Anchor box relative to the first fragment in the word/line.
693 const auto& first_span_fragment = fFragments[grouping_span->fOffset];
694 return make_box(first_span_fragment.fOrigin,
695 grouping_span->fAdvance,
696 grouping_span->fAscent);
698 case AnchorPointGrouping::kAll:
699 // Anchor box is the same as the text box.
705 const auto ab = anchor_box();
707 // Apply grouping alignment.
708 const auto ap = SkV2 { ab.centerX() + ab.width() * 0.5f * grouping_alignment.x,
709 ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
711 // The anchor point is relative to the fragment position.
712 return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
715 SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
716 const FragmentRec& rec, const SkV2& frag_offset) const {
718 props.position.x + rec.fOrigin.fX + frag_offset.x,
719 props.position.y + rec.fOrigin.fY + frag_offset.y,
724 return SkM44::Translate(pos.x, pos.y, pos.z);
727 // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
728 const auto align_offset =
729 align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
731 // Path positioning is based on the fragment position relative to the paragraph box
732 // upper-left corner:
734 // - the horizontal component determines the distance on path
736 // - the vertical component is post-applied after orienting on path
738 // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
740 const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
741 const auto path_distance = rel_pos.x + align_offset;
743 return fPathInfo->getMatrix(path_distance, fText->fHAlign)
744 * SkM44::Translate(0, rel_pos.y, pos.z);
747 void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
748 const FragmentRec& rec,
749 const SkV2& frag_offset,
750 const SkV2& grouping_alignment,
751 const TextAnimator::DomainSpan* grouping_span) const {
752 const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
754 rec.fMatrixNode->setMatrix(
755 this->fragmentMatrix(props, rec, anchor_point + frag_offset)
756 * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
757 * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
758 * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
759 * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
760 * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
762 const auto scale_alpha = [](SkColor c, float o) {
763 return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
766 if (rec.fFillColorNode) {
767 rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
769 if (rec.fStrokeColorNode) {
770 rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
773 rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
774 props.blur.y * kBlurSizeToSigma });
778 } // namespace skottie::internal