animation/lottie: updated the frame count unit.

replace the frame count unit from the int32_t to float
since animations could smoothly interpolate key-frames.

This notificably improve the animation smoothness in Lottie

Beta API changes:
Result Animation::frame(uint32_t no) -> Result Animation::frame(float no)
uint32_t Animation::curFrame() const -> float Animation::curFrame() const
uint32_t Animation::totalFrame() const -> float Animation::totalFrame() const
This commit is contained in:
Hermet Park 2023-10-23 18:25:57 +09:00 committed by Hermet Park
parent fea0d1bb77
commit e570064eba
19 changed files with 137 additions and 146 deletions

View file

@ -1708,7 +1708,7 @@ public:
* *
* @BETA_API * @BETA_API
*/ */
Result frame(uint32_t no) noexcept; Result frame(float no) noexcept;
/** /**
* @brief Retrieves a picture instance associated with this animation instance. * @brief Retrieves a picture instance associated with this animation instance.
@ -1732,12 +1732,12 @@ public:
* *
* @note If the Picture is not properly configured, this function will return 0. * @note If the Picture is not properly configured, this function will return 0.
* *
* @see Animation::frame(uint32_t no) * @see Animation::frame(float no)
* @see Animation::totalFrame() * @see Animation::totalFrame()
* *
* @BETA_API * @BETA_API
*/ */
uint32_t curFrame() const noexcept; float curFrame() const noexcept;
/** /**
* @brief Retrieves the total number of frames in the animation. * @brief Retrieves the total number of frames in the animation.
@ -1749,7 +1749,7 @@ public:
* *
* @BETA_API * @BETA_API
*/ */
uint32_t totalFrame() const noexcept; float totalFrame() const noexcept;
/** /**
* @brief Retrieves the duration of the animation in seconds. * @brief Retrieves the duration of the animation in seconds.

View file

@ -2234,7 +2234,7 @@ TVG_API Tvg_Animation* tvg_animation_new();
* *
* \see tvg_animation_get_total_frame() * \see tvg_animation_get_total_frame()
*/ */
TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, uint32_t no); TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, float no);
/*! /*!
@ -2266,7 +2266,7 @@ TVG_API Tvg_Paint* tvg_animation_get_picture(Tvg_Animation* animation);
* \see tvg_animation_get_total_frame() * \see tvg_animation_get_total_frame()
* \see tvg_animation_set_frame() * \see tvg_animation_set_frame()
*/ */
TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, uint32_t* no); TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, float* no);
/*! /*!
@ -2282,7 +2282,7 @@ TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, uint32_t* n
* \note Frame numbering starts from 0. * \note Frame numbering starts from 0.
* \note If the Picture is not properly configured, this function will return 0. * \note If the Picture is not properly configured, this function will return 0.
*/ */
TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, uint32_t* cnt); TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, float* cnt);
/*! /*!

View file

@ -728,14 +728,14 @@ TVG_API Tvg_Animation* tvg_animation_new()
} }
TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, uint32_t no) TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, float no)
{ {
if (!animation) return TVG_RESULT_INVALID_ARGUMENT; if (!animation) return TVG_RESULT_INVALID_ARGUMENT;
return (Tvg_Result) reinterpret_cast<Animation*>(animation)->frame(no); return (Tvg_Result) reinterpret_cast<Animation*>(animation)->frame(no);
} }
TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, uint32_t* no) TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, float* no)
{ {
if (!animation || !no) return TVG_RESULT_INVALID_ARGUMENT; if (!animation || !no) return TVG_RESULT_INVALID_ARGUMENT;
*no = reinterpret_cast<Animation*>(animation)->curFrame(); *no = reinterpret_cast<Animation*>(animation)->curFrame();
@ -743,7 +743,7 @@ TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, uint32_t* n
} }
TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, uint32_t* cnt) TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, float* cnt)
{ {
if (!animation || !cnt) return TVG_RESULT_INVALID_ARGUMENT; if (!animation || !cnt) return TVG_RESULT_INVALID_ARGUMENT;
*cnt = reinterpret_cast<Animation*>(animation)->totalFrame(); *cnt = reinterpret_cast<Animation*>(animation)->totalFrame();

View file

@ -149,10 +149,9 @@ public:
return val(animation->totalFrame()); return val(animation->totalFrame());
} }
bool frame(uint32_t no) bool frame(float no)
{ {
if (!canvas || !animation) return false; if (!canvas || !animation) return false;
if (animation->curFrame() == no) return true;
if (animation->frame(no) != Result::Success) { if (animation->frame(no) != Result::Success) {
errorMsg = "frame() fail"; errorMsg = "frame() fail";
return false; return false;

View file

@ -34,10 +34,7 @@ void tvgUpdateCmds(tvg::Canvas* canvas, tvg::Animation* animation, float progres
if (!canvas) return; if (!canvas) return;
//Update animation frame only when it's changed //Update animation frame only when it's changed
auto frame = lroundf(animation->totalFrame() * progress); if (animation->frame(animation->totalFrame() * progress) == tvg::Result::Success) {
if (frame != animation->curFrame()) {
animation->frame(frame);
canvas->update(animation->picture()); canvas->update(animation->picture());
} }
} }

View file

@ -257,19 +257,18 @@ void transitCb(Elm_Transit_Effect *effect, Elm_Transit* transit, double progress
{ {
if (!canvas) return; if (!canvas) return;
uint32_t total_frame = 0; float total_frame = 0.0f;
tvg_animation_get_total_frame(animation, &total_frame); tvg_animation_get_total_frame(animation, &total_frame);
uint32_t new_frame = lroundf(total_frame * progress); float new_frame = total_frame * progress;
uint32_t cur_frame = 0; float cur_frame = 0.0f;
tvg_animation_get_frame(animation, &cur_frame); tvg_animation_get_frame(animation, &cur_frame);
//Update animation frame only when it's changed //Update animation frame only when it's changed
if (new_frame == cur_frame) return; if (tvg_animation_set_frame(animation, new_frame) == TVG_RESULT_SUCCESS) {
tvg_animation_set_frame(animation, new_frame);
tvg_canvas_update_paint(canvas, tvg_animation_get_picture(animation)); tvg_canvas_update_paint(canvas, tvg_animation_get_picture(animation));
}
//Draw the canvas //Draw the canvas
tvg_canvas_draw(canvas); tvg_canvas_draw(canvas);

View file

@ -93,14 +93,10 @@ void tvgUpdateCmds(Elm_Transit_Effect *effect, Elm_Transit* transit, double prog
auto animation = static_cast<tvg::Animation*>(effect); auto animation = static_cast<tvg::Animation*>(effect);
//Update animation frame only when it's changed //Update animation frame only when it's changed
auto frame = lroundf(animation->totalFrame() * progress);
if (frame != animation->curFrame()) {
auto before = ecore_time_get(); auto before = ecore_time_get();
animation->frame(frame); animation->frame(animation->totalFrame() * progress);
auto after = ecore_time_get(); auto after = ecore_time_get();
updateTime += after - before; updateTime += after - before;
}
} }
void tvgDrawCmds(tvg::Canvas* canvas) void tvgDrawCmds(tvg::Canvas* canvas)

View file

@ -83,8 +83,8 @@ struct RenderContext
}; };
static void _updateChildren(LottieGroup* parent, int32_t frameNo, queue<RenderContext>& contexts); static void _updateChildren(LottieGroup* parent, float frameNo, queue<RenderContext>& contexts);
static void _updateLayer(LottieLayer* root, LottieLayer* layer, int32_t frameNo); static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo);
static bool _buildPrecomp(LottieComposition* comp, LottieGroup* parent); static bool _buildPrecomp(LottieComposition* comp, LottieGroup* parent);
static void _rotateX(Matrix* m, float degree) static void _rotateX(Matrix* m, float degree)
@ -114,7 +114,7 @@ static void _rotationZ(Matrix* m, float degree)
} }
static bool _updateTransform(LottieTransform* transform, int32_t frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity) static bool _updateTransform(LottieTransform* transform, float frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity)
{ {
mathIdentity(&matrix); mathIdentity(&matrix);
@ -154,9 +154,9 @@ static bool _updateTransform(LottieTransform* transform, int32_t frameNo, bool a
} }
static void _updateTransform(LottieLayer* layer, int32_t frameNo) static void _updateTransform(LottieLayer* layer, float frameNo)
{ {
if (!layer || layer->cache.frameNo == frameNo) return; if (!layer || mathEqual(layer->cache.frameNo, frameNo)) return;
auto transform = layer->transform; auto transform = layer->transform;
auto parent = layer->parent; auto parent = layer->parent;
@ -177,7 +177,7 @@ static void _updateTransform(LottieLayer* layer, int32_t frameNo)
} }
static void _updateTransform(LottieTransform* transform, int32_t frameNo, RenderContext& ctx) static void _updateTransform(LottieTransform* transform, float frameNo, RenderContext& ctx)
{ {
if (!transform) return; if (!transform) return;
@ -198,7 +198,7 @@ static void _updateTransform(LottieTransform* transform, int32_t frameNo, Render
} }
static void _updateGroup(LottieGroup* parent, LottieGroup* group, int32_t frameNo, RenderContext& ctx) static void _updateGroup(LottieGroup* parent, LottieGroup* group, float frameNo, RenderContext& ctx)
{ {
if (group->children.empty()) return; if (group->children.empty()) return;
@ -212,7 +212,7 @@ static void _updateGroup(LottieGroup* parent, LottieGroup* group, int32_t frameN
} }
static void _updateStroke(LottieStroke* stroke, int32_t frameNo, RenderContext& ctx) static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext& ctx)
{ {
ctx.propagator->stroke(stroke->width(frameNo)); ctx.propagator->stroke(stroke->width(frameNo));
ctx.propagator->stroke(stroke->cap); ctx.propagator->stroke(stroke->cap);
@ -245,7 +245,7 @@ static bool _fragmentedStroking(LottieObject** child, queue<RenderContext>& cont
} }
static void _updateSolidStroke(LottieObject** child, int32_t frameNo, queue<RenderContext>& contexts, RenderContext& ctx) static void _updateSolidStroke(LottieObject** child, float frameNo, queue<RenderContext>& contexts, RenderContext& ctx)
{ {
if (_fragmentedStroking(child, contexts, ctx)) return; if (_fragmentedStroking(child, contexts, ctx)) return;
@ -259,7 +259,7 @@ static void _updateSolidStroke(LottieObject** child, int32_t frameNo, queue<Rend
} }
static void _updateGradientStroke(LottieObject** child, int32_t frameNo, queue<RenderContext>& contexts, RenderContext& ctx) static void _updateGradientStroke(LottieObject** child, float frameNo, queue<RenderContext>& contexts, RenderContext& ctx)
{ {
if (_fragmentedStroking(child, contexts, ctx)) return; if (_fragmentedStroking(child, contexts, ctx)) return;
@ -272,7 +272,7 @@ static void _updateGradientStroke(LottieObject** child, int32_t frameNo, queue<R
} }
static void _updateFill(LottieSolidFill* fill, int32_t frameNo, RenderContext& ctx) static void _updateFill(LottieSolidFill* fill, float frameNo, RenderContext& ctx)
{ {
if (ctx.stroking) return; if (ctx.stroking) return;
@ -286,7 +286,7 @@ static void _updateFill(LottieSolidFill* fill, int32_t frameNo, RenderContext& c
} }
static Shape* _updateFill(LottieGradientFill* fill, int32_t frameNo, RenderContext& ctx) static Shape* _updateFill(LottieGradientFill* fill, float frameNo, RenderContext& ctx)
{ {
if (ctx.stroking) return nullptr; if (ctx.stroking) return nullptr;
@ -303,7 +303,7 @@ static Shape* _updateFill(LottieGradientFill* fill, int32_t frameNo, RenderConte
} }
static Shape* _draw(LottieGroup* parent, int32_t frameNo, RenderContext& ctx) static Shape* _draw(LottieGroup* parent, RenderContext& ctx)
{ {
if (ctx.allowMerging && ctx.merging) return ctx.merging; if (ctx.allowMerging && ctx.merging) return ctx.merging;
@ -316,7 +316,7 @@ static Shape* _draw(LottieGroup* parent, int32_t frameNo, RenderContext& ctx)
//OPTIMIZE: path? //OPTIMIZE: path?
static void _repeat(LottieGroup* parent, int32_t frameNo, unique_ptr<Shape> path, RenderContext& ctx) static void _repeat(LottieGroup* parent, unique_ptr<Shape> path, RenderContext& ctx)
{ {
auto repeater = ctx.repeater; auto repeater = ctx.repeater;
@ -363,7 +363,7 @@ static void _repeat(LottieGroup* parent, int32_t frameNo, unique_ptr<Shape> path
} }
static void _updateRect(LottieGroup* parent, LottieRect* rect, int32_t frameNo, RenderContext& ctx) static void _updateRect(LottieGroup* parent, LottieRect* rect, float frameNo, RenderContext& ctx)
{ {
auto position = rect->position(frameNo); auto position = rect->position(frameNo);
auto size = rect->size(frameNo); auto size = rect->size(frameNo);
@ -378,15 +378,15 @@ static void _updateRect(LottieGroup* parent, LottieRect* rect, int32_t frameNo,
if (ctx.repeater) { if (ctx.repeater) {
auto path = Shape::gen(); auto path = Shape::gen();
path->appendRect(position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, roundness); path->appendRect(position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, roundness);
_repeat(parent, frameNo, std::move(path), ctx); _repeat(parent, std::move(path), ctx);
} else { } else {
auto merging = _draw(parent, frameNo, ctx); auto merging = _draw(parent, ctx);
merging->appendRect(position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, roundness); merging->appendRect(position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, roundness);
} }
} }
static void _updateEllipse(LottieGroup* parent, LottieEllipse* ellipse, int32_t frameNo, RenderContext& ctx) static void _updateEllipse(LottieGroup* parent, LottieEllipse* ellipse, float frameNo, RenderContext& ctx)
{ {
auto position = ellipse->position(frameNo); auto position = ellipse->position(frameNo);
auto size = ellipse->size(frameNo); auto size = ellipse->size(frameNo);
@ -394,22 +394,22 @@ static void _updateEllipse(LottieGroup* parent, LottieEllipse* ellipse, int32_t
if (ctx.repeater) { if (ctx.repeater) {
auto path = Shape::gen(); auto path = Shape::gen();
path->appendCircle(position.x, position.y, size.x * 0.5f, size.y * 0.5f); path->appendCircle(position.x, position.y, size.x * 0.5f, size.y * 0.5f);
_repeat(parent, frameNo, std::move(path), ctx); _repeat(parent, std::move(path), ctx);
} else { } else {
auto merging = _draw(parent, frameNo, ctx); auto merging = _draw(parent, ctx);
merging->appendCircle(position.x, position.y, size.x * 0.5f, size.y * 0.5f); merging->appendCircle(position.x, position.y, size.x * 0.5f, size.y * 0.5f);
} }
} }
static void _updatePath(LottieGroup* parent, LottiePath* path, int32_t frameNo, RenderContext& ctx) static void _updatePath(LottieGroup* parent, LottiePath* path, float frameNo, RenderContext& ctx)
{ {
if (ctx.repeater) { if (ctx.repeater) {
auto p = Shape::gen(); auto p = Shape::gen();
path->pathset(frameNo, P(p)->rs.path.cmds, P(p)->rs.path.pts); path->pathset(frameNo, P(p)->rs.path.cmds, P(p)->rs.path.pts);
_repeat(parent, frameNo, std::move(p), ctx); _repeat(parent, std::move(p), ctx);
} else { } else {
auto merging = _draw(parent, frameNo, ctx); auto merging = _draw(parent, ctx);
if (path->pathset(frameNo, P(merging)->rs.path.cmds, P(merging)->rs.path.pts)) { if (path->pathset(frameNo, P(merging)->rs.path.cmds, P(merging)->rs.path.pts)) {
P(merging)->update(RenderUpdateFlag::Path); P(merging)->update(RenderUpdateFlag::Path);
@ -423,7 +423,7 @@ static void _updatePath(LottieGroup* parent, LottiePath* path, int32_t frameNo,
} }
static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, int32_t frameNo, Shape* merging) static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging)
{ {
static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f; static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f;
@ -532,7 +532,7 @@ static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* trans
} }
static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, int32_t frameNo, Shape* merging) static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging)
{ {
static constexpr auto POLYGON_MAGIC_NUMBER = 0.25f; static constexpr auto POLYGON_MAGIC_NUMBER = 0.25f;
@ -601,7 +601,7 @@ static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* tr
} }
static void _updatePolystar(LottieGroup* parent, LottiePolyStar* star, int32_t frameNo, RenderContext& ctx) static void _updatePolystar(LottieGroup* parent, LottiePolyStar* star, float frameNo, RenderContext& ctx)
{ {
//Optimize: Can we skip the individual coords transform? //Optimize: Can we skip the individual coords transform?
Matrix matrix; Matrix matrix;
@ -616,9 +616,9 @@ static void _updatePolystar(LottieGroup* parent, LottiePolyStar* star, int32_t f
auto p = Shape::gen(); auto p = Shape::gen();
if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, p.get()); if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, p.get());
else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, p.get()); else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, p.get());
_repeat(parent, frameNo, std::move(p), ctx); _repeat(parent, std::move(p), ctx);
} else { } else {
auto merging = _draw(parent, frameNo, ctx); auto merging = _draw(parent, ctx);
if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, merging); if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, merging);
else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, merging); else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, merging);
P(merging)->update(RenderUpdateFlag::Path); P(merging)->update(RenderUpdateFlag::Path);
@ -626,7 +626,7 @@ static void _updatePolystar(LottieGroup* parent, LottiePolyStar* star, int32_t f
} }
static void _updateImage(LottieGroup* parent, LottieImage* image, int32_t frameNo, RenderContext& ctx) static void _updateImage(LottieGroup* parent, LottieImage* image, float frameNo, RenderContext& ctx)
{ {
auto picture = image->picture; auto picture = image->picture;
@ -665,14 +665,14 @@ static void _updateImage(LottieGroup* parent, LottieImage* image, int32_t frameN
} }
static void _updateRoundedCorner(LottieRoundedCorner* roundedCorner, int32_t frameNo, RenderContext& ctx) static void _updateRoundedCorner(LottieRoundedCorner* roundedCorner, float frameNo, RenderContext& ctx)
{ {
auto roundness = roundedCorner->radius(frameNo); auto roundness = roundedCorner->radius(frameNo);
if (ctx.roundness < roundness) ctx.roundness = roundness; if (ctx.roundness < roundness) ctx.roundness = roundness;
} }
static void _updateRepeater(LottieRepeater* repeater, int32_t frameNo, RenderContext& ctx) static void _updateRepeater(LottieRepeater* repeater, float frameNo, RenderContext& ctx)
{ {
if (!ctx.repeater) ctx.repeater = new RenderRepeater(); if (!ctx.repeater) ctx.repeater = new RenderRepeater();
ctx.repeater->cnt = static_cast<int>(repeater->copies(frameNo)); ctx.repeater->cnt = static_cast<int>(repeater->copies(frameNo));
@ -690,7 +690,7 @@ static void _updateRepeater(LottieRepeater* repeater, int32_t frameNo, RenderCon
} }
static void _updateTrimpath(LottieTrimpath* trimpath, int32_t frameNo, RenderContext& ctx) static void _updateTrimpath(LottieTrimpath* trimpath, float frameNo, RenderContext& ctx)
{ {
float begin, end; float begin, end;
trimpath->segment(frameNo, begin, end); trimpath->segment(frameNo, begin, end);
@ -709,7 +709,7 @@ static void _updateTrimpath(LottieTrimpath* trimpath, int32_t frameNo, RenderCon
} }
static void _updateChildren(LottieGroup* parent, int32_t frameNo, queue<RenderContext>& contexts) static void _updateChildren(LottieGroup* parent, float frameNo, queue<RenderContext>& contexts)
{ {
contexts.front().begin = parent->children.end() - 1; contexts.front().begin = parent->children.end() - 1;
@ -783,7 +783,7 @@ static void _updateChildren(LottieGroup* parent, int32_t frameNo, queue<RenderCo
} }
static void _updatePrecomp(LottieLayer* precomp, int32_t frameNo) static void _updatePrecomp(LottieLayer* precomp, float frameNo)
{ {
if (precomp->children.count == 0) return; if (precomp->children.count == 0) return;
@ -808,7 +808,7 @@ static void _updatePrecomp(LottieLayer* precomp, int32_t frameNo)
} }
static void _updateSolid(LottieLayer* layer, int32_t frameNo) static void _updateSolid(LottieLayer* layer, float frameNo)
{ {
auto shape = Shape::gen(); auto shape = Shape::gen();
shape->appendRect(0, 0, static_cast<float>(layer->w), static_cast<float>(layer->h)); shape->appendRect(0, 0, static_cast<float>(layer->w), static_cast<float>(layer->h));
@ -817,7 +817,7 @@ static void _updateSolid(LottieLayer* layer, int32_t frameNo)
} }
static void _updateMaskings(LottieLayer* layer, int32_t frameNo) static void _updateMaskings(LottieLayer* layer, float frameNo)
{ {
if (layer->masks.count == 0) return; if (layer->masks.count == 0) return;
@ -855,7 +855,7 @@ static void _updateMaskings(LottieLayer* layer, int32_t frameNo)
} }
static void _updateLayer(LottieLayer* root, LottieLayer* layer, int32_t frameNo) static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo)
{ {
layer->scene = nullptr; layer->scene = nullptr;
@ -1021,7 +1021,7 @@ static bool _buildPrecomp(LottieComposition* comp, LottieGroup* parent)
/* External Class Implementation */ /* External Class Implementation */
/************************************************************************/ /************************************************************************/
bool LottieBuilder::update(LottieComposition* comp, int32_t frameNo) bool LottieBuilder::update(LottieComposition* comp, float frameNo)
{ {
frameNo += comp->startFrame; frameNo += comp->startFrame;
if (frameNo < comp->startFrame) frameNo = comp->startFrame; if (frameNo < comp->startFrame) frameNo = comp->startFrame;

View file

@ -29,7 +29,7 @@ struct LottieComposition;
struct LottieBuilder struct LottieBuilder
{ {
bool update(LottieComposition* comp, int32_t frameNo); bool update(LottieComposition* comp, float progress);
void build(LottieComposition* comp); void build(LottieComposition* comp);
}; };

View file

@ -191,7 +191,8 @@ bool LottieLoader::header()
return false; return false;
} }
frameDuration = (endFrame - startFrame) / frameRate; frameCnt = (endFrame - startFrame);
frameDuration = frameCnt / frameRate;
TVGLOG("LOTTIE", "info: frame rate = %f, duration = %f size = %d x %d", frameRate, frameDuration, (int)w, (int)h); TVGLOG("LOTTIE", "info: frame rate = %f, duration = %f size = %d x %d", frameRate, frameDuration, (int)w, (int)h);
@ -286,6 +287,10 @@ bool LottieLoader::read()
TaskScheduler::request(this); TaskScheduler::request(this);
if (comp && TaskScheduler::threads() == 0) {
frameCnt = comp->frameCnt();
}
return true; return true;
} }
@ -309,15 +314,13 @@ unique_ptr<Paint> LottieLoader::paint()
} }
bool LottieLoader::frame(uint32_t frameNo) bool LottieLoader::frame(float no)
{ {
if (this->frameNo == frameNo) return true; if (mathEqual(this->frameNo, no)) return false;
this->done(); this->done();
if (!comp || frameNo >= comp->frameCnt()) return false; this->frameNo = no;
this->frameNo = frameNo;
TaskScheduler::request(this); TaskScheduler::request(this);
@ -325,16 +328,13 @@ bool LottieLoader::frame(uint32_t frameNo)
} }
uint32_t LottieLoader::totalFrame() float LottieLoader::totalFrame()
{ {
this->done(); return frameCnt;
if (!comp) return 0;
return comp->frameCnt();
} }
uint32_t LottieLoader::curFrame() float LottieLoader::curFrame()
{ {
return frameNo; return frameNo;
} }

View file

@ -35,8 +35,9 @@ class LottieLoader : public FrameModule, public Task
public: public:
const char* content = nullptr; //lottie file data const char* content = nullptr; //lottie file data
uint32_t size = 0; //lottie data size uint32_t size = 0; //lottie data size
uint32_t frameNo = 0; //current frame number float frameNo = 0.0f; //current frame number
float frameDuration; float frameCnt = 0.0f;
float frameDuration = 0.0f;
LottieBuilder* builder = nullptr; LottieBuilder* builder = nullptr;
LottieComposition* comp = nullptr; LottieComposition* comp = nullptr;
@ -57,9 +58,9 @@ public:
unique_ptr<Paint> paint() override; unique_ptr<Paint> paint() override;
//Frame Controls //Frame Controls
bool frame(uint32_t frameNo) override; bool frame(float no) override;
uint32_t totalFrame() override; float totalFrame() override;
uint32_t curFrame() override; float curFrame() override;
float duration() override; float duration() override;
void sync() override; void sync() override;

View file

@ -34,7 +34,7 @@
/* External Class Implementation */ /* External Class Implementation */
/************************************************************************/ /************************************************************************/
void LottieTrimpath::segment(int32_t frameNo, float& start, float& end) void LottieTrimpath::segment(float frameNo, float& start, float& end)
{ {
auto s = this->start(frameNo) * 0.01f; auto s = this->start(frameNo) * 0.01f;
auto e = this->end(frameNo) * 0.01f; auto e = this->end(frameNo) * 0.01f;
@ -77,7 +77,7 @@ void LottieTrimpath::segment(int32_t frameNo, float& start, float& end)
} }
Fill* LottieGradient::fill(int32_t frameNo) Fill* LottieGradient::fill(float frameNo)
{ {
Fill* fill = nullptr; Fill* fill = nullptr;
@ -145,14 +145,14 @@ void LottieLayer::prepare()
} }
int32_t LottieLayer::remap(int32_t frameNo) float LottieLayer::remap(float frameNo)
{ {
if (timeRemap.frames || timeRemap.value) { if (timeRemap.frames || timeRemap.value) {
frameNo = comp->frameAtTime(timeRemap(frameNo)); frameNo = comp->frameAtTime(timeRemap(frameNo));
} else { } else {
frameNo -= startFrame; frameNo -= startFrame;
} }
return (int32_t)(frameNo / timeStretch); return (frameNo / timeStretch);
} }

View file

@ -49,17 +49,17 @@ struct LottieStroke
return dashattr->value[no]; return dashattr->value[no];
} }
float dashOffset(int32_t frameNo) float dashOffset(float frameNo)
{ {
return dash(0)(frameNo); return dash(0)(frameNo);
} }
float dashGap(int32_t frameNo) float dashGap(float frameNo)
{ {
return dash(2)(frameNo); return dash(2)(frameNo);
} }
float dashSize(int32_t frameNo) float dashSize(float frameNo)
{ {
auto d = dash(1)(frameNo); auto d = dash(1)(frameNo);
if (d == 0.0f) return 0.1f; if (d == 0.0f) return 0.1f;
@ -177,7 +177,7 @@ struct LottieGradient
return false; return false;
} }
Fill* fill(int32_t frameNo); Fill* fill(float frameNo);
LottiePoint start = Point{0.0f, 0.0f}; LottiePoint start = Point{0.0f, 0.0f};
LottiePoint end = Point{0.0f, 0.0f}; LottiePoint end = Point{0.0f, 0.0f};
@ -248,7 +248,7 @@ struct LottieTrimpath : LottieObject
if (start.frames || end.frames || offset.frames) statical = false; if (start.frames || end.frames || offset.frames) statical = false;
} }
void segment(int32_t frameNo, float& start, float& end); void segment(float frameNo, float& start, float& end);
LottieFloat start = 0.0f; LottieFloat start = 0.0f;
LottieFloat end = 0.0f; LottieFloat end = 0.0f;
@ -507,7 +507,7 @@ struct LottieLayer : LottieGroup
delete(transform); delete(transform);
} }
uint8_t opacity(int32_t frameNo) uint8_t opacity(float frameNo)
{ {
//return zero if the visibility is false. //return zero if the visibility is false.
if (type == Null) return 255; if (type == Null) return 255;
@ -515,7 +515,7 @@ struct LottieLayer : LottieGroup
} }
void prepare(); void prepare();
int32_t remap(int32_t frameNo); float remap(float frameNo);
//Optimize: compact data?? //Optimize: compact data??
RGB24 color; RGB24 color;
@ -534,16 +534,16 @@ struct LottieLayer : LottieGroup
float timeStretch = 1.0f; float timeStretch = 1.0f;
uint32_t w = 0, h = 0; uint32_t w = 0, h = 0;
int32_t inFrame = 0; float inFrame = 0.0f;
int32_t outFrame = 0; float outFrame = 0.0f;
uint32_t startFrame = 0; float startFrame = 0.0f;
char* refId = nullptr; //pre-composition reference. char* refId = nullptr; //pre-composition reference.
int16_t pid = -1; //id of the parent layer. int16_t pid = -1; //id of the parent layer.
int16_t id = -1; //id of the current layer. int16_t id = -1; //id of the current layer.
//cached data //cached data
struct { struct {
int32_t frameNo = -1; float frameNo = -1.0f;
Matrix matrix; Matrix matrix;
uint8_t opacity; uint8_t opacity;
} cache; } cache;
@ -583,7 +583,7 @@ struct LottieComposition
char* version = nullptr; char* version = nullptr;
char* name = nullptr; char* name = nullptr;
uint32_t w, h; uint32_t w, h;
int32_t startFrame, endFrame; float startFrame, endFrame;
float frameRate; float frameRate;
Array<LottieObject*> assets; Array<LottieObject*> assets;
Array<LottieInterpolator*> interpolators; Array<LottieInterpolator*> interpolators;

View file

@ -382,7 +382,7 @@ void LottieParser::parseKeyFrame(T& prop)
} }
} }
} else if (!strcmp(key, "t")) { } else if (!strcmp(key, "t")) {
frame.no = lroundf(getFloat()); frame.no = getFloat();
} else if (!strcmp(key, "s")) { } else if (!strcmp(key, "s")) {
getValue(frame.value); getValue(frame.value);
} else if (!strcmp(key, "e")) { } else if (!strcmp(key, "e")) {
@ -1003,9 +1003,9 @@ LottieLayer* LottieParser::parseLayer()
} }
else if (!strcmp(key, "ao")) layer->autoOrient = getInt(); else if (!strcmp(key, "ao")) layer->autoOrient = getInt();
else if (!strcmp(key, "shapes")) parseShapes(layer); else if (!strcmp(key, "shapes")) parseShapes(layer);
else if (!strcmp(key, "ip")) layer->inFrame = lroundf(getFloat()); else if (!strcmp(key, "ip")) layer->inFrame = getFloat();
else if (!strcmp(key, "op")) layer->outFrame = lroundf(getFloat()); else if (!strcmp(key, "op")) layer->outFrame = getFloat();
else if (!strcmp(key, "st")) layer->startFrame = lroundf(getFloat()); else if (!strcmp(key, "st")) layer->startFrame = getFloat();
else if (!strcmp(key, "bm")) layer->blendMethod = getBlendMethod(); else if (!strcmp(key, "bm")) layer->blendMethod = getBlendMethod();
else if (!strcmp(key, "parent")) layer->pid = getInt(); else if (!strcmp(key, "parent")) layer->pid = getInt();
else if (!strcmp(key, "tm")) parseTimeRemap(layer); else if (!strcmp(key, "tm")) parseTimeRemap(layer);
@ -1091,8 +1091,8 @@ bool LottieParser::parse()
while (auto key = nextObjectKey()) { while (auto key = nextObjectKey()) {
if (!strcmp(key, "v")) comp->version = getStringCopy(); if (!strcmp(key, "v")) comp->version = getStringCopy();
else if (!strcmp(key, "fr")) comp->frameRate = getFloat(); else if (!strcmp(key, "fr")) comp->frameRate = getFloat();
else if (!strcmp(key, "ip")) comp->startFrame = lroundf(getFloat()); else if (!strcmp(key, "ip")) comp->startFrame = getFloat();
else if (!strcmp(key, "op")) comp->endFrame = lroundf(getFloat()); else if (!strcmp(key, "op")) comp->endFrame = getFloat();
else if (!strcmp(key, "w")) comp->w = getInt(); else if (!strcmp(key, "w")) comp->w = getInt();
else if (!strcmp(key, "h")) comp->h = getInt(); else if (!strcmp(key, "h")) comp->h = getInt();
else if (!strcmp(key, "nm")) comp->name = getStringCopy(); else if (!strcmp(key, "nm")) comp->name = getStringCopy();

View file

@ -93,13 +93,13 @@ template<typename T>
struct LottieScalarFrame struct LottieScalarFrame
{ {
T value; //keyframe value T value; //keyframe value
int32_t no; //frame number float no; //frame number
LottieInterpolator* interpolator; LottieInterpolator* interpolator;
bool hold = false; //do not interpolate. bool hold = false; //do not interpolate.
T interpolate(LottieScalarFrame<T>* next, int32_t frameNo) T interpolate(LottieScalarFrame<T>* next, float frameNo)
{ {
auto t = float(frameNo - no) / float(next->no - no); auto t = (frameNo - no) / (next->no - no);
if (interpolator) t = interpolator->progress(t); if (interpolator) t = interpolator->progress(t);
if (hold) { if (hold) {
@ -115,16 +115,16 @@ template<typename T>
struct LottieVectorFrame struct LottieVectorFrame
{ {
T value; //keyframe value T value; //keyframe value
int32_t no; //frame number float no; //frame number
LottieInterpolator* interpolator; LottieInterpolator* interpolator;
T outTangent, inTangent; T outTangent, inTangent;
float length; float length;
bool hasTangent = false; bool hasTangent = false;
bool hold = false; bool hold = false;
T interpolate(LottieVectorFrame* next, int32_t frameNo) T interpolate(LottieVectorFrame* next, float frameNo)
{ {
auto t = float(frameNo - no) / float(next->no - no); auto t = (frameNo - no) / (next->no - no);
if (interpolator) t = interpolator->progress(t); if (interpolator) t = interpolator->progress(t);
if (hold) { if (hold) {
@ -141,10 +141,10 @@ struct LottieVectorFrame
} }
} }
float angle(LottieVectorFrame* next, int32_t frameNo) float angle(LottieVectorFrame* next, float frameNo)
{ {
if (!hasTangent) return 0; if (!hasTangent) return 0;
auto t = float(frameNo - no) / float(next->no - no); auto t = (frameNo - no) / (next->no - no);
if (interpolator) t = interpolator->progress(t); if (interpolator) t = interpolator->progress(t);
Bezier bz = {value, value + outTangent, next->value + inTangent, next->value}; Bezier bz = {value, value + outTangent, next->value + inTangent, next->value};
t = bezAt(bz, t * length, length); t = bezAt(bz, t * length, length);
@ -190,7 +190,7 @@ struct LottieProperty
return (*frames)[frames->count]; return (*frames)[frames->count];
} }
T operator()(int32_t frameNo) T operator()(float frameNo)
{ {
if (!frames) return value; if (!frames) return value;
if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value; if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value;
@ -202,7 +202,7 @@ struct LottieProperty
while (low <= high) { while (low <= high) {
auto mid = low + (high - low) / 2; auto mid = low + (high - low) / 2;
auto frame = frames->data + mid; auto frame = frames->data + mid;
if (frameNo == frame->no) return frame->value; if (mathEqual(frameNo, frame->no)) return frame->value;
else if (frameNo > frame->no) low = mid + 1; else if (frameNo > frame->no) low = mid + 1;
else high = mid - 1; else high = mid - 1;
} }
@ -211,7 +211,7 @@ struct LottieProperty
return (frame - 1)->interpolate(frame, frameNo); return (frame - 1)->interpolate(frame, frameNo);
} }
float angle(int32_t frameNo) { return 0; } float angle(float frameNo) { return 0; }
void prepare() {} void prepare() {}
}; };
@ -258,7 +258,7 @@ struct LottiePathSet
return (*frames)[frames->count]; return (*frames)[frames->count];
} }
bool operator()(int32_t frameNo, Array<PathCommand>& cmds, Array<Point>& pts) bool operator()(float frameNo, Array<PathCommand>& cmds, Array<Point>& pts)
{ {
if (!frames) { if (!frames) {
copy(value, cmds); copy(value, cmds);
@ -284,7 +284,7 @@ struct LottiePathSet
while (low <= high) { while (low <= high) {
auto mid = low + (high - low) / 2; auto mid = low + (high - low) / 2;
auto frame = frames->data + mid; auto frame = frames->data + mid;
if (frameNo == frame->no) { if (mathEqual(frameNo, frame->no)) {
copy(frame->value, cmds); copy(frame->value, cmds);
copy(frame->value, pts); copy(frame->value, pts);
return true; return true;
@ -300,7 +300,7 @@ struct LottiePathSet
auto pframe = frame - 1; auto pframe = frame - 1;
copy(pframe->value, cmds); copy(pframe->value, cmds);
auto t = float(frameNo - pframe->no) / float(frame->no - pframe->no); auto t = (frameNo - pframe->no) / (frame->no - pframe->no);
if (pframe->interpolator) t = pframe->interpolator->progress(t); if (pframe->interpolator) t = pframe->interpolator->progress(t);
if (pframe->hold) { if (pframe->hold) {
@ -358,7 +358,7 @@ struct LottieColorStop
return (*frames)[frames->count]; return (*frames)[frames->count];
} }
void operator()(int32_t frameNo, Fill* fill) void operator()(float frameNo, Fill* fill)
{ {
if (!frames) { if (!frames) {
fill->colorStops(value.data, count); fill->colorStops(value.data, count);
@ -381,7 +381,7 @@ struct LottieColorStop
while (low <= high) { while (low <= high) {
auto mid = low + (high - low) / 2; auto mid = low + (high - low) / 2;
auto frame = frames->data + mid; auto frame = frames->data + mid;
if (frameNo == frame->no) { if (mathEqual(frameNo, frame->no)) {
fill->colorStops(frame->value.data, count); fill->colorStops(frame->value.data, count);
return; return;
} else if (frameNo > frame->no) { } else if (frameNo > frame->no) {
@ -394,7 +394,7 @@ struct LottieColorStop
//interpolate //interpolate
auto frame = frames->data + low; auto frame = frames->data + low;
auto pframe = frame - 1; auto pframe = frame - 1;
auto t = float(frameNo - pframe->no) / float(frame->no - pframe->no); auto t = (frameNo - pframe->no) / (frame->no - pframe->no);
if (pframe->interpolator) t = pframe->interpolator->progress(t); if (pframe->interpolator) t = pframe->interpolator->progress(t);
if (pframe->hold) { if (pframe->hold) {
@ -453,7 +453,7 @@ struct LottiePosition
return (*frames)[frames->count]; return (*frames)[frames->count];
} }
Point operator()(int32_t frameNo) Point operator()(float frameNo)
{ {
if (!frames) return value; if (!frames) return value;
if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value; if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value;
@ -465,7 +465,7 @@ struct LottiePosition
while (low <= high) { while (low <= high) {
auto mid = low + (high - low) / 2; auto mid = low + (high - low) / 2;
auto frame = frames->data + mid; auto frame = frames->data + mid;
if (frameNo == frame->no) return frame->value; if (mathEqual(frameNo, frame->no)) return frame->value;
else if (frameNo > frame->no) low = mid + 1; else if (frameNo > frame->no) low = mid + 1;
else high = mid - 1; else high = mid - 1;
} }
@ -474,7 +474,7 @@ struct LottiePosition
return (frame - 1)->interpolate(frame, frameNo); return (frame - 1)->interpolate(frame, frameNo);
} }
float angle(int32_t frameNo) float angle(float frameNo)
{ {
if (!frames) return 0; if (!frames) return 0;
if (frames->count == 1 || frameNo <= frames->first().no) return 0; if (frames->count == 1 || frameNo <= frames->first().no) return 0;

View file

@ -62,7 +62,7 @@ Animation::Animation() : pImpl(new Impl)
} }
Result Animation::frame(uint32_t no) noexcept Result Animation::frame(float no) noexcept
{ {
auto loader = pImpl->picture->pImpl->loader.get(); auto loader = pImpl->picture->pImpl->loader.get();
@ -80,7 +80,7 @@ Picture* Animation::picture() const noexcept
} }
uint32_t Animation::curFrame() const noexcept float Animation::curFrame() const noexcept
{ {
auto loader = pImpl->picture->pImpl->loader.get(); auto loader = pImpl->picture->pImpl->loader.get();
@ -91,7 +91,7 @@ uint32_t Animation::curFrame() const noexcept
} }
uint32_t Animation::totalFrame() const noexcept float Animation::totalFrame() const noexcept
{ {
auto loader = pImpl->picture->pImpl->loader.get(); auto loader = pImpl->picture->pImpl->loader.get();

View file

@ -33,10 +33,9 @@ class FrameModule: public LoadModule
public: public:
virtual ~FrameModule() {} virtual ~FrameModule() {}
virtual bool frame(uint32_t frameNo) = 0; //set the current frame number virtual bool frame(float no) = 0; //set the current frame number
virtual float totalFrame() = 0; //return the total frame count
virtual uint32_t totalFrame() = 0; //return the total frame count virtual float curFrame() = 0; //return the current frame number
virtual uint32_t curFrame() = 0; //return the current frame number
virtual float duration() = 0; //return the animation duration in seconds virtual float duration() = 0; //return the animation duration in seconds
virtual bool animatable() override { return true; } virtual bool animatable() override { return true; }

View file

@ -40,16 +40,16 @@ TEST_CASE("Animation Basic", "[capiAnimation]")
//Negative cases //Negative cases
REQUIRE(tvg_animation_set_frame(animation, 0) == TVG_RESULT_INSUFFICIENT_CONDITION); REQUIRE(tvg_animation_set_frame(animation, 0) == TVG_RESULT_INSUFFICIENT_CONDITION);
uint32_t frame = 0; auto frame = 0.0f;
REQUIRE(tvg_animation_set_frame(animation, frame) == TVG_RESULT_INSUFFICIENT_CONDITION); REQUIRE(tvg_animation_set_frame(animation, frame) == TVG_RESULT_INSUFFICIENT_CONDITION);
REQUIRE(tvg_animation_get_frame(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT); REQUIRE(tvg_animation_get_frame(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT);
REQUIRE(tvg_animation_get_frame(animation, &frame) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_get_frame(animation, &frame) == TVG_RESULT_SUCCESS);
REQUIRE(frame == 0); REQUIRE(frame == 0.0f);
REQUIRE(tvg_animation_get_total_frame(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT); REQUIRE(tvg_animation_get_total_frame(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT);
REQUIRE(tvg_animation_get_total_frame(animation, &frame) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_get_total_frame(animation, &frame) == TVG_RESULT_SUCCESS);
REQUIRE(frame == 0); REQUIRE(frame == 0.0f);
REQUIRE(tvg_animation_get_duration(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT); REQUIRE(tvg_animation_get_duration(animation, nullptr) == TVG_RESULT_INVALID_ARGUMENT);
float duration = 0.0f; float duration = 0.0f;
@ -79,13 +79,13 @@ TEST_CASE("Animation Lottie", "[capiAnimation]")
REQUIRE(tvg_picture_load(picture, TEST_DIR"/invalid.json") == TVG_RESULT_INVALID_ARGUMENT); REQUIRE(tvg_picture_load(picture, TEST_DIR"/invalid.json") == TVG_RESULT_INVALID_ARGUMENT);
REQUIRE(tvg_picture_load(picture, TEST_DIR"/test.json") == TVG_RESULT_SUCCESS); REQUIRE(tvg_picture_load(picture, TEST_DIR"/test.json") == TVG_RESULT_SUCCESS);
uint32_t frame; float frame;
REQUIRE(tvg_animation_get_total_frame(animation, &frame) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_get_total_frame(animation, &frame) == TVG_RESULT_SUCCESS);
REQUIRE(frame == 120); REQUIRE(frame == Approx(120).margin(004004));
REQUIRE(tvg_animation_set_frame(animation, frame - 1) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_set_frame(animation, frame - 1) == TVG_RESULT_SUCCESS);
REQUIRE(tvg_animation_get_frame(animation, &frame) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_get_frame(animation, &frame) == TVG_RESULT_SUCCESS);
REQUIRE(frame == 119); REQUIRE(frame == Approx(119).margin(004004));
float duration; float duration;
REQUIRE(tvg_animation_get_duration(animation, &duration) == TVG_RESULT_SUCCESS); REQUIRE(tvg_animation_get_duration(animation, &duration) == TVG_RESULT_SUCCESS);

View file

@ -39,9 +39,9 @@ TEST_CASE("Animation Basic", "[tvgAnimation]")
REQUIRE(picture->identifier == Picture::identifier); REQUIRE(picture->identifier == Picture::identifier);
//Negative cases //Negative cases
REQUIRE(animation->frame(0) == Result::InsufficientCondition); REQUIRE(animation->frame(0.0f) == Result::InsufficientCondition);
REQUIRE(animation->curFrame() == 0); REQUIRE(animation->curFrame() == 0.0f);
REQUIRE(animation->totalFrame() == 0); REQUIRE(animation->totalFrame() == 0.0f);
REQUIRE(animation->duration() == 0.0f); REQUIRE(animation->duration() == 0.0f);
} }
@ -60,7 +60,7 @@ TEST_CASE("Animation Lottie", "[tvgAnimation]")
REQUIRE(picture->load(TEST_DIR"/invalid.json") == Result::InvalidArguments); REQUIRE(picture->load(TEST_DIR"/invalid.json") == Result::InvalidArguments);
REQUIRE(picture->load(TEST_DIR"/test.json") == Result::Success); REQUIRE(picture->load(TEST_DIR"/test.json") == Result::Success);
REQUIRE(animation->totalFrame() == 120); REQUIRE(animation->totalFrame() == Approx(120).margin(004004));
REQUIRE(animation->curFrame() == 0); REQUIRE(animation->curFrame() == 0);
REQUIRE(animation->duration() == Approx(4).margin(004004)); REQUIRE(animation->duration() == Approx(4).margin(004004));
REQUIRE(animation->frame(20) == Result::Success); REQUIRE(animation->frame(20) == Result::Success);