blob: df5e6e289dcca1ff57148bd4ed4d8fad1025a801 [file] [log] [blame]
Florin Malita094ccde2017-12-30 12:27:00 -05001/*
2 * Copyright 2017 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 "Skotty.h"
9
10#include "SkCanvas.h"
11#include "SkottyAnimator.h"
12#include "SkottyPriv.h"
13#include "SkottyProperties.h"
14#include "SkData.h"
Florin Malita49328072018-01-08 12:51:12 -050015#include "SkImage.h"
Florin Malita094ccde2017-12-30 12:27:00 -050016#include "SkMakeUnique.h"
Florin Malita49328072018-01-08 12:51:12 -050017#include "SkOSPath.h"
Florin Malita094ccde2017-12-30 12:27:00 -050018#include "SkPaint.h"
Florin Malita0e66fba2018-01-09 17:10:18 -050019#include "SkParse.h"
Florin Malita094ccde2017-12-30 12:27:00 -050020#include "SkPath.h"
21#include "SkPoint.h"
22#include "SkSGColor.h"
23#include "SkSGDraw.h"
Florin Malita094ccde2017-12-30 12:27:00 -050024#include "SkSGGroup.h"
Florin Malita49328072018-01-08 12:51:12 -050025#include "SkSGImage.h"
26#include "SkSGInvalidationController.h"
Florin Malitae6345d92018-01-03 23:37:54 -050027#include "SkSGMerge.h"
Florin Malitac0034172018-01-08 16:42:59 -050028#include "SkSGOpacityEffect.h"
Florin Malita094ccde2017-12-30 12:27:00 -050029#include "SkSGPath.h"
Florin Malita2e1d7e22018-01-02 10:40:00 -050030#include "SkSGRect.h"
Florin Malita094ccde2017-12-30 12:27:00 -050031#include "SkSGTransform.h"
Florin Malita51b8c892018-01-07 08:54:24 -050032#include "SkSGTrimEffect.h"
Florin Malita094ccde2017-12-30 12:27:00 -050033#include "SkStream.h"
34#include "SkTArray.h"
35#include "SkTHash.h"
36
37#include <cmath>
Florin Malita18eafd92018-01-04 21:11:55 -050038#include <unordered_map>
Florin Malitae6345d92018-01-03 23:37:54 -050039#include <vector>
40
Florin Malita094ccde2017-12-30 12:27:00 -050041#include "stdlib.h"
42
43namespace skotty {
44
45namespace {
46
47using AssetMap = SkTHashMap<SkString, const Json::Value*>;
48
49struct AttachContext {
Florin Malita49328072018-01-08 12:51:12 -050050 const ResourceProvider& fResources;
Florin Malita094ccde2017-12-30 12:27:00 -050051 const AssetMap& fAssets;
52 SkTArray<std::unique_ptr<AnimatorBase>>& fAnimators;
53};
54
55bool LogFail(const Json::Value& json, const char* msg) {
56 const auto dump = json.toStyledString();
57 LOG("!! %s: %s", msg, dump.c_str());
58 return false;
59}
60
61// This is the workhorse for binding properties: depending on whether the property is animated,
62// it will either apply immediately or instantiate and attach a keyframe animator.
Florin Malitaf9590922018-01-09 11:56:09 -050063template <typename ValT, typename NodeT>
64bool BindProperty(const Json::Value& jprop, AttachContext* ctx, const sk_sp<NodeT>& node,
65 typename Animator<ValT, NodeT>::ApplyFuncT&& apply) {
Florin Malita094ccde2017-12-30 12:27:00 -050066 if (!jprop.isObject())
67 return false;
68
Florin Malita95448a92018-01-08 10:15:12 -050069 const auto& jpropA = jprop["a"];
70 const auto& jpropK = jprop["k"];
71
72 // Older Json versions don't have an "a" animation marker.
73 // For those, we attempt to parse both ways.
74 if (jpropA.isNull() || !ParseBool(jpropA, "false")) {
Florin Malitaf9590922018-01-09 11:56:09 -050075 ValT val;
76 if (ValueTraits<ValT>::Parse(jpropK, &val)) {
Florin Malita95448a92018-01-08 10:15:12 -050077 // Static property.
Florin Malitaf9590922018-01-09 11:56:09 -050078 apply(node.get(), val);
Florin Malita95448a92018-01-08 10:15:12 -050079 return true;
Florin Malita094ccde2017-12-30 12:27:00 -050080 }
81
Florin Malita95448a92018-01-08 10:15:12 -050082 if (!jpropA.isNull()) {
83 return LogFail(jprop, "Could not parse (explicit) static property");
Florin Malita094ccde2017-12-30 12:27:00 -050084 }
Florin Malita094ccde2017-12-30 12:27:00 -050085 }
86
Florin Malita95448a92018-01-08 10:15:12 -050087 // Keyframe property.
Florin Malitaf9590922018-01-09 11:56:09 -050088 using AnimatorT = Animator<ValT, NodeT>;
89 auto animator = AnimatorT::Make(ParseFrames<ValT>(jpropK), node, std::move(apply));
Florin Malita95448a92018-01-08 10:15:12 -050090
91 if (!animator) {
92 return LogFail(jprop, "Could not parse keyframed property");
93 }
94
95 ctx->fAnimators.push_back(std::move(animator));
96
Florin Malita094ccde2017-12-30 12:27:00 -050097 return true;
98}
99
Florin Malita18eafd92018-01-04 21:11:55 -0500100sk_sp<sksg::Matrix> AttachMatrix(const Json::Value& t, AttachContext* ctx,
101 sk_sp<sksg::Matrix> parentMatrix) {
102 if (!t.isObject())
103 return nullptr;
Florin Malita094ccde2017-12-30 12:27:00 -0500104
Florin Malita18eafd92018-01-04 21:11:55 -0500105 auto matrix = sksg::Matrix::Make(SkMatrix::I(), std::move(parentMatrix));
106 auto composite = sk_make_sp<CompositeTransform>(matrix);
Florin Malitaf9590922018-01-09 11:56:09 -0500107 auto anchor_attached = BindProperty<VectorValue>(t["a"], ctx, composite,
108 [](CompositeTransform* node, const VectorValue& a) {
109 node->setAnchorPoint(ValueTraits<VectorValue>::As<SkPoint>(a));
Florin Malita094ccde2017-12-30 12:27:00 -0500110 });
Florin Malitaf9590922018-01-09 11:56:09 -0500111 auto position_attached = BindProperty<VectorValue>(t["p"], ctx, composite,
112 [](CompositeTransform* node, const VectorValue& p) {
113 node->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
Florin Malita094ccde2017-12-30 12:27:00 -0500114 });
Florin Malitaf9590922018-01-09 11:56:09 -0500115 auto scale_attached = BindProperty<VectorValue>(t["s"], ctx, composite,
116 [](CompositeTransform* node, const VectorValue& s) {
117 node->setScale(ValueTraits<VectorValue>::As<SkVector>(s));
Florin Malita094ccde2017-12-30 12:27:00 -0500118 });
Florin Malitaf9590922018-01-09 11:56:09 -0500119 auto rotation_attached = BindProperty<ScalarValue>(t["r"], ctx, composite,
120 [](CompositeTransform* node, const ScalarValue& r) {
Florin Malita094ccde2017-12-30 12:27:00 -0500121 node->setRotation(r);
122 });
Florin Malitaf9590922018-01-09 11:56:09 -0500123 auto skew_attached = BindProperty<ScalarValue>(t["sk"], ctx, composite,
124 [](CompositeTransform* node, const ScalarValue& sk) {
Florin Malita094ccde2017-12-30 12:27:00 -0500125 node->setSkew(sk);
126 });
Florin Malitaf9590922018-01-09 11:56:09 -0500127 auto skewaxis_attached = BindProperty<ScalarValue>(t["sa"], ctx, composite,
128 [](CompositeTransform* node, const ScalarValue& sa) {
Florin Malita094ccde2017-12-30 12:27:00 -0500129 node->setSkewAxis(sa);
130 });
131
132 if (!anchor_attached &&
133 !position_attached &&
134 !scale_attached &&
135 !rotation_attached &&
136 !skew_attached &&
137 !skewaxis_attached) {
138 LogFail(t, "Could not parse transform");
Florin Malita18eafd92018-01-04 21:11:55 -0500139 return nullptr;
Florin Malita094ccde2017-12-30 12:27:00 -0500140 }
141
Florin Malita18eafd92018-01-04 21:11:55 -0500142 return matrix;
Florin Malita094ccde2017-12-30 12:27:00 -0500143}
144
Florin Malitac0034172018-01-08 16:42:59 -0500145sk_sp<sksg::RenderNode> AttachOpacity(const Json::Value& jtransform, AttachContext* ctx,
146 sk_sp<sksg::RenderNode> childNode) {
147 if (!jtransform.isObject() || !childNode)
148 return childNode;
149
150 // This is more peeky than other attachers, because we want to avoid redundant opacity
151 // nodes for the extremely common case of static opaciy == 100.
152 const auto& opacity = jtransform["o"];
153 if (opacity.isObject() &&
154 !ParseBool(opacity["a"], true) &&
155 ParseScalar(opacity["k"], -1) == 100) {
156 // Ignoring static full opacity.
157 return childNode;
158 }
159
160 auto opacityNode = sksg::OpacityEffect::Make(childNode);
Florin Malitaf9590922018-01-09 11:56:09 -0500161 BindProperty<ScalarValue>(opacity, ctx, opacityNode,
162 [](sksg::OpacityEffect* node, const ScalarValue& o) {
Florin Malitac0034172018-01-08 16:42:59 -0500163 // BM opacity is [0..100]
164 node->setOpacity(o * 0.01f);
165 });
166
167 return opacityNode;
168}
169
Florin Malita094ccde2017-12-30 12:27:00 -0500170sk_sp<sksg::RenderNode> AttachShape(const Json::Value&, AttachContext* ctx);
171sk_sp<sksg::RenderNode> AttachComposition(const Json::Value&, AttachContext* ctx);
172
173sk_sp<sksg::RenderNode> AttachShapeGroup(const Json::Value& jgroup, AttachContext* ctx) {
174 SkASSERT(jgroup.isObject());
175
176 return AttachShape(jgroup["it"], ctx);
177}
178
179sk_sp<sksg::GeometryNode> AttachPathGeometry(const Json::Value& jpath, AttachContext* ctx) {
180 SkASSERT(jpath.isObject());
181
182 auto path_node = sksg::Path::Make();
Florin Malitaf9590922018-01-09 11:56:09 -0500183 auto path_attached = BindProperty<ShapeValue>(jpath["ks"], ctx, path_node,
184 [](sksg::Path* node, const ShapeValue& p) { node->setPath(p); });
Florin Malita094ccde2017-12-30 12:27:00 -0500185
186 if (path_attached)
187 LOG("** Attached path geometry - verbs: %d\n", path_node->getPath().countVerbs());
188
189 return path_attached ? path_node : nullptr;
190}
191
Florin Malita2e1d7e22018-01-02 10:40:00 -0500192sk_sp<sksg::GeometryNode> AttachRRectGeometry(const Json::Value& jrect, AttachContext* ctx) {
193 SkASSERT(jrect.isObject());
194
195 auto rect_node = sksg::RRect::Make();
196 auto composite = sk_make_sp<CompositeRRect>(rect_node);
197
Florin Malitaf9590922018-01-09 11:56:09 -0500198 auto p_attached = BindProperty<VectorValue>(jrect["p"], ctx, composite,
199 [](CompositeRRect* node, const VectorValue& p) {
200 node->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
201 });
202 auto s_attached = BindProperty<VectorValue>(jrect["s"], ctx, composite,
203 [](CompositeRRect* node, const VectorValue& s) {
204 node->setSize(ValueTraits<VectorValue>::As<SkSize>(s));
205 });
206 auto r_attached = BindProperty<ScalarValue>(jrect["r"], ctx, composite,
207 [](CompositeRRect* node, const ScalarValue& r) {
208 node->setRadius(SkSize::Make(r, r));
209 });
Florin Malita2e1d7e22018-01-02 10:40:00 -0500210
211 if (!p_attached && !s_attached && !r_attached) {
212 return nullptr;
213 }
214
Florin Malitafbc13f12018-01-04 10:26:35 -0500215 LOG("** Attached (r)rect geometry\n");
216
217 return rect_node;
218}
219
220sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const Json::Value& jellipse, AttachContext* ctx) {
221 SkASSERT(jellipse.isObject());
222
223 auto rect_node = sksg::RRect::Make();
224 auto composite = sk_make_sp<CompositeRRect>(rect_node);
225
Florin Malitaf9590922018-01-09 11:56:09 -0500226 auto p_attached = BindProperty<VectorValue>(jellipse["p"], ctx, composite,
227 [](CompositeRRect* node, const VectorValue& p) {
228 node->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
229 });
230 auto s_attached = BindProperty<VectorValue>(jellipse["s"], ctx, composite,
231 [](CompositeRRect* node, const VectorValue& s) {
232 const auto sz = ValueTraits<VectorValue>::As<SkSize>(s);
233 node->setSize(sz);
234 node->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2));
235 });
Florin Malitafbc13f12018-01-04 10:26:35 -0500236
237 if (!p_attached && !s_attached) {
238 return nullptr;
239 }
240
241 LOG("** Attached ellipse geometry\n");
242
Florin Malita2e1d7e22018-01-02 10:40:00 -0500243 return rect_node;
244}
245
Florin Malita02a32b02018-01-04 11:27:09 -0500246sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const Json::Value& jstar, AttachContext* ctx) {
247 SkASSERT(jstar.isObject());
248
249 static constexpr CompositePolyStar::Type gTypes[] = {
250 CompositePolyStar::Type::kStar, // "sy": 1
251 CompositePolyStar::Type::kPoly, // "sy": 2
252 };
253
254 const auto type = ParseInt(jstar["sy"], 0) - 1;
255 if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gTypes))) {
256 LogFail(jstar, "Unknown polystar type");
257 return nullptr;
258 }
259
260 auto path_node = sksg::Path::Make();
261 auto composite = sk_make_sp<CompositePolyStar>(path_node, gTypes[type]);
262
Florin Malitaf9590922018-01-09 11:56:09 -0500263 BindProperty<VectorValue>(jstar["p"], ctx, composite,
264 [](CompositePolyStar* node, const VectorValue& p) {
265 node->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
266 });
267 BindProperty<ScalarValue>(jstar["pt"], ctx, composite,
268 [](CompositePolyStar* node, const ScalarValue& pt) {
269 node->setPointCount(pt);
270 });
271 BindProperty<ScalarValue>(jstar["ir"], ctx, composite,
272 [](CompositePolyStar* node, const ScalarValue& ir) {
273 node->setInnerRadius(ir);
274 });
275 BindProperty<ScalarValue>(jstar["or"], ctx, composite,
276 [](CompositePolyStar* node, const ScalarValue& otr) {
Florin Malita9661b982018-01-06 14:25:49 -0500277 node->setOuterRadius(otr);
278 });
Florin Malitaf9590922018-01-09 11:56:09 -0500279 BindProperty<ScalarValue>(jstar["is"], ctx, composite,
280 [](CompositePolyStar* node, const ScalarValue& is) {
Florin Malita9661b982018-01-06 14:25:49 -0500281 node->setInnerRoundness(is);
282 });
Florin Malitaf9590922018-01-09 11:56:09 -0500283 BindProperty<ScalarValue>(jstar["os"], ctx, composite,
284 [](CompositePolyStar* node, const ScalarValue& os) {
Florin Malita9661b982018-01-06 14:25:49 -0500285 node->setOuterRoundness(os);
286 });
Florin Malitaf9590922018-01-09 11:56:09 -0500287 BindProperty<ScalarValue>(jstar["r"], ctx, composite,
288 [](CompositePolyStar* node, const ScalarValue& r) {
289 node->setRotation(r);
290 });
Florin Malita02a32b02018-01-04 11:27:09 -0500291
292 return path_node;
293}
294
Florin Malita094ccde2017-12-30 12:27:00 -0500295sk_sp<sksg::Color> AttachColorPaint(const Json::Value& obj, AttachContext* ctx) {
296 SkASSERT(obj.isObject());
297
298 auto color_node = sksg::Color::Make(SK_ColorBLACK);
299 color_node->setAntiAlias(true);
300
Florin Malitadcbb2db2018-01-09 13:11:56 -0500301 auto composite = sk_make_sp<CompositeColor>(color_node);
302 auto color_attached = BindProperty<VectorValue>(obj["c"], ctx, composite,
303 [](CompositeColor* node, const VectorValue& c) {
Florin Malitaf9590922018-01-09 11:56:09 -0500304 node->setColor(ValueTraits<VectorValue>::As<SkColor>(c));
305 });
Florin Malitadcbb2db2018-01-09 13:11:56 -0500306 auto opacity_attached = BindProperty<ScalarValue>(obj["o"], ctx, composite,
307 [](CompositeColor* node, const ScalarValue& o) {
308 node->setOpacity(o);
309 });
Florin Malita094ccde2017-12-30 12:27:00 -0500310
Florin Malitadcbb2db2018-01-09 13:11:56 -0500311 return (color_attached || opacity_attached) ? color_node : nullptr;
Florin Malita094ccde2017-12-30 12:27:00 -0500312}
313
314sk_sp<sksg::PaintNode> AttachFillPaint(const Json::Value& jfill, AttachContext* ctx) {
315 SkASSERT(jfill.isObject());
316
317 auto color = AttachColorPaint(jfill, ctx);
318 if (color) {
319 LOG("** Attached color fill: 0x%x\n", color->getColor());
320 }
321 return color;
322}
323
324sk_sp<sksg::PaintNode> AttachStrokePaint(const Json::Value& jstroke, AttachContext* ctx) {
325 SkASSERT(jstroke.isObject());
326
327 auto stroke_node = AttachColorPaint(jstroke, ctx);
328 if (!stroke_node)
329 return nullptr;
330
331 LOG("** Attached color stroke: 0x%x\n", stroke_node->getColor());
332
333 stroke_node->setStyle(SkPaint::kStroke_Style);
334
Florin Malitaf9590922018-01-09 11:56:09 -0500335 auto width_attached = BindProperty<ScalarValue>(jstroke["w"], ctx, stroke_node,
336 [](sksg::Color* node, const ScalarValue& w) {
337 node->setStrokeWidth(w);
338 });
Florin Malita094ccde2017-12-30 12:27:00 -0500339 if (!width_attached)
340 return nullptr;
341
342 stroke_node->setStrokeMiter(ParseScalar(jstroke["ml"], 4));
343
344 static constexpr SkPaint::Join gJoins[] = {
345 SkPaint::kMiter_Join,
346 SkPaint::kRound_Join,
347 SkPaint::kBevel_Join,
348 };
349 stroke_node->setStrokeJoin(gJoins[SkTPin<int>(ParseInt(jstroke["lj"], 1) - 1,
350 0, SK_ARRAY_COUNT(gJoins) - 1)]);
351
352 static constexpr SkPaint::Cap gCaps[] = {
353 SkPaint::kButt_Cap,
354 SkPaint::kRound_Cap,
355 SkPaint::kSquare_Cap,
356 };
357 stroke_node->setStrokeCap(gCaps[SkTPin<int>(ParseInt(jstroke["lc"], 1) - 1,
358 0, SK_ARRAY_COUNT(gCaps) - 1)]);
359
360 return stroke_node;
361}
362
Florin Malitae6345d92018-01-03 23:37:54 -0500363std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
364 const Json::Value& jmerge, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
365 std::vector<sk_sp<sksg::GeometryNode>> merged;
366
367 static constexpr sksg::Merge::Mode gModes[] = {
368 sksg::Merge::Mode::kMerge, // "mm": 1
369 sksg::Merge::Mode::kUnion, // "mm": 2
370 sksg::Merge::Mode::kDifference, // "mm": 3
371 sksg::Merge::Mode::kIntersect, // "mm": 4
372 sksg::Merge::Mode::kXOR , // "mm": 5
373 };
374
Florin Malita51b8c892018-01-07 08:54:24 -0500375 const auto mode = gModes[SkTPin<int>(ParseInt(jmerge["mm"], 1) - 1,
376 0, SK_ARRAY_COUNT(gModes) - 1)];
Florin Malitae6345d92018-01-03 23:37:54 -0500377 merged.push_back(sksg::Merge::Make(std::move(geos), mode));
378
379 LOG("** Attached merge path effect, mode: %d\n", mode);
380
381 return merged;
382}
383
Florin Malita51b8c892018-01-07 08:54:24 -0500384std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
385 const Json::Value& jtrim, AttachContext* ctx, std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
386
387 enum class Mode {
388 kMerged, // "m": 1
389 kSeparate, // "m": 2
390 } gModes[] = { Mode::kMerged, Mode::kSeparate };
391
392 const auto mode = gModes[SkTPin<int>(ParseInt(jtrim["m"], 1) - 1,
393 0, SK_ARRAY_COUNT(gModes) - 1)];
394
395 std::vector<sk_sp<sksg::GeometryNode>> inputs;
396 if (mode == Mode::kMerged) {
397 inputs.push_back(sksg::Merge::Make(std::move(geos), sksg::Merge::Mode::kMerge));
398 } else {
399 inputs = std::move(geos);
400 }
401
402 std::vector<sk_sp<sksg::GeometryNode>> trimmed;
403 trimmed.reserve(inputs.size());
404 for (const auto& i : inputs) {
405 const auto trim = sksg::TrimEffect::Make(i);
406 trimmed.push_back(trim);
Florin Malitaf9590922018-01-09 11:56:09 -0500407 BindProperty<ScalarValue>(jtrim["s"], ctx, trim,
408 [](sksg::TrimEffect* node, const ScalarValue& s) {
Florin Malita51b8c892018-01-07 08:54:24 -0500409 node->setStart(s * 0.01f);
410 });
Florin Malitaf9590922018-01-09 11:56:09 -0500411 BindProperty<ScalarValue>(jtrim["e"], ctx, trim,
412 [](sksg::TrimEffect* node, const ScalarValue& e) {
Florin Malita51b8c892018-01-07 08:54:24 -0500413 node->setEnd(e * 0.01f);
414 });
415 // TODO: "offset" doesn't currently work the same as BM - figure out what's going on.
Florin Malitaf9590922018-01-09 11:56:09 -0500416 BindProperty<ScalarValue>(jtrim["o"], ctx, trim,
417 [](sksg::TrimEffect* node, const ScalarValue& o) {
Florin Malita51b8c892018-01-07 08:54:24 -0500418 node->setOffset(o * 0.01f);
419 });
420 }
421
422 return trimmed;
423}
424
Florin Malita094ccde2017-12-30 12:27:00 -0500425using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const Json::Value&, AttachContext*);
426static constexpr GeometryAttacherT gGeometryAttachers[] = {
427 AttachPathGeometry,
Florin Malita2e1d7e22018-01-02 10:40:00 -0500428 AttachRRectGeometry,
Florin Malitafbc13f12018-01-04 10:26:35 -0500429 AttachEllipseGeometry,
Florin Malita02a32b02018-01-04 11:27:09 -0500430 AttachPolystarGeometry,
Florin Malita094ccde2017-12-30 12:27:00 -0500431};
432
433using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const Json::Value&, AttachContext*);
434static constexpr PaintAttacherT gPaintAttachers[] = {
435 AttachFillPaint,
436 AttachStrokePaint,
437};
438
439using GroupAttacherT = sk_sp<sksg::RenderNode> (*)(const Json::Value&, AttachContext*);
440static constexpr GroupAttacherT gGroupAttachers[] = {
441 AttachShapeGroup,
442};
443
Florin Malitae6345d92018-01-03 23:37:54 -0500444using GeometryEffectAttacherT =
445 std::vector<sk_sp<sksg::GeometryNode>> (*)(const Json::Value&,
446 AttachContext*,
447 std::vector<sk_sp<sksg::GeometryNode>>&&);
448static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
449 AttachMergeGeometryEffect,
Florin Malita51b8c892018-01-07 08:54:24 -0500450 AttachTrimGeometryEffect,
Florin Malitae6345d92018-01-03 23:37:54 -0500451};
452
Florin Malita094ccde2017-12-30 12:27:00 -0500453enum class ShapeType {
454 kGeometry,
Florin Malitae6345d92018-01-03 23:37:54 -0500455 kGeometryEffect,
Florin Malita094ccde2017-12-30 12:27:00 -0500456 kPaint,
457 kGroup,
Florin Malitadacc02b2017-12-31 09:12:31 -0500458 kTransform,
Florin Malita094ccde2017-12-30 12:27:00 -0500459};
460
461struct ShapeInfo {
462 const char* fTypeString;
463 ShapeType fShapeType;
464 uint32_t fAttacherIndex; // index into respective attacher tables
465};
466
467const ShapeInfo* FindShapeInfo(const Json::Value& shape) {
468 static constexpr ShapeInfo gShapeInfo[] = {
Florin Malitafbc13f12018-01-04 10:26:35 -0500469 { "el", ShapeType::kGeometry , 2 }, // ellipse -> AttachEllipseGeometry
Florin Malitae6345d92018-01-03 23:37:54 -0500470 { "fl", ShapeType::kPaint , 0 }, // fill -> AttachFillPaint
471 { "gr", ShapeType::kGroup , 0 }, // group -> AttachShapeGroup
472 { "mm", ShapeType::kGeometryEffect, 0 }, // merge -> AttachMergeGeometryEffect
Florin Malita02a32b02018-01-04 11:27:09 -0500473 { "rc", ShapeType::kGeometry , 1 }, // rrect -> AttachRRectGeometry
Florin Malitae6345d92018-01-03 23:37:54 -0500474 { "sh", ShapeType::kGeometry , 0 }, // shape -> AttachPathGeometry
Florin Malita02a32b02018-01-04 11:27:09 -0500475 { "sr", ShapeType::kGeometry , 3 }, // polystar -> AttachPolyStarGeometry
Florin Malitae6345d92018-01-03 23:37:54 -0500476 { "st", ShapeType::kPaint , 1 }, // stroke -> AttachStrokePaint
Florin Malita51b8c892018-01-07 08:54:24 -0500477 { "tm", ShapeType::kGeometryEffect, 1 }, // trim -> AttachTrimGeometryEffect
Florin Malita18eafd92018-01-04 21:11:55 -0500478 { "tr", ShapeType::kTransform , 0 }, // transform -> In-place handler
Florin Malita094ccde2017-12-30 12:27:00 -0500479 };
480
481 if (!shape.isObject())
482 return nullptr;
483
484 const auto& type = shape["ty"];
485 if (!type.isString())
486 return nullptr;
487
488 const auto* info = bsearch(type.asCString(),
489 gShapeInfo,
490 SK_ARRAY_COUNT(gShapeInfo),
491 sizeof(ShapeInfo),
492 [](const void* key, const void* info) {
493 return strcmp(static_cast<const char*>(key),
494 static_cast<const ShapeInfo*>(info)->fTypeString);
495 });
496
497 return static_cast<const ShapeInfo*>(info);
498}
499
500sk_sp<sksg::RenderNode> AttachShape(const Json::Value& shapeArray, AttachContext* ctx) {
501 if (!shapeArray.isArray())
502 return nullptr;
503
Florin Malita2a8275b2018-01-02 12:52:43 -0500504 // (https://helpx.adobe.com/after-effects/using/overview-shape-layers-paths-vector.html#groups_and_render_order_for_shapes_and_shape_attributes)
505 //
506 // Render order for shapes within a shape layer
507 //
508 // The rules for rendering a shape layer are similar to the rules for rendering a composition
509 // that contains nested compositions:
510 //
511 // * Within a group, the shape at the bottom of the Timeline panel stacking order is rendered
512 // first.
513 //
514 // * All path operations within a group are performed before paint operations. This means,
515 // for example, that the stroke follows the distortions in the path made by the Wiggle Paths
516 // path operation. Path operations within a group are performed from top to bottom.
517 //
518 // * Paint operations within a group are performed from the bottom to the top in the Timeline
519 // panel stacking order. This means, for example, that a stroke is rendered on top of
520 // (in front of) a stroke that appears after it in the Timeline panel.
521 //
Florin Malitadacc02b2017-12-31 09:12:31 -0500522 sk_sp<sksg::Group> shape_group = sksg::Group::Make();
523 sk_sp<sksg::RenderNode> xformed_group = shape_group;
Florin Malita094ccde2017-12-30 12:27:00 -0500524
Florin Malitae6345d92018-01-03 23:37:54 -0500525 std::vector<sk_sp<sksg::GeometryNode>> geos;
526 std::vector<sk_sp<sksg::RenderNode>> draws;
Florin Malita094ccde2017-12-30 12:27:00 -0500527
528 for (const auto& s : shapeArray) {
529 const auto* info = FindShapeInfo(s);
530 if (!info) {
531 LogFail(s.isObject() ? s["ty"] : s, "Unknown shape");
532 continue;
533 }
534
535 switch (info->fShapeType) {
536 case ShapeType::kGeometry: {
537 SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
538 if (auto geo = gGeometryAttachers[info->fAttacherIndex](s, ctx)) {
539 geos.push_back(std::move(geo));
540 }
541 } break;
Florin Malitae6345d92018-01-03 23:37:54 -0500542 case ShapeType::kGeometryEffect: {
543 SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
544 geos = gGeometryEffectAttachers[info->fAttacherIndex](s, ctx, std::move(geos));
545 } break;
Florin Malita094ccde2017-12-30 12:27:00 -0500546 case ShapeType::kPaint: {
547 SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers));
548 if (auto paint = gPaintAttachers[info->fAttacherIndex](s, ctx)) {
Florin Malita2a8275b2018-01-02 12:52:43 -0500549 for (const auto& geo : geos) {
550 draws.push_back(sksg::Draw::Make(geo, paint));
551 }
Florin Malita094ccde2017-12-30 12:27:00 -0500552 }
553 } break;
554 case ShapeType::kGroup: {
555 SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGroupAttachers));
556 if (auto group = gGroupAttachers[info->fAttacherIndex](s, ctx)) {
Florin Malita2a8275b2018-01-02 12:52:43 -0500557 draws.push_back(std::move(group));
Florin Malita094ccde2017-12-30 12:27:00 -0500558 }
559 } break;
Florin Malitadacc02b2017-12-31 09:12:31 -0500560 case ShapeType::kTransform: {
Florin Malita2a8275b2018-01-02 12:52:43 -0500561 // TODO: BM appears to transform the geometry, not the draw op itself.
Florin Malita18eafd92018-01-04 21:11:55 -0500562 if (auto matrix = AttachMatrix(s, ctx, nullptr)) {
563 xformed_group = sksg::Transform::Make(std::move(xformed_group),
564 std::move(matrix));
565 }
Florin Malitac0034172018-01-08 16:42:59 -0500566 xformed_group = AttachOpacity(s, ctx, std::move(xformed_group));
Florin Malitadacc02b2017-12-31 09:12:31 -0500567 } break;
Florin Malita094ccde2017-12-30 12:27:00 -0500568 }
569 }
570
Florin Malita2a8275b2018-01-02 12:52:43 -0500571 if (draws.empty()) {
572 return nullptr;
Florin Malita094ccde2017-12-30 12:27:00 -0500573 }
574
Florin Malitae6345d92018-01-03 23:37:54 -0500575 for (auto draw = draws.rbegin(); draw != draws.rend(); ++draw) {
576 shape_group->addChild(std::move(*draw));
Florin Malita2a8275b2018-01-02 12:52:43 -0500577 }
578
Florin Malitae6345d92018-01-03 23:37:54 -0500579 LOG("** Attached shape: %zd draws.\n", draws.size());
Florin Malitadacc02b2017-12-31 09:12:31 -0500580 return xformed_group;
Florin Malita094ccde2017-12-30 12:27:00 -0500581}
582
583sk_sp<sksg::RenderNode> AttachCompLayer(const Json::Value& layer, AttachContext* ctx) {
584 SkASSERT(layer.isObject());
585
586 auto refId = ParseString(layer["refId"], "");
587 if (refId.isEmpty()) {
588 LOG("!! Comp layer missing refId\n");
589 return nullptr;
590 }
591
592 const auto* comp = ctx->fAssets.find(refId);
593 if (!comp) {
594 LOG("!! Pre-comp not found: '%s'\n", refId.c_str());
595 return nullptr;
596 }
597
598 // TODO: cycle detection
599 return AttachComposition(**comp, ctx);
600}
601
Florin Malita0e66fba2018-01-09 17:10:18 -0500602sk_sp<sksg::RenderNode> AttachSolidLayer(const Json::Value& jlayer, AttachContext*) {
603 SkASSERT(jlayer.isObject());
Florin Malita094ccde2017-12-30 12:27:00 -0500604
Florin Malita0e66fba2018-01-09 17:10:18 -0500605 const auto size = SkSize::Make(ParseScalar(jlayer["sw"], -1),
606 ParseScalar(jlayer["sh"], -1));
607 const auto hex = ParseString(jlayer["sc"], "");
608 uint32_t c;
609 if (size.isEmpty() ||
610 !hex.startsWith("#") ||
611 !SkParse::FindHex(hex.c_str() + 1, &c)) {
612 LogFail(jlayer, "Could not parse solid layer");
613 return nullptr;
614 }
615
616 const SkColor color = 0xff000000 | c;
617
618 return sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeSize(size)),
619 sksg::Color::Make(color));
Florin Malita094ccde2017-12-30 12:27:00 -0500620}
621
Florin Malita49328072018-01-08 12:51:12 -0500622sk_sp<sksg::RenderNode> AttachImageAsset(const Json::Value& jimage, AttachContext* ctx) {
623 SkASSERT(jimage.isObject());
624
625 const auto name = ParseString(jimage["p"], ""),
626 path = ParseString(jimage["u"], "");
627 if (name.isEmpty())
628 return nullptr;
629
630 // TODO: plumb resource paths explicitly to ResourceProvider?
631 const auto resName = path.isEmpty() ? name : SkOSPath::Join(path.c_str(), name.c_str());
632 const auto resStream = ctx->fResources.openStream(resName.c_str());
633 if (!resStream || !resStream->hasLength()) {
634 LOG("!! Could not load image resource: %s\n", resName.c_str());
635 return nullptr;
636 }
637
638 // TODO: non-intrisic image sizing
639 return sksg::Image::Make(
640 SkImage::MakeFromEncoded(SkData::MakeFromStream(resStream.get(), resStream->getLength())));
641}
642
643sk_sp<sksg::RenderNode> AttachImageLayer(const Json::Value& layer, AttachContext* ctx) {
Florin Malita094ccde2017-12-30 12:27:00 -0500644 SkASSERT(layer.isObject());
645
Florin Malita49328072018-01-08 12:51:12 -0500646 auto refId = ParseString(layer["refId"], "");
647 if (refId.isEmpty()) {
648 LOG("!! Image layer missing refId\n");
649 return nullptr;
650 }
651
652 const auto* jimage = ctx->fAssets.find(refId);
653 if (!jimage) {
654 LOG("!! Image asset not found: '%s'\n", refId.c_str());
655 return nullptr;
656 }
657
658 return AttachImageAsset(**jimage, ctx);
Florin Malita094ccde2017-12-30 12:27:00 -0500659}
660
661sk_sp<sksg::RenderNode> AttachNullLayer(const Json::Value& layer, AttachContext*) {
662 SkASSERT(layer.isObject());
663
Florin Malita18eafd92018-01-04 21:11:55 -0500664 // Null layers are used solely to drive dependent transforms,
665 // but we use free-floating sksg::Matrices for that purpose.
Florin Malita094ccde2017-12-30 12:27:00 -0500666 return nullptr;
667}
668
669sk_sp<sksg::RenderNode> AttachShapeLayer(const Json::Value& layer, AttachContext* ctx) {
670 SkASSERT(layer.isObject());
671
672 LOG("** Attaching shape layer ind: %d\n", ParseInt(layer["ind"], 0));
673
674 return AttachShape(layer["shapes"], ctx);
675}
676
677sk_sp<sksg::RenderNode> AttachTextLayer(const Json::Value& layer, AttachContext*) {
678 SkASSERT(layer.isObject());
679
680 LOG("?? Text layer stub\n");
681 return nullptr;
682}
683
Florin Malita18eafd92018-01-04 21:11:55 -0500684struct AttachLayerContext {
685 AttachLayerContext(const Json::Value& jlayers, AttachContext* ctx)
686 : fLayerList(jlayers), fCtx(ctx) {}
687
688 const Json::Value& fLayerList;
689 AttachContext* fCtx;
690 std::unordered_map<const Json::Value*, sk_sp<sksg::Matrix>> fLayerMatrixCache;
691 std::unordered_map<int, const Json::Value*> fLayerIndexCache;
692
693 const Json::Value* findLayer(int index) {
694 SkASSERT(fLayerList.isArray());
695
696 if (index < 0) {
697 return nullptr;
698 }
699
700 const auto cached = fLayerIndexCache.find(index);
701 if (cached != fLayerIndexCache.end()) {
702 return cached->second;
703 }
704
705 for (const auto& l : fLayerList) {
706 if (!l.isObject()) {
707 continue;
708 }
709
710 if (ParseInt(l["ind"], -1) == index) {
711 fLayerIndexCache.insert(std::make_pair(index, &l));
712 return &l;
713 }
714 }
715
716 return nullptr;
717 }
718
719 sk_sp<sksg::Matrix> AttachLayerMatrix(const Json::Value& jlayer) {
720 SkASSERT(jlayer.isObject());
721
722 const auto cached = fLayerMatrixCache.find(&jlayer);
723 if (cached != fLayerMatrixCache.end()) {
724 return cached->second;
725 }
726
727 const auto* parentLayer = this->findLayer(ParseInt(jlayer["parent"], -1));
728
729 // TODO: cycle detection?
730 auto parentMatrix = (parentLayer && parentLayer != &jlayer)
731 ? this->AttachLayerMatrix(*parentLayer) : nullptr;
732
733 auto layerMatrix = AttachMatrix(jlayer["ks"], fCtx, std::move(parentMatrix));
734 fLayerMatrixCache.insert(std::make_pair(&jlayer, layerMatrix));
735
736 return layerMatrix;
737 }
738};
739
740sk_sp<sksg::RenderNode> AttachLayer(const Json::Value& jlayer,
741 AttachLayerContext* layerCtx) {
742 if (!jlayer.isObject())
Florin Malita094ccde2017-12-30 12:27:00 -0500743 return nullptr;
744
745 using LayerAttacher = sk_sp<sksg::RenderNode> (*)(const Json::Value&, AttachContext*);
746 static constexpr LayerAttacher gLayerAttachers[] = {
747 AttachCompLayer, // 'ty': 0
748 AttachSolidLayer, // 'ty': 1
749 AttachImageLayer, // 'ty': 2
750 AttachNullLayer, // 'ty': 3
751 AttachShapeLayer, // 'ty': 4
752 AttachTextLayer, // 'ty': 5
753 };
754
Florin Malita18eafd92018-01-04 21:11:55 -0500755 int type = ParseInt(jlayer["ty"], -1);
Florin Malita094ccde2017-12-30 12:27:00 -0500756 if (type < 0 || type >= SkTo<int>(SK_ARRAY_COUNT(gLayerAttachers))) {
757 return nullptr;
758 }
759
Florin Malita71cba8f2018-01-09 08:07:14 -0500760 // Layer content.
761 auto layer = gLayerAttachers[type](jlayer, layerCtx->fCtx);
762 if (auto layerMatrix = layerCtx->AttachLayerMatrix(jlayer)) {
763 // Optional layer transform.
764 layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix));
765 }
766 // Optional layer opacity.
767 layer = AttachOpacity(jlayer["ks"], layerCtx->fCtx, std::move(layer));
Florin Malita18eafd92018-01-04 21:11:55 -0500768
Florin Malita71cba8f2018-01-09 08:07:14 -0500769 // TODO: we should also disable related/inactive animators.
770 class Activator final : public AnimatorBase {
771 public:
772 Activator(sk_sp<sksg::OpacityEffect> controlNode, float in, float out)
773 : fControlNode(std::move(controlNode))
774 , fIn(in)
775 , fOut(out) {}
776
Florin Malitaa6dd7522018-01-09 08:46:52 -0500777 void tick(float t) override {
Florin Malita71cba8f2018-01-09 08:07:14 -0500778 // Keep the layer fully transparent except for its [in..out] lifespan.
779 // (note: opacity == 0 disables rendering, while opacity == 1 is a noop)
780 fControlNode->setOpacity(t >= fIn && t <= fOut ? 1 : 0);
781 }
782
783 private:
784 const sk_sp<sksg::OpacityEffect> fControlNode;
785 const float fIn,
786 fOut;
787 };
788
789 auto layerControl = sksg::OpacityEffect::Make(std::move(layer));
790 const auto in = ParseScalar(jlayer["ip"], 0),
791 out = ParseScalar(jlayer["op"], in);
792
793 if (in >= out || ! layerControl)
794 return nullptr;
795
796 layerCtx->fCtx->fAnimators.push_back(skstd::make_unique<Activator>(layerControl, in, out));
797
798 return layerControl;
Florin Malita094ccde2017-12-30 12:27:00 -0500799}
800
801sk_sp<sksg::RenderNode> AttachComposition(const Json::Value& comp, AttachContext* ctx) {
802 if (!comp.isObject())
803 return nullptr;
804
Florin Malita18eafd92018-01-04 21:11:55 -0500805 const auto& jlayers = comp["layers"];
806 if (!jlayers.isArray())
807 return nullptr;
Florin Malita094ccde2017-12-30 12:27:00 -0500808
Florin Malita18eafd92018-01-04 21:11:55 -0500809 SkSTArray<16, sk_sp<sksg::RenderNode>, true> layers;
810 AttachLayerContext layerCtx(jlayers, ctx);
811
812 for (const auto& l : jlayers) {
813 if (auto layer_fragment = AttachLayer(l, &layerCtx)) {
Florin Malita2a8275b2018-01-02 12:52:43 -0500814 layers.push_back(std::move(layer_fragment));
Florin Malita094ccde2017-12-30 12:27:00 -0500815 }
816 }
817
Florin Malita2a8275b2018-01-02 12:52:43 -0500818 if (layers.empty()) {
819 return nullptr;
820 }
821
822 // Layers are painted in bottom->top order.
823 auto comp_group = sksg::Group::Make();
824 for (int i = layers.count() - 1; i >= 0; --i) {
825 comp_group->addChild(std::move(layers[i]));
826 }
827
828 LOG("** Attached composition '%s': %d layers.\n",
829 ParseString(comp["id"], "").c_str(), layers.count());
830
Florin Malita094ccde2017-12-30 12:27:00 -0500831 return comp_group;
832}
833
834} // namespace
835
Florin Malita49328072018-01-08 12:51:12 -0500836std::unique_ptr<Animation> Animation::Make(SkStream* stream, const ResourceProvider& res) {
Florin Malita094ccde2017-12-30 12:27:00 -0500837 if (!stream->hasLength()) {
838 // TODO: handle explicit buffering?
839 LOG("!! cannot parse streaming content\n");
840 return nullptr;
841 }
842
843 Json::Value json;
844 {
845 auto data = SkData::MakeFromStream(stream, stream->getLength());
846 if (!data) {
847 LOG("!! could not read stream\n");
848 return nullptr;
849 }
850
851 Json::Reader reader;
852
853 auto dataStart = static_cast<const char*>(data->data());
854 if (!reader.parse(dataStart, dataStart + data->size(), json, false) || !json.isObject()) {
855 LOG("!! failed to parse json: %s\n", reader.getFormattedErrorMessages().c_str());
856 return nullptr;
857 }
858 }
859
860 const auto version = ParseString(json["v"], "");
861 const auto size = SkSize::Make(ParseScalar(json["w"], -1), ParseScalar(json["h"], -1));
862 const auto fps = ParseScalar(json["fr"], -1);
863
864 if (size.isEmpty() || version.isEmpty() || fps < 0) {
865 LOG("!! invalid animation params (version: %s, size: [%f %f], frame rate: %f)",
866 version.c_str(), size.width(), size.height(), fps);
867 return nullptr;
868 }
869
Florin Malita49328072018-01-08 12:51:12 -0500870 return std::unique_ptr<Animation>(new Animation(res, std::move(version), size, fps, json));
Florin Malita094ccde2017-12-30 12:27:00 -0500871}
872
Florin Malita49328072018-01-08 12:51:12 -0500873std::unique_ptr<Animation> Animation::MakeFromFile(const char path[], const ResourceProvider* res) {
874 class DirectoryResourceProvider final : public ResourceProvider {
875 public:
876 explicit DirectoryResourceProvider(SkString dir) : fDir(std::move(dir)) {}
877
878 std::unique_ptr<SkStream> openStream(const char resource[]) const override {
879 const auto resPath = SkOSPath::Join(fDir.c_str(), resource);
880 return SkStream::MakeFromFile(resPath.c_str());
881 }
882
883 private:
884 const SkString fDir;
885 };
886
887 const auto jsonStream = SkStream::MakeFromFile(path);
888 if (!jsonStream)
889 return nullptr;
890
891 std::unique_ptr<ResourceProvider> defaultProvider;
892 if (!res) {
893 defaultProvider = skstd::make_unique<DirectoryResourceProvider>(SkOSPath::Dirname(path));
894 }
895
896 return Make(jsonStream.get(), res ? *res : *defaultProvider);
897}
898
899Animation::Animation(const ResourceProvider& resources,
900 SkString version, const SkSize& size, SkScalar fps, const Json::Value& json)
Florin Malita094ccde2017-12-30 12:27:00 -0500901 : fVersion(std::move(version))
902 , fSize(size)
903 , fFrameRate(fps)
904 , fInPoint(ParseScalar(json["ip"], 0))
905 , fOutPoint(SkTMax(ParseScalar(json["op"], SK_ScalarMax), fInPoint)) {
906
907 AssetMap assets;
908 for (const auto& asset : json["assets"]) {
909 if (!asset.isObject()) {
910 continue;
911 }
912
913 assets.set(ParseString(asset["id"], ""), &asset);
914 }
915
Florin Malita49328072018-01-08 12:51:12 -0500916 AttachContext ctx = { resources, assets, fAnimators };
Florin Malita094ccde2017-12-30 12:27:00 -0500917 fDom = AttachComposition(json, &ctx);
918
Florin Malitadb385732018-01-09 12:19:32 -0500919 // In case the client calls render before the first tick.
920 this->animationTick(0);
921
Florin Malita094ccde2017-12-30 12:27:00 -0500922 LOG("** Attached %d animators\n", fAnimators.count());
923}
924
925Animation::~Animation() = default;
926
Mike Reed29859872018-01-08 08:25:27 -0500927void Animation::render(SkCanvas* canvas, const SkRect* dstR) const {
Florin Malita094ccde2017-12-30 12:27:00 -0500928 if (!fDom)
929 return;
930
931 sksg::InvalidationController ic;
932 fDom->revalidate(&ic, SkMatrix::I());
933
934 // TODO: proper inval
Mike Reed29859872018-01-08 08:25:27 -0500935 SkAutoCanvasRestore restore(canvas, true);
936 const SkRect srcR = SkRect::MakeSize(this->size());
937 if (dstR) {
938 canvas->concat(SkMatrix::MakeRectToRect(srcR, *dstR, SkMatrix::kCenter_ScaleToFit));
939 }
940 canvas->clipRect(srcR);
Florin Malita094ccde2017-12-30 12:27:00 -0500941 fDom->render(canvas);
942
943 if (!fShowInval)
944 return;
945
946 SkPaint fill, stroke;
947 fill.setAntiAlias(true);
948 fill.setColor(0x40ff0000);
949 stroke.setAntiAlias(true);
950 stroke.setColor(0xffff0000);
951 stroke.setStyle(SkPaint::kStroke_Style);
952
953 for (const auto& r : ic) {
954 canvas->drawRect(r, fill);
955 canvas->drawRect(r, stroke);
956 }
957}
958
959void Animation::animationTick(SkMSec ms) {
960 // 't' in the BM model really means 'frame #'
961 auto t = static_cast<float>(ms) * fFrameRate / 1000;
962
963 t = fInPoint + std::fmod(t, fOutPoint - fInPoint);
964
965 // TODO: this can be optimized quite a bit with some sorting/state tracking.
966 for (const auto& a : fAnimators) {
967 a->tick(t);
968 }
969}
970
971} // namespace skotty