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