[skottie] Multi-frame image support
Extend the image asset provider API to support animated/multi-frame images.
Add a GM based on SkAnimCodecPlayer + animated public domain GIF
(source: https://giphy.com/explore/public-domain).
Bug: skia:
Change-Id: Iaa596e01a7626ca6574db1ebc90632f5a9a02bdc
Reviewed-on: https://skia-review.googlesource.com/159162
Commit-Queue: Florin Malita <fmalita@chromium.org>
Reviewed-by: Mike Reed <reed@google.com>
diff --git a/modules/skottie/gm/SkottieGM.cpp b/modules/skottie/gm/SkottieGM.cpp
index 229df62..6e88837 100644
--- a/modules/skottie/gm/SkottieGM.cpp
+++ b/modules/skottie/gm/SkottieGM.cpp
@@ -7,8 +7,10 @@
#include "gm.h"
#include "Resources.h"
+#include "SkAnimCodecPlayer.h"
#include "SkAnimTimer.h"
#include "SkColor.h"
+#include "SkMakeUnique.h"
#include "Skottie.h"
#include "SkottieProperty.h"
@@ -88,7 +90,6 @@
DEF_GM(return new SkottieWebFontGM;)
class SkottieColorizeGM : public skiagm::GM {
-public:
protected:
SkString onShortName() override {
return SkString("skottie_colorize");
@@ -126,7 +127,6 @@
return true;
}
-protected:
bool onHandleKey(SkUnichar uni) override {
static constexpr SkColor kColors[] = {
SK_ColorBLACK,
@@ -173,3 +173,81 @@
};
DEF_GM(return new SkottieColorizeGM;)
+
+class SkottieMultiFrameGM : public skiagm::GM {
+public:
+protected:
+ SkString onShortName() override {
+ return SkString("skottie_multiframe");
+ }
+
+ SkISize onISize() override {
+ return SkISize::Make(kSize, kSize);
+ }
+
+ void onOnceBeforeDraw() override {
+ if (auto stream = GetResourceAsStream("skottie/skottie_sample_multiframe.json")) {
+ fAnimation = Animation::Builder()
+ .setResourceProvider(sk_make_sp<MultiFrameResourceProvider>())
+ .make(stream.get());
+ }
+ }
+
+ void onDraw(SkCanvas* canvas) override {
+ if (!fAnimation) {
+ return;
+ }
+
+ auto dest = SkRect::MakeWH(kSize, kSize);
+ fAnimation->render(canvas, &dest);
+ }
+
+ bool onAnimate(const SkAnimTimer& timer) override {
+ if (!fAnimation) {
+ return false;
+ }
+
+ const auto duration = fAnimation->duration();
+ fAnimation->seek(std::fmod(timer.secs(), duration) / duration);
+ return true;
+ }
+
+private:
+ class MultiFrameImageAsset final : public skottie::ImageAsset {
+ public:
+ MultiFrameImageAsset() {
+ if (auto codec = SkCodec::MakeFromData(GetResourceAsData("images/flightAnim.gif"))) {
+ fPlayer = skstd::make_unique<SkAnimCodecPlayer>(std::move(codec));
+ }
+ }
+
+ bool isMultiFrame() override { return fPlayer ? fPlayer->duration() > 0 : false; }
+
+ sk_sp<SkImage> getFrame(float t) override {
+ if (!fPlayer) {
+ return nullptr;
+ }
+
+ fPlayer->seek(static_cast<uint32_t>(t * 1000));
+ return fPlayer->getFrame();
+ }
+
+ private:
+ std::unique_ptr<SkAnimCodecPlayer> fPlayer;
+ };
+
+ class MultiFrameResourceProvider final : public skottie::ResourceProvider {
+ public:
+ sk_sp<ImageAsset> loadImageAsset(const char[], const char[]) const override {
+ return sk_make_sp<MultiFrameImageAsset>();
+ }
+ };
+
+ static constexpr SkScalar kSize = 800;
+
+ sk_sp<Animation> fAnimation;
+
+ using INHERITED = skiagm::GM;
+};
+
+DEF_GM(return new SkottieMultiFrameGM;)
diff --git a/modules/skottie/include/Skottie.h b/modules/skottie/include/Skottie.h
index 9c6599d..f6a87d2 100644
--- a/modules/skottie/include/Skottie.h
+++ b/modules/skottie/include/Skottie.h
@@ -18,6 +18,7 @@
class SkCanvas;
class SkData;
+class SkImage;
struct SkRect;
class SkStream;
@@ -30,24 +31,50 @@
class PropertyObserver;
/**
+ * Image asset proxy interface.
+ */
+class SK_API ImageAsset : public SkRefCnt {
+public:
+ /**
+ * Returns true if the image asset is animated.
+ */
+ virtual bool isMultiFrame() = 0;
+
+ /**
+ * Returns the SkImage for a given frame.
+ *
+ * If the image asset is static, getImage() is only called once, at animation load time.
+ * Otherwise, this gets invoked every time the animation time is adjusted (on every seek).
+ *
+ * Embedders should cache and serve the same SkImage whenever possible, for efficiency.
+ *
+ * @param t Frame time code, in seconds, relative to the image layer timeline origin
+ * (in-point).
+ */
+ virtual sk_sp<SkImage> getFrame(float t) = 0;
+};
+
+/**
* ResourceProvider allows Skottie embedders to control loading of external
* Skottie resources -- e.g. images, fonts, nested animations.
*/
class SK_API ResourceProvider : public SkRefCnt {
public:
- ResourceProvider() = default;
- virtual ~ResourceProvider() = default;
- ResourceProvider(const ResourceProvider&) = delete;
- ResourceProvider& operator=(const ResourceProvider&) = delete;
-
/**
- * Load a resource (image, nested animation) specified by |path| + |name|, and
- * return as an SkData.
+ * Load a generic resource (currently only nested animations) specified by |path| + |name|,
+ * and return as an SkData.
*/
virtual sk_sp<SkData> load(const char resource_path[],
const char resource_name[]) const;
/**
+ * Load an image asset specified by |path| + |name|, and returns the corresponding
+ * ImageAsset proxy.
+ */
+ virtual sk_sp<ImageAsset> loadImageAsset(const char resource_path[],
+ const char resource_name[]) const;
+
+ /**
* Load an external font and return as SkData.
*
* @param name font name ("fName" Lottie property)
@@ -68,11 +95,6 @@
*/
class SK_API Logger : public SkRefCnt {
public:
- Logger() = default;
- virtual ~Logger() = default;
- Logger(const Logger&) = delete;
- Logger& operator=(const Logger&) = delete;
-
enum class Level {
kWarning,
kError,
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 41ece92..e4223cc 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -10,6 +10,7 @@
#include "SkCanvas.h"
#include "SkData.h"
#include "SkFontMgr.h"
+#include "SkImage.h"
#include "SkMakeUnique.h"
#include "SkOSPath.h"
#include "SkPaint.h"
@@ -245,6 +246,26 @@
return nullptr;
}
+sk_sp<ImageAsset> ResourceProvider::loadImageAsset(const char path[], const char name[]) const {
+ // Legacy API fallback. TODO: remove after clients get updated.
+ class StaticImageAsset final : public ImageAsset {
+ public:
+ explicit StaticImageAsset(sk_sp<SkImage> img) : fImage(std::move(img)) {}
+
+ bool isMultiFrame() override { return false; }
+
+ sk_sp<SkImage> getFrame(float) override { return fImage; }
+
+ private:
+ sk_sp<SkImage> fImage;
+ };
+
+ auto image = SkImage::MakeFromEncoded(this->load(path, name));
+
+ return image ? sk_make_sp<StaticImageAsset>(std::move(image))
+ : nullptr;
+}
+
sk_sp<SkData> ResourceProvider::loadFont(const char[], const char[]) const {
return nullptr;
}
diff --git a/modules/skottie/src/SkottieLayer.cpp b/modules/skottie/src/SkottieLayer.cpp
index 1928ed1..b53b7f1 100644
--- a/modules/skottie/src/SkottieLayer.cpp
+++ b/modules/skottie/src/SkottieLayer.cpp
@@ -239,8 +239,8 @@
sk_sp<sksg::RenderNode> AnimationBuilder::attachAssetRef(
const skjson::ObjectValue& jlayer, AnimatorScope* ascope,
- sk_sp<sksg::RenderNode>(AnimationBuilder::* attach_proc)(const skjson::ObjectValue& comp,
- AnimatorScope* ascope) const) const {
+ const std::function<sk_sp<sksg::RenderNode>(const skjson::ObjectValue&,
+ AnimatorScope*)>& func) const {
const auto refId = ParseDefault<SkString>(jlayer["refId"], SkString());
if (refId.isEmpty()) {
@@ -265,13 +265,14 @@
}
asset_info->fIsAttaching = true;
- auto asset = (this->*attach_proc)(*asset_info->fAsset, ascope);
+ auto asset = func(*asset_info->fAsset, ascope);
asset_info->fIsAttaching = false;
return asset;
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachSolidLayer(const skjson::ObjectValue& jlayer,
+ const LayerInfo&,
AnimatorScope*) const {
const auto size = SkSize::Make(ParseDefault<float>(jlayer["sw"], 0.0f),
ParseDefault<float>(jlayer["sh"], 0.0f));
@@ -291,8 +292,8 @@
sksg::Color::Make(color));
}
-sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
- AnimatorScope*) const {
+const AnimationBuilder::ImageAssetInfo*
+AnimationBuilder::loadImageAsset(const skjson::ObjectValue& jimage) const {
const skjson::StringValue* name = jimage["p"];
const skjson::StringValue* path = jimage["u"];
if (!name) {
@@ -302,41 +303,91 @@
const auto name_cstr = name->begin(),
path_cstr = path ? path->begin() : "";
const auto res_id = SkStringPrintf("%s|%s", path_cstr, name_cstr);
- if (auto* attached_image = fAssetCache.find(res_id)) {
- return *attached_image;
+ if (auto* cached_info = fImageAssetCache.find(res_id)) {
+ return cached_info;
}
- const auto data = fResourceProvider->load(path_cstr, name_cstr);
- if (!data) {
+ auto asset = fResourceProvider->loadImageAsset(path_cstr, name_cstr);
+ if (!asset) {
this->log(Logger::Level::kError, nullptr,
- "Could not load image resource: %s/%s.", path_cstr, name_cstr);
+ "Could not load image asset: %s/%s.", path_cstr, name_cstr);
return nullptr;
}
- auto image = SkImage::MakeFromEncoded(data);
+ const auto size = SkISize::Make(ParseDefault<int>(jimage["w"], 0),
+ ParseDefault<int>(jimage["h"], 0));
+ return fImageAssetCache.set(res_id, { std::move(asset), size });
+}
+
+sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
+ const LayerInfo& layer_info,
+ AnimatorScope* ascope) const {
+ const auto* asset_info = this->loadImageAsset(jimage);
+ if (!asset_info) {
+ return nullptr;
+ }
+ SkASSERT(asset_info->fAsset);
+
+ auto image = asset_info->fAsset->getFrame(0);
if (!image) {
+ this->log(Logger::Level::kError, nullptr, "Could not load first image asset frame.");
return nullptr;
}
- const auto width = ParseDefault<int>(jimage["w"], image->width()),
- height = ParseDefault<int>(jimage["h"], image->height());
+ auto image_node = sksg::Image::Make(image);
- sk_sp<sksg::RenderNode> image_node = sksg::Image::Make(image);
- if (width != image->width() || height != image->height()) {
- image_node = sksg::Transform::Make(std::move(image_node),
- SkMatrix::MakeScale(static_cast<float>(width) / image->width(),
- static_cast<float>(height) / image->height()));
+ if (asset_info->fAsset->isMultiFrame()) {
+ class MultiFrameAnimator final : public sksg::Animator {
+ public:
+ MultiFrameAnimator(sk_sp<ImageAsset> asset, sk_sp<sksg::Image> image_node,
+ float time_bias, float time_scale)
+ : fAsset(std::move(asset))
+ , fImageNode(std::move(image_node))
+ , fTimeBias(time_bias)
+ , fTimeScale(time_scale) {}
+
+ void onTick(float t) override {
+ fImageNode->setImage(fAsset->getFrame((t + fTimeBias) * fTimeScale));
+ }
+
+ private:
+ sk_sp<ImageAsset> fAsset;
+ sk_sp<sksg::Image> fImageNode;
+ float fTimeBias,
+ fTimeScale;
+ };
+
+ ascope->push_back(skstd::make_unique<MultiFrameAnimator>(asset_info->fAsset,
+ image_node,
+ layer_info.fInPoint,
+ 1 / fFrameRate));
}
- return *fAssetCache.set(res_id, std::move(image_node));
+ const auto asset_size = SkISize::Make(
+ asset_info->fSize.width() > 0 ? asset_info->fSize.width() : image->width(),
+ asset_info->fSize.height() > 0 ? asset_info->fSize.height() : image->height());
+
+ if (asset_size == image->bounds().size()) {
+ // No resize needed.
+ return std::move(image_node);
+ }
+
+ return sksg::Transform::Make(std::move(image_node),
+ SkMatrix::MakeScale(static_cast<float>(asset_size.width()) / image->width(),
+ static_cast<float>(asset_size.height()) / image->height()));
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectValue& jlayer,
+ const LayerInfo& layer_info,
AnimatorScope* ascope) const {
- return this->attachAssetRef(jlayer, ascope, &AnimationBuilder::attachImageAsset);
+ return this->attachAssetRef(jlayer, ascope,
+ [this, &layer_info] (const skjson::ObjectValue& jimage, AnimatorScope* ascope) {
+ return this->attachImageAsset(jimage, layer_info, ascope);
+ });
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachNullLayer(const skjson::ObjectValue& layer,
+ const LayerInfo&,
AnimatorScope*) const {
// Null layers are used solely to drive dependent transforms,
// but we use free-floating sksg::Matrices for that purpose.
@@ -410,9 +461,20 @@
AttachLayerContext* layerCtx) const {
if (!jlayer) return nullptr;
+ const LayerInfo layer_info = {
+ ParseDefault<float>((*jlayer)["ip"], 0.0f),
+ ParseDefault<float>((*jlayer)["op"], 0.0f)
+ };
+ if (layer_info.fInPoint >= layer_info.fOutPoint) {
+ this->log(Logger::Level::kError, nullptr,
+ "Invalid layer in/out points: %f/%f.", layer_info.fInPoint, layer_info.fOutPoint);
+ return nullptr;
+ }
+
const AutoPropertyTracker apt(this, *jlayer);
using LayerAttacher = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
+ const LayerInfo&,
AnimatorScope*) const;
static constexpr LayerAttacher gLayerAttachers[] = {
&AnimationBuilder::attachPrecompLayer, // 'ty': 0
@@ -431,7 +493,7 @@
AnimatorScope layer_animators;
// Layer content.
- auto layer = (this->*(gLayerAttachers[type]))(*jlayer, &layer_animators);
+ auto layer = (this->*(gLayerAttachers[type]))(*jlayer, layer_info, &layer_animators);
// Clip layers with explicit dimensions.
float w = 0, h = 0;
@@ -490,14 +552,13 @@
};
auto controller_node = sksg::OpacityEffect::Make(std::move(layer));
- const auto in = ParseDefault<float>((*jlayer)["ip"], 0.0f),
- out = ParseDefault<float>((*jlayer)["op"], in);
-
- if (in >= out || !controller_node)
+ if (!controller_node) {
return nullptr;
+ }
layerCtx->fScope->push_back(
- skstd::make_unique<LayerController>(std::move(layer_animators), controller_node, in, out));
+ skstd::make_unique<LayerController>(std::move(layer_animators), controller_node,
+ layer_info.fInPoint, layer_info.fOutPoint));
if (ParseDefault<bool>((*jlayer)["td"], false)) {
// This layer is a matte. We apply it as a mask to the next layer.
diff --git a/modules/skottie/src/SkottiePrecompLayer.cpp b/modules/skottie/src/SkottiePrecompLayer.cpp
index f229795..f110c2d 100644
--- a/modules/skottie/src/SkottiePrecompLayer.cpp
+++ b/modules/skottie/src/SkottiePrecompLayer.cpp
@@ -19,6 +19,7 @@
namespace internal {
sk_sp<sksg::RenderNode> AnimationBuilder::attachPrecompLayer(const skjson::ObjectValue& jlayer,
+ const LayerInfo&,
AnimatorScope* ascope) const {
const skjson::ObjectValue* time_remap = jlayer["tm"];
// Empirically, a time mapper supersedes start/stretch.
@@ -31,7 +32,10 @@
AnimatorScope local_animators;
auto precomp_layer = this->attachAssetRef(jlayer,
requires_time_mapping ? &local_animators : ascope,
- &AnimationBuilder::attachComposition);
+ [this] (const skjson::ObjectValue& jcomp,
+ AnimatorScope* ascope) {
+ return this->attachComposition(jcomp, ascope);
+ });
// Applies a bias/scale/remap t-adjustment to child animators.
class CompTimeMapper final : public sksg::GroupAnimator {
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 97ca6a3..741c36a 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -18,6 +18,8 @@
#include "SkTypeface.h"
#include "SkUTF.h"
+#include <functional>
+
class SkFontMgr;
namespace skjson {
@@ -77,6 +79,8 @@
private:
struct AttachLayerContext;
struct AttachShapeContext;
+ struct ImageAssetInfo;
+ struct LayerInfo;
void parseAssets(const skjson::ArrayValue*);
void parseFonts (const skjson::ObjectValue* jfonts,
@@ -89,18 +93,26 @@
sk_sp<sksg::RenderNode> attachShape(const skjson::ArrayValue*, AttachShapeContext*) const;
sk_sp<sksg::RenderNode> attachAssetRef(const skjson::ObjectValue&, AnimatorScope*,
- sk_sp<sksg::RenderNode>(AnimationBuilder::*)(const skjson::ObjectValue&,
- AnimatorScope* ctx) const) const;
- sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, AnimatorScope*) const;
+ const std::function<sk_sp<sksg::RenderNode>(const skjson::ObjectValue&,
+ AnimatorScope* ctx)>&) const;
+ const ImageAssetInfo* loadImageAsset(const skjson::ObjectValue&) const;
+ sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
sk_sp<sksg::RenderNode> attachNestedAnimation(const char* name, AnimatorScope* ascope) const;
- sk_sp<sksg::RenderNode> attachImageLayer (const skjson::ObjectValue&, AnimatorScope*) const;
- sk_sp<sksg::RenderNode> attachNullLayer (const skjson::ObjectValue&, AnimatorScope*) const;
- sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, AnimatorScope*) const;
- sk_sp<sksg::RenderNode> attachShapeLayer (const skjson::ObjectValue&, AnimatorScope*) const;
- sk_sp<sksg::RenderNode> attachSolidLayer (const skjson::ObjectValue&, AnimatorScope*) const;
- sk_sp<sksg::RenderNode> attachTextLayer (const skjson::ObjectValue&, AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachImageLayer (const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachNullLayer (const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachShapeLayer (const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachSolidLayer (const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
+ sk_sp<sksg::RenderNode> attachTextLayer (const skjson::ObjectValue&, const LayerInfo&,
+ AnimatorScope*) const;
bool dispatchColorProperty(const sk_sp<sksg::Color>&) const;
bool dispatchOpacityProperty(const sk_sp<sksg::OpacityEffect>&) const;
@@ -156,6 +168,11 @@
mutable const char* fPropertyObserverContext;
+ struct LayerInfo {
+ float fInPoint,
+ fOutPoint;
+ };
+
struct AssetInfo {
const skjson::ObjectValue* fAsset;
mutable bool fIsAttaching; // Used for cycle detection
@@ -170,14 +187,14 @@
bool matches(const char family[], const char style[]) const;
};
- // TODO: consolidate these two?
- using AssetMap = SkTHashMap<SkString, AssetInfo>;
- using AssetCache = SkTHashMap<SkString, sk_sp<sksg::RenderNode>>;
- using FontMap = SkTHashMap<SkString, FontInfo>;
+ struct ImageAssetInfo {
+ sk_sp<ImageAsset> fAsset;
+ SkISize fSize;
+ };
- AssetMap fAssets;
- FontMap fFonts;
- mutable AssetCache fAssetCache;
+ SkTHashMap<SkString, AssetInfo> fAssets;
+ SkTHashMap<SkString, FontInfo> fFonts;
+ mutable SkTHashMap<SkString, ImageAssetInfo> fImageAssetCache;
using INHERITED = SkNoncopyable;
};
diff --git a/modules/skottie/src/SkottieShapeLayer.cpp b/modules/skottie/src/SkottieShapeLayer.cpp
index dd37620..d0668d2 100644
--- a/modules/skottie/src/SkottieShapeLayer.cpp
+++ b/modules/skottie/src/SkottieShapeLayer.cpp
@@ -618,6 +618,7 @@
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
+ const LayerInfo&,
AnimatorScope* ascope) const {
std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
std::vector<GeometryEffectRec> geometryEffectStack;
diff --git a/modules/skottie/src/SkottieTextLayer.cpp b/modules/skottie/src/SkottieTextLayer.cpp
index 1f043b9..68929c4 100644
--- a/modules/skottie/src/SkottieTextLayer.cpp
+++ b/modules/skottie/src/SkottieTextLayer.cpp
@@ -236,6 +236,7 @@
}
sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& layer,
+ const LayerInfo&,
AnimatorScope* ascope) const {
// General text node format:
// "t": {
diff --git a/modules/sksg/include/SkSGImage.h b/modules/sksg/include/SkSGImage.h
index dd524c3..b6aef47 100644
--- a/modules/sksg/include/SkSGImage.h
+++ b/modules/sksg/include/SkSGImage.h
@@ -23,9 +23,10 @@
class Image final : public RenderNode {
public:
static sk_sp<Image> Make(sk_sp<SkImage> image) {
- return image ? sk_sp<Image>(new Image(std::move(image))) : nullptr;
+ return sk_sp<Image>(new Image(std::move(image)));
}
+ SG_ATTRIBUTE(Image, sk_sp<SkImage> , fImage )
SG_ATTRIBUTE(Quality , SkFilterQuality, fQuality )
SG_ATTRIBUTE(AntiAlias, bool , fAntiAlias)
@@ -37,9 +38,9 @@
SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
private:
- const sk_sp<SkImage> fImage;
- SkFilterQuality fQuality = kLow_SkFilterQuality;
- bool fAntiAlias = true;
+ sk_sp<SkImage> fImage;
+ SkFilterQuality fQuality = kLow_SkFilterQuality;
+ bool fAntiAlias = true;
typedef RenderNode INHERITED;
};
diff --git a/modules/sksg/src/SkSGImage.cpp b/modules/sksg/src/SkSGImage.cpp
index 87c686d..59da5d7 100644
--- a/modules/sksg/src/SkSGImage.cpp
+++ b/modules/sksg/src/SkSGImage.cpp
@@ -15,6 +15,10 @@
Image::Image(sk_sp<SkImage> image) : fImage(std::move(image)) {}
void Image::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
+ if (!fImage) {
+ return;
+ }
+
SkPaint paint;
paint.setAntiAlias(fAntiAlias);
paint.setFilterQuality(fQuality);
@@ -27,7 +31,7 @@
}
SkRect Image::onRevalidate(InvalidationController*, const SkMatrix& ctm) {
- return SkRect::Make(fImage->bounds());
+ return fImage ? SkRect::Make(fImage->bounds()) : SkRect::MakeEmpty();
}
} // namespace sksg
diff --git a/resources/images/flightAnim.gif b/resources/images/flightAnim.gif
new file mode 100644
index 0000000..7aaa327
--- /dev/null
+++ b/resources/images/flightAnim.gif
Binary files differ
diff --git a/resources/skottie/skottie_sample_multiframe.json b/resources/skottie/skottie_sample_multiframe.json
new file mode 100644
index 0000000..afd7849
--- /dev/null
+++ b/resources/skottie/skottie_sample_multiframe.json
@@ -0,0 +1,52 @@
+{
+ "v":"4.6.9",
+ "fr":60,
+ "ip":0,
+ "op":200,
+ "w":800,
+ "h":600,
+ "ddd":0,
+
+ "assets": [
+ {
+ "id": "image_0",
+ "p": "image_0.png",
+ "u": "images/",
+ "w": 600,
+ "h": 400
+ }
+ ],
+
+ "layers":[
+ {
+ "ddd":0,
+ "ind":1,
+ "ty":2,
+ "refId": "image_0",
+ "nm":"Custom Path 1",
+ "ao": 0,
+ "ip": 0,
+ "op": 200,
+ "st": 0,
+ "sr": 1,
+ "bm": 0,
+ "ks": {
+ "o": { "a":0, "k":100 },
+ "r": { "a":1, "k": [
+ { "t": 0, "s": 0, "e": 5 },
+ { "t": 50, "s": 5, "e": -5 },
+ { "t": 150, "s": -5, "e": 0 },
+ { "t": 200 }
+ ]},
+ "p": { "a":0, "k":[ 400, 300, 0 ] },
+ "a": { "a":0, "k":[ 300, 200, 0 ] },
+ "s": { "a":1, "k":[
+ { "t": 0, "s": [ 100, 100, 100 ], "e": [ 150, 150, 100 ] },
+ { "t": 100, "s": [ 150, 150, 100 ], "e": [ 100, 100, 100 ] },
+ { "t": 200 }
+ ]}
+ }
+
+ }
+ ]
+}