1/*
2 * Copyright 2018 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
9
10#include "include/core/SkPath.h"
11#include "modules/skottie/src/SkottieJson.h"
12#include "modules/skottie/src/SkottiePriv.h"
13#include "modules/skottie/src/SkottieValue.h"
14#include "modules/sksg/include/SkSGDraw.h"
15#include "modules/sksg/include/SkSGGeometryTransform.h"
16#include "modules/sksg/include/SkSGGroup.h"
17#include "modules/sksg/include/SkSGMerge.h"
18#include "modules/sksg/include/SkSGPaint.h"
19#include "modules/sksg/include/SkSGPath.h"
20#include "modules/sksg/include/SkSGRect.h"
21#include "modules/sksg/include/SkSGRenderEffect.h"
22#include "modules/sksg/include/SkSGRoundEffect.h"
23#include "modules/sksg/include/SkSGTransform.h"
24#include "modules/sksg/include/SkSGTrimEffect.h"
25#include "src/utils/SkJSON.h"
26
27#include <algorithm>
28#include <iterator>
29
30namespace skottie {
31namespace internal {
32
33namespace {
34
35using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const skjson::ObjectValue&,
36 const AnimationBuilder*);
37static constexpr GeometryAttacherT gGeometryAttachers[] = {
38 ShapeBuilder::AttachPathGeometry,
39 ShapeBuilder::AttachRRectGeometry,
40 ShapeBuilder::AttachEllipseGeometry,
41 ShapeBuilder::AttachPolystarGeometry,
42};
43
44using GeometryEffectAttacherT =
45 std::vector<sk_sp<sksg::GeometryNode>> (*)(const skjson::ObjectValue&,
46 const AnimationBuilder*,
47 std::vector<sk_sp<sksg::GeometryNode>>&&);
48static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
49 ShapeBuilder::AttachMergeGeometryEffect,
50 ShapeBuilder::AttachTrimGeometryEffect,
51 ShapeBuilder::AttachRoundGeometryEffect,
52};
53
54using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&,
55 const AnimationBuilder*);
56static constexpr PaintAttacherT gPaintAttachers[] = {
57 ShapeBuilder::AttachColorFill,
58 ShapeBuilder::AttachColorStroke,
59 ShapeBuilder::AttachGradientFill,
60 ShapeBuilder::AttachGradientStroke,
61};
62
63// Some paint types (looking at you dashed-stroke) mess with the local geometry.
64static constexpr GeometryEffectAttacherT gPaintGeometryAdjusters[] = {
65 nullptr, // color fill
66 ShapeBuilder::AdjustStrokeGeometry, // color stroke
67 nullptr, // gradient fill
68 ShapeBuilder::AdjustStrokeGeometry, // gradient stroke
69};
70static_assert(SK_ARRAY_COUNT(gPaintGeometryAdjusters) == SK_ARRAY_COUNT(gPaintAttachers), "");
71
72using DrawEffectAttacherT =
73 std::vector<sk_sp<sksg::RenderNode>> (*)(const skjson::ObjectValue&,
74 const AnimationBuilder*,
75 std::vector<sk_sp<sksg::RenderNode>>&&);
76
77static constexpr DrawEffectAttacherT gDrawEffectAttachers[] = {
78 ShapeBuilder::AttachRepeaterDrawEffect,
79};
80
81enum class ShapeType {
82 kGeometry,
83 kGeometryEffect,
84 kPaint,
85 kGroup,
86 kTransform,
87 kDrawEffect,
88};
89
90struct ShapeInfo {
91 const char* fTypeString;
92 ShapeType fShapeType;
93 uint32_t fAttacherIndex; // index into respective attacher tables
94};
95
96const ShapeInfo* FindShapeInfo(const skjson::ObjectValue& jshape) {
97 static constexpr ShapeInfo gShapeInfo[] = {
98 { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry
99 { "fl", ShapeType::kPaint , 0 }, // fill -> AttachColorFill
100 { "gf", ShapeType::kPaint , 2 }, // gfill -> AttachGradientFill
101 { "gr", ShapeType::kGroup , 0 }, // group -> Inline handler
102 { "gs", ShapeType::kPaint , 3 }, // gstroke -> AttachGradientStroke
103 { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect
104 { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry
105 { "rd", ShapeType::kGeometryEffect, 2 }, // round -> AttachRoundGeometryEffect
106 { "rp", ShapeType::kDrawEffect , 0 }, // repeater -> AttachRepeaterDrawEffect
107 { "sh", ShapeType::kGeometry , 0 }, // shape -> AttachPathGeometry
108 { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry
109 { "st", ShapeType::kPaint , 1 }, // stroke -> AttachColorStroke
110 { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect
111 { "tr", ShapeType::kTransform , 0 }, // transform -> Inline handler
112 };
113
114 const skjson::StringValue* type = jshape["ty"];
115 if (!type) {
116 return nullptr;
117 }
118
119 const auto* info = bsearch(type->begin(),
120 gShapeInfo,
121 SK_ARRAY_COUNT(gShapeInfo),
122 sizeof(ShapeInfo),
123 [](const void* key, const void* info) {
124 return strcmp(static_cast<const char*>(key),
125 static_cast<const ShapeInfo*>(info)->fTypeString);
126 });
127
128 return static_cast<const ShapeInfo*>(info);
129}
130
131struct GeometryEffectRec {
132 const skjson::ObjectValue& fJson;
133 GeometryEffectAttacherT fAttach;
134};
135
136} // namespace
137
138sk_sp<sksg::GeometryNode> ShapeBuilder::AttachPathGeometry(const skjson::ObjectValue& jpath,
139 const AnimationBuilder* abuilder) {
140 return abuilder->attachPath(jpath["ks"]);
141}
142
143struct AnimationBuilder::AttachShapeContext {
144 AttachShapeContext(std::vector<sk_sp<sksg::GeometryNode>>* geos,
145 std::vector<GeometryEffectRec>* effects,
146 size_t committedAnimators)
147 : fGeometryStack(geos)
148 , fGeometryEffectStack(effects)
149 , fCommittedAnimators(committedAnimators) {}
150
151 std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack;
152 std::vector<GeometryEffectRec>* fGeometryEffectStack;
153 size_t fCommittedAnimators;
154};
155
156sk_sp<sksg::RenderNode> AnimationBuilder::attachShape(const skjson::ArrayValue* jshape,
157 AttachShapeContext* ctx) const {
158 if (!jshape)
159 return nullptr;
160
161 SkDEBUGCODE(const auto initialGeometryEffects = ctx->fGeometryEffectStack->size();)
162
163 const skjson::ObjectValue* jtransform = nullptr;
164
165 struct ShapeRec {
166 const skjson::ObjectValue& fJson;
167 const ShapeInfo& fInfo;
168 };
169
170 // First pass (bottom->top):
171 //
172 // * pick up the group transform and opacity
173 // * push local geometry effects onto the stack
174 // * store recs for next pass
175 //
176 std::vector<ShapeRec> recs;
177 for (size_t i = 0; i < jshape->size(); ++i) {
178 const skjson::ObjectValue* shape = (*jshape)[jshape->size() - 1 - i];
179 if (!shape) continue;
180
181 const auto* info = FindShapeInfo(*shape);
182 if (!info) {
183 this->log(Logger::Level::kError, &(*shape)["ty"], "Unknown shape.");
184 continue;
185 }
186
187 if (ParseDefault<bool>((*shape)["hd"], false)) {
188 // Ignore hidden shapes.
189 continue;
190 }
191
192 recs.push_back({ *shape, *info });
193
194 switch (info->fShapeType) {
195 case ShapeType::kTransform:
196 // Just track the transform property for now -- we'll deal with it later.
197 jtransform = shape;
198 break;
199 case ShapeType::kGeometryEffect:
200 SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
201 ctx->fGeometryEffectStack->push_back(
202 { *shape, gGeometryEffectAttachers[info->fAttacherIndex] });
203 break;
204 default:
205 break;
206 }
207 }
208
209 // Second pass (top -> bottom, after 2x reverse):
210 //
211 // * track local geometry
212 // * emit local paints
213 //
214 std::vector<sk_sp<sksg::GeometryNode>> geos;
215 std::vector<sk_sp<sksg::RenderNode >> draws;
216
217 const auto add_draw = [this, &draws](sk_sp<sksg::RenderNode> draw, const ShapeRec& rec) {
218 // All draws can have an optional blend mode.
219 draws.push_back(this->attachBlendMode(rec.fJson, std::move(draw)));
220 };
221
222 for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) {
223 const AutoPropertyTracker apt(this, rec->fJson);
224
225 switch (rec->fInfo.fShapeType) {
226 case ShapeType::kGeometry: {
227 SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
228 if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this)) {
229 geos.push_back(std::move(geo));
230 }
231 } break;
232 case ShapeType::kGeometryEffect: {
233 // Apply the current effect and pop from the stack.
234 SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
235 if (!geos.empty()) {
236 geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
237 this,
238 std::move(geos));
239 }
240
241 SkASSERT(&ctx->fGeometryEffectStack->back().fJson == &rec->fJson);
242 SkASSERT(ctx->fGeometryEffectStack->back().fAttach ==
243 gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]);
244 ctx->fGeometryEffectStack->pop_back();
245 } break;
246 case ShapeType::kGroup: {
247 AttachShapeContext groupShapeCtx(&geos,
248 ctx->fGeometryEffectStack,
249 ctx->fCommittedAnimators);
250 if (auto subgroup = this->attachShape(rec->fJson["it"], &groupShapeCtx)) {
251 add_draw(std::move(subgroup), *rec);
252 SkASSERT(groupShapeCtx.fCommittedAnimators >= ctx->fCommittedAnimators);
253 ctx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators;
254 }
255 } break;
256 case ShapeType::kPaint: {
257 SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers));
258 auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this);
259 if (!paint || geos.empty())
260 break;
261
262 auto drawGeos = geos;
263
264 // Apply all pending effects from the stack.
265 for (auto it = ctx->fGeometryEffectStack->rbegin();
266 it != ctx->fGeometryEffectStack->rend(); ++it) {
267 drawGeos = it->fAttach(it->fJson, this, std::move(drawGeos));
268 }
269
270 // Apply local paint geometry adjustments (e.g. dashing).
271 SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintGeometryAdjusters));
272 if (const auto adjuster = gPaintGeometryAdjusters[rec->fInfo.fAttacherIndex]) {
273 drawGeos = adjuster(rec->fJson, this, std::move(drawGeos));
274 }
275
276 // If we still have multiple geos, reduce using 'merge'.
277 auto geo = drawGeos.size() > 1
278 ? ShapeBuilder::MergeGeometry(std::move(drawGeos), sksg::Merge::Mode::kMerge)
279 : drawGeos[0];
280
281 SkASSERT(geo);
282 add_draw(sksg::Draw::Make(std::move(geo), std::move(paint)), *rec);
283 ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
284 } break;
285 case ShapeType::kDrawEffect: {
286 SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gDrawEffectAttachers));
287 if (!draws.empty()) {
288 draws = gDrawEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
289 this,
290 std::move(draws));
291 ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
292 }
293 } break;
294 default:
295 break;
296 }
297 }
298
299 // By now we should have popped all local geometry effects.
300 SkASSERT(ctx->fGeometryEffectStack->size() == initialGeometryEffects);
301
302 sk_sp<sksg::RenderNode> shape_wrapper;
303 if (draws.size() == 1) {
304 // For a single draw, we don't need a group.
305 shape_wrapper = std::move(draws.front());
306 } else if (!draws.empty()) {
307 // Emit local draws reversed (bottom->top, per spec).
308 std::reverse(draws.begin(), draws.end());
309 draws.shrink_to_fit();
310
311 // We need a group to dispatch multiple draws.
312 shape_wrapper = sksg::Group::Make(std::move(draws));
313 }
314
315 sk_sp<sksg::Transform> shape_transform;
316 if (jtransform) {
317 const AutoPropertyTracker apt(this, *jtransform);
318
319 // This is tricky due to the interaction with ctx->fCommittedAnimators: we want any
320 // animators related to tranform/opacity to be committed => they must be inserted in front
321 // of the dangling/uncommitted ones.
322 AutoScope ascope(this);
323
324 if ((shape_transform = this->attachMatrix2D(*jtransform, nullptr))) {
325 shape_wrapper = sksg::TransformEffect::Make(std::move(shape_wrapper), shape_transform);
326 }
327 shape_wrapper = this->attachOpacity(*jtransform, std::move(shape_wrapper));
328
329 auto local_scope = ascope.release();
330 fCurrentAnimatorScope->insert(fCurrentAnimatorScope->begin() + ctx->fCommittedAnimators,
331 std::make_move_iterator(local_scope.begin()),
332 std::make_move_iterator(local_scope.end()));
333 ctx->fCommittedAnimators += local_scope.size();
334 }
335
336 // Push transformed local geometries to parent list, for subsequent paints.
337 for (auto& geo : geos) {
338 ctx->fGeometryStack->push_back(shape_transform
339 ? sksg::GeometryTransform::Make(std::move(geo), shape_transform)
340 : std::move(geo));
341 }
342
343 return shape_wrapper;
344}
345
346sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
347 LayerInfo*) const {
348 std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
349 std::vector<GeometryEffectRec> geometryEffectStack;
350 AttachShapeContext shapeCtx(&geometryStack, &geometryEffectStack,
351 fCurrentAnimatorScope->size());
352 auto shapeNode = this->attachShape(layer["shapes"], &shapeCtx);
353
354 // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches
355 // geometries => at the end, we can end up with unused geometries, which are nevertheless alive
356 // due to attached animators. To avoid this, we track committed animators and discard the
357 // orphans here.
358 SkASSERT(shapeCtx.fCommittedAnimators <= fCurrentAnimatorScope->size());
359 fCurrentAnimatorScope->resize(shapeCtx.fCommittedAnimators);
360
361 return shapeNode;
362}
363
364} // namespace internal
365} // namespace skottie
366