lottie: support frame tweening feature

frame tweening allows user to interpolate two frames
over a speicified duration. This Tweening functionality
can be particularly powerful when combined with state-based
(a.k.a. Marker) animation playback in Lottie.

For example, apps support state machine based animations,
where the transition sequences between states are not linear
and can occur in highly irregular directions.

Experimental APIs:
- Result LottieAnimation::tween(float from, float to, float progress)
- Tvg_Result tvg_lottie_animation_tween(Tvg_Animation* animation, float from, float to, float progress)
This commit is contained in:
Hermet Park 2025-02-06 12:34:12 +09:00 committed by Hermet Park
parent 0dd0a3b45c
commit 78cfc33f4c
12 changed files with 355 additions and 150 deletions

View file

@ -2542,6 +2542,25 @@ TVG_API Tvg_Result tvg_lottie_animation_get_markers_cnt(Tvg_Animation* animation
TVG_API Tvg_Result tvg_lottie_animation_get_marker(Tvg_Animation* animation, uint32_t idx, const char** name);
/**
* @brief Interpolates between two frames over a specified duration.
*
* This method performs tweening, a process of generating intermediate frame
* between @p from and @p to based on the given @p progress.
*
* @param[in] animation The Tvg_Animation pointer to the Lottie animation object.
* @param[in] from The start frame number of the interpolation.
* @param[in] to The end frame number of the interpolation.
* @param[in] progress The current progress of the interpolation (range: 0.0 to 1.0).
*
* @return Tvg_Result enumeration.
* @retval TVG_RESULT_INSUFFICIENT_CONDITION In case the animation is not loaded.
*
* @note Experimental API
*/
TVG_API Tvg_Result tvg_lottie_animation_tween(Tvg_Animation* animation, float from, float to, float progress);
/** \} */ // end addtogroup ThorVGCapi_LottieAnimation

View file

@ -960,6 +960,16 @@ TVG_API Tvg_Result tvg_lottie_animation_get_marker(Tvg_Animation* animation, uin
return TVG_RESULT_NOT_SUPPORTED;
}
TVG_API Tvg_Result tvg_lottie_animation_tween(Tvg_Animation* animation, float from, float to, float progress)
{
#ifdef THORVG_LOTTIE_LOADER_SUPPORT
if (!animation) return TVG_RESULT_INVALID_ARGUMENT;
return (Tvg_Result) reinterpret_cast<LottieAnimation*>(animation)->tween(from, to, progress);
#endif
return TVG_RESULT_NOT_SUPPORTED;
}
#ifdef __cplusplus
}
#endif

View file

@ -51,6 +51,22 @@ public:
*/
Result segment(const char* marker) noexcept;
/**
* @brief Interpolates between two frames over a specified duration.
*
* This method performs tweening, a process of generating intermediate frame
* between @p from and @p to based on the given @p progress.
*
* @param[in] from The start frame number of the interpolation.
* @param[in] to The end frame number of the interpolation.
* @param[in] progress The current progress of the interpolation (range: 0.0 to 1.0).
*
* @retval Result::InsufficientCondition In case the animation is not loaded.
*
* @note Experimental API
*/
Result tween(float from, float to, float progress) noexcept;
/**
* @brief Gets the marker count of the animation.
*

View file

@ -33,9 +33,10 @@ LottieAnimation::LottieAnimation()
Result LottieAnimation::override(const char* slot) noexcept
{
if (!PICTURE(pImpl->picture)->loader) return Result::InsufficientCondition;
auto loader = PICTURE(pImpl->picture)->loader;
if (!loader) return Result::InsufficientCondition;
if (static_cast<LottieLoader*>(PICTURE(pImpl->picture)->loader)->override(slot)) return Result::Success;
if (static_cast<LottieLoader*>(loader)->override(slot)) return Result::Success;
return Result::InvalidArguments;
}
@ -58,6 +59,15 @@ Result LottieAnimation::segment(const char* marker) noexcept
}
Result LottieAnimation::tween(float from, float to, float progress) noexcept
{
auto loader = PICTURE(pImpl->picture)->loader;
if (!loader) return Result::InsufficientCondition;
if (!static_cast<LottieLoader*>(loader)->tween(from, to, progress)) return Result::InsufficientCondition;
return Result::Success;
}
uint32_t LottieAnimation::markersCnt() noexcept
{
auto loader = PICTURE(pImpl->picture)->loader;

View file

@ -38,13 +38,13 @@ static bool _buildComposition(LottieComposition* comp, LottieLayer* parent);
static bool _draw(LottieGroup* parent, LottieShape* shape, RenderContext* ctx);
static void _rotate(LottieTransform* transform, float frameNo, Matrix& m, float angle, LottieExpressions* exps)
static void _rotate(LottieTransform* transform, float frameNo, Matrix& m, float angle, Tween& tween, LottieExpressions* exps)
{
//rotation xyz
if (transform->rotationEx) {
auto radianX = deg2rad(transform->rotationEx->x(frameNo, exps));
auto radianY = deg2rad(transform->rotationEx->y(frameNo, exps));
auto radianZ = deg2rad(transform->rotation(frameNo, exps)) + angle;
auto radianX = deg2rad(transform->rotationEx->x(frameNo, tween, exps));
auto radianY = deg2rad(transform->rotationEx->y(frameNo, tween, exps));
auto radianZ = deg2rad(transform->rotation(frameNo, tween, exps)) + angle;
auto cx = cosf(radianX), sx = sinf(radianX);
auto cy = cosf(radianY), sy = sinf(radianY);;
auto cz = cosf(radianZ), sz = sinf(radianZ);;
@ -54,7 +54,7 @@ static void _rotate(LottieTransform* transform, float frameNo, Matrix& m, float
m.e22 = -sx * sy * sz + cx * cz;
//rotation z
} else {
auto degree = transform->rotation(frameNo, exps) + angle;
auto degree = transform->rotation(frameNo, tween, exps) + angle;
if (degree == 0.0f) return;
auto radian = deg2rad(degree);
m.e11 = cosf(radian);
@ -68,7 +68,7 @@ static void _rotate(LottieTransform* transform, float frameNo, Matrix& m, float
static void _skew(Matrix* m, float angleDeg, float axisDeg)
{
auto angle = -deg2rad(angleDeg);
float tanVal = tanf(angle);
auto tanVal = tanf(angle);
axisDeg = fmod(axisDeg, 180.0f);
if (fabsf(axisDeg) < 0.01f || fabsf(axisDeg - 180.0f) < 0.01f || fabsf(axisDeg + 180.0f) < 0.01f) {
@ -86,8 +86,8 @@ static void _skew(Matrix* m, float angleDeg, float axisDeg)
}
auto axis = -deg2rad(axisDeg);
float cosVal = cosf(axis);
float sinVal = sinf(axis);
auto cosVal = cosf(axis);
auto sinVal = sinf(axis);
auto A = sinVal * cosVal * tanVal;
auto B = cosVal * cosVal * tanVal;
auto C = sinVal * sinVal * tanVal;
@ -101,7 +101,7 @@ static void _skew(Matrix* m, float angleDeg, float axisDeg)
}
static bool _updateTransform(LottieTransform* transform, float frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity, LottieExpressions* exps)
static bool _updateTransform(LottieTransform* transform, float frameNo, Matrix& matrix, uint8_t& opacity, bool autoOrient, Tween& tween, LottieExpressions* exps)
{
identity(&matrix);
@ -110,14 +110,13 @@ static bool _updateTransform(LottieTransform* transform, float frameNo, bool aut
return false;
}
if (transform->coords) translate(&matrix, {transform->coords->x(frameNo, exps), transform->coords->y(frameNo, exps)});
else translate(&matrix, transform->position(frameNo, exps));
if (transform->coords) translate(&matrix, {transform->coords->x(frameNo, tween, exps), transform->coords->y(frameNo, tween, exps)});
else translate(&matrix, transform->position(frameNo, tween, exps));
auto angle = 0.0f;
if (autoOrient) angle = transform->position.angle(frameNo);
_rotate(transform, frameNo, matrix, angle, exps);
auto angle = (autoOrient) ? transform->position.angle(frameNo, tween) : 0.0f;
_rotate(transform, frameNo, matrix, angle, tween, exps);
auto skewAngle = transform->skewAngle(frameNo, exps);
auto skewAngle = transform->skewAngle(frameNo, tween, exps);
if (skewAngle != 0.0f) {
// For angles where tangent explodes, the shape degenerates into an infinitely thin line.
// This is handled by zeroing out the matrix due to finite numerical precision.
@ -126,15 +125,15 @@ static bool _updateTransform(LottieTransform* transform, float frameNo, bool aut
_skew(&matrix, skewAngle, transform->skewAxis(frameNo, exps));
}
auto scale = transform->scale(frameNo, exps);
auto scale = transform->scale(frameNo, tween, exps);
scaleR(&matrix, scale * 0.01f);
//Lottie specific anchor transform.
translateR(&matrix, -transform->anchor(frameNo, exps));
translateR(&matrix, -transform->anchor(frameNo, tween, exps));
//invisible just in case.
if (scale.x == 0.0f || scale.y == 0.0f) opacity = 0;
else opacity = transform->opacity(frameNo, exps);
if (tvg::zero(scale)) opacity = 0;
else opacity = transform->opacity(frameNo, tween, exps);
return true;
}
@ -142,7 +141,7 @@ static bool _updateTransform(LottieTransform* transform, float frameNo, bool aut
void LottieBuilder::updateTransform(LottieLayer* layer, float frameNo)
{
if (!layer || tvg::equal(layer->cache.frameNo, frameNo)) return;
if (!layer || (!tweening() && tvg::equal(layer->cache.frameNo, frameNo))) return;
auto transform = layer->transform;
auto parent = layer->parent;
@ -151,7 +150,7 @@ void LottieBuilder::updateTransform(LottieLayer* layer, float frameNo)
auto& matrix = layer->cache.matrix;
_updateTransform(transform, frameNo, layer->autoOrient, matrix, layer->cache.opacity, exps);
_updateTransform(transform, frameNo, matrix, layer->cache.opacity, layer->autoOrient, tween, exps);
if (parent) {
if (!identity((const Matrix*) &parent->cache.matrix)) {
@ -172,14 +171,14 @@ void LottieBuilder::updateTransform(LottieGroup* parent, LottieObject** child, f
if (parent->mergeable()) {
if (!ctx->transform) ctx->transform = (Matrix*)malloc(sizeof(Matrix));
_updateTransform(transform, frameNo, false, *ctx->transform, opacity, exps);
_updateTransform(transform, frameNo, *ctx->transform, opacity, false, tween, exps);
return;
}
ctx->merging = nullptr;
Matrix matrix;
if (!_updateTransform(transform, frameNo, false, matrix, opacity, exps)) return;
if (!_updateTransform(transform, frameNo, matrix, opacity, false, tween, exps)) return;
ctx->propagator->transform(ctx->propagator->transform() * matrix);
ctx->propagator->opacity(MULTIPLY(opacity, PAINT(ctx->propagator)->opacity));
@ -213,18 +212,18 @@ void LottieBuilder::updateGroup(LottieGroup* parent, LottieObject** child, float
}
static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ctx, LottieExpressions* exps)
static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ctx, Tween& tween, LottieExpressions* exps)
{
ctx->propagator->strokeWidth(stroke->width(frameNo, exps));
ctx->propagator->strokeWidth(stroke->width(frameNo, tween, exps));
ctx->propagator->strokeCap(stroke->cap);
ctx->propagator->strokeJoin(stroke->join);
ctx->propagator->strokeMiterlimit(stroke->miterLimit);
if (stroke->dashattr) {
float dashes[2];
dashes[0] = stroke->dashSize(frameNo, exps);
dashes[1] = dashes[0] + stroke->dashGap(frameNo, exps);
ctx->propagator->strokeDash(dashes, 2, stroke->dashOffset(frameNo, exps));
dashes[0] = stroke->dashSize(frameNo, tween, exps);
dashes[1] = dashes[0] + stroke->dashGap(frameNo, tween, exps);
ctx->propagator->strokeDash(dashes, 2, stroke->dashOffset(frameNo, tween, exps));
} else {
ctx->propagator->strokeDash(nullptr, 0);
}
@ -252,9 +251,9 @@ void LottieBuilder::updateSolidStroke(LottieGroup* parent, LottieObject** child,
auto stroke = static_cast<LottieSolidStroke*>(*child);
ctx->merging = nullptr;
auto color = stroke->color(frameNo, exps);
ctx->propagator->strokeFill(color.rgb[0], color.rgb[1], color.rgb[2], stroke->opacity(frameNo, exps));
_updateStroke(static_cast<LottieStroke*>(stroke), frameNo, ctx, exps);
auto color = stroke->color(frameNo, tween, exps);
ctx->propagator->strokeFill(color.rgb[0], color.rgb[1], color.rgb[2], stroke->opacity(frameNo, tween, exps));
_updateStroke(static_cast<LottieStroke*>(stroke), frameNo, ctx, tween, exps);
}
@ -265,8 +264,8 @@ void LottieBuilder::updateGradientStroke(LottieGroup* parent, LottieObject** chi
auto stroke = static_cast<LottieGradientStroke*>(*child);
ctx->merging = nullptr;
ctx->propagator->strokeFill(stroke->fill(frameNo, exps));
_updateStroke(static_cast<LottieStroke*>(stroke), frameNo, ctx, exps);
ctx->propagator->strokeFill(stroke->fill(frameNo, tween, exps));
_updateStroke(static_cast<LottieStroke*>(stroke), frameNo, ctx, tween, exps);
}
@ -277,8 +276,9 @@ void LottieBuilder::updateSolidFill(LottieGroup* parent, LottieObject** child, f
auto fill = static_cast<LottieSolidFill*>(*child);
ctx->merging = nullptr;
auto color = fill->color(frameNo, exps);
ctx->propagator->fill(color.rgb[0], color.rgb[1], color.rgb[2], fill->opacity(frameNo, exps));
auto color = fill->color(frameNo, tween, exps);
ctx->propagator->fill(color.rgb[0], color.rgb[1], color.rgb[2], fill->opacity(frameNo, tween, exps));
ctx->propagator->fill(fill->rule);
if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true);
@ -293,7 +293,7 @@ void LottieBuilder::updateGradientFill(LottieGroup* parent, LottieObject** child
ctx->merging = nullptr;
//TODO: reuse the fill instance?
ctx->propagator->fill(fill->fill(frameNo, exps));
ctx->propagator->fill(fill->fill(frameNo, tween, exps));
ctx->propagator->fill(fill->rule);
if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true);
@ -464,9 +464,9 @@ void LottieBuilder::appendRect(Shape* shape, Point& pos, Point& size, float r, b
void LottieBuilder::updateRect(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, RenderContext* ctx)
{
auto rect = static_cast<LottieRect*>(*child);
auto size = rect->size(frameNo, exps);
auto pos = rect->position(frameNo, exps) - size * 0.5f;
auto r = rect->radius(frameNo, exps);
auto size = rect->size(frameNo, tween, exps);
auto pos = rect->position(frameNo, tween, exps) - size * 0.5f;
auto r = rect->radius(frameNo, tween, exps);
if (r == 0.0f) {
if (ctx->roundness) ctx->roundness->modifyRect(size, r);
@ -523,8 +523,8 @@ void LottieBuilder::updateEllipse(LottieGroup* parent, LottieObject** child, flo
{
auto ellipse = static_cast<LottieEllipse*>(*child);
auto pos = ellipse->position(frameNo, exps);
auto size = ellipse->size(frameNo, exps) * 0.5f;
auto pos = ellipse->position(frameNo, tween, exps);
auto size = ellipse->size(frameNo, tween, exps) * 0.5f;
if (!ctx->repeaters.empty()) {
auto shape = ellipse->pooling();
@ -542,29 +542,29 @@ void LottieBuilder::updatePath(LottieGroup* parent, LottieObject** child, float
{
auto path = static_cast<LottiePath*>(*child);
if (!ctx->repeaters.empty()) {
auto shape = path->pooling();
shape->reset();
path->pathset(frameNo, SHAPE(shape)->rs.path, ctx->transform, exps, ctx->modifier);
_repeat(parent, shape, ctx);
} else {
if (ctx->repeaters.empty()) {
_draw(parent, path, ctx);
if (path->pathset(frameNo, SHAPE(ctx->merging)->rs.path, ctx->transform, exps, ctx->modifier)) {
if (path->pathset(frameNo, SHAPE(ctx->merging)->rs.path, ctx->transform, tween, exps, ctx->modifier)) {
PAINT(ctx->merging)->update(RenderUpdateFlag::Path);
}
} else {
auto shape = path->pooling();
shape->reset();
path->pathset(frameNo, SHAPE(shape)->rs.path, ctx->transform, tween, exps, ctx->modifier);
_repeat(parent, shape, ctx);
}
}
void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx)
void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx, Tween& tween, LottieExpressions* exps)
{
static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f;
auto ptsCnt = star->ptsCnt(frameNo, exps);
auto innerRadius = star->innerRadius(frameNo, exps);
auto outerRadius = star->outerRadius(frameNo, exps);
auto innerRoundness = star->innerRoundness(frameNo, exps) * 0.01f;
auto outerRoundness = star->outerRoundness(frameNo, exps) * 0.01f;
auto ptsCnt = star->ptsCnt(frameNo, tween, exps);
auto innerRadius = star->innerRadius(frameNo, tween, exps);
auto outerRadius = star->outerRadius(frameNo, tween, exps);
auto innerRoundness = star->innerRoundness(frameNo, tween, exps) * 0.01f;
auto outerRoundness = star->outerRoundness(frameNo, tween, exps) * 0.01f;
auto angle = deg2rad(-90.0f);
auto partialPointRadius = 0.0f;
@ -676,13 +676,13 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
}
void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx)
void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx, Tween& tween, LottieExpressions* exps)
{
static constexpr auto POLYGON_MAGIC_NUMBER = 0.25f;
auto ptsCnt = size_t(floor(star->ptsCnt(frameNo, exps)));
auto radius = star->outerRadius(frameNo, exps);
auto outerRoundness = star->outerRoundness(frameNo, exps) * 0.01f;
auto ptsCnt = size_t(floor(star->ptsCnt(frameNo, tween, exps)));
auto radius = star->outerRadius(frameNo, tween, exps);
auto outerRoundness = star->outerRoundness(frameNo, tween, exps) * 0.01f;
auto angle = deg2rad(-90.0f);
auto anglePerPoint = 2.0f * MATH_PI / float(ptsCnt);
@ -761,8 +761,8 @@ void LottieBuilder::updatePolystar(LottieGroup* parent, LottieObject** child, fl
//Optimize: Can we skip the individual coords transform?
Matrix matrix;
identity(&matrix);
translate(&matrix, star->position(frameNo, exps));
rotate(&matrix, star->rotation(frameNo, exps));
translate(&matrix, star->position(frameNo, tween, exps));
rotate(&matrix, star->rotation(frameNo, tween, exps));
if (ctx->transform) matrix = *ctx->transform * matrix;
@ -771,13 +771,13 @@ void LottieBuilder::updatePolystar(LottieGroup* parent, LottieObject** child, fl
if (!ctx->repeaters.empty()) {
auto shape = star->pooling();
shape->reset();
if (star->type == LottiePolyStar::Star) updateStar(star, frameNo, (identity ? nullptr : &matrix), shape, ctx);
else updatePolygon(parent, star, frameNo, (identity ? nullptr : &matrix), shape, ctx);
if (star->type == LottiePolyStar::Star) updateStar(star, frameNo, (identity ? nullptr : &matrix), shape, ctx, tween, exps);
else updatePolygon(parent, star, frameNo, (identity ? nullptr : &matrix), shape, ctx, tween, exps);
_repeat(parent, shape, ctx);
} else {
_draw(parent, star, ctx);
if (star->type == LottiePolyStar::Star) updateStar(star, frameNo, (identity ? nullptr : &matrix), ctx->merging, ctx);
else updatePolygon(parent, star, frameNo, (identity ? nullptr : &matrix), ctx->merging, ctx);
if (star->type == LottiePolyStar::Star) updateStar(star, frameNo, (identity ? nullptr : &matrix), ctx->merging, ctx, tween, exps);
else updatePolygon(parent, star, frameNo, (identity ? nullptr : &matrix), ctx->merging, ctx, tween, exps);
PAINT(ctx->merging)->update(RenderUpdateFlag::Path);
}
}
@ -786,7 +786,7 @@ void LottieBuilder::updatePolystar(LottieGroup* parent, LottieObject** child, fl
void LottieBuilder::updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, RenderContext* ctx)
{
auto roundedCorner = static_cast<LottieRoundedCorner*>(*child);
auto r = roundedCorner->radius(frameNo, exps);
auto r = roundedCorner->radius(frameNo, tween, exps);
if (r < LottieRoundnessModifier::ROUNDNESS_EPSILON) return;
if (!ctx->roundness) ctx->roundness = new LottieRoundnessModifier(&buffer, r);
@ -799,7 +799,7 @@ void LottieBuilder::updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieOb
void LottieBuilder::updateOffsetPath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist<RenderContext>& contexts, RenderContext* ctx)
{
auto offset = static_cast<LottieOffsetPath*>(*child);
if (!ctx->offset) ctx->offset = new LottieOffsetModifier(offset->offset(frameNo, exps), offset->miterLimit(frameNo, exps), offset->join);
if (!ctx->offset) ctx->offset = new LottieOffsetModifier(offset->offset(frameNo, tween, exps), offset->miterLimit(frameNo, tween, exps), offset->join);
ctx->update(ctx->offset);
}
@ -810,15 +810,15 @@ void LottieBuilder::updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject*
auto repeater = static_cast<LottieRepeater*>(*child);
RenderRepeater r;
r.cnt = static_cast<int>(repeater->copies(frameNo, exps));
r.cnt = static_cast<int>(repeater->copies(frameNo, tween, exps));
r.transform = ctx->propagator->transform();
r.offset = repeater->offset(frameNo, exps);
r.position = repeater->position(frameNo, exps);
r.anchor = repeater->anchor(frameNo, exps);
r.scale = repeater->scale(frameNo, exps);
r.rotation = repeater->rotation(frameNo, exps);
r.startOpacity = repeater->startOpacity(frameNo, exps);
r.endOpacity = repeater->endOpacity(frameNo, exps);
r.offset = repeater->offset(frameNo, tween, exps);
r.position = repeater->position(frameNo, tween, exps);
r.anchor = repeater->anchor(frameNo, tween, exps);
r.scale = repeater->scale(frameNo, tween, exps);
r.rotation = repeater->rotation(frameNo, tween, exps);
r.startOpacity = repeater->startOpacity(frameNo, tween, exps);
r.endOpacity = repeater->endOpacity(frameNo, tween, exps);
r.inorder = repeater->inorder;
r.interpOpacity = (r.startOpacity == r.endOpacity) ? false : true;
ctx->repeaters.push(r);
@ -832,7 +832,7 @@ void LottieBuilder::updateTrimpath(TVG_UNUSED LottieGroup* parent, LottieObject*
auto trimpath = static_cast<LottieTrimpath*>(*child);
float begin, end;
trimpath->segment(frameNo, begin, end, exps);
trimpath->segment(frameNo, begin, end, tween, exps);
if (SHAPE(ctx->propagator)->rs.stroke) {
auto pbegin = SHAPE(ctx->propagator)->rs.stroke->trim.begin;
@ -940,6 +940,18 @@ void LottieBuilder::updatePrecomp(LottieComposition* comp, LottieLayer* precomp,
}
void LottieBuilder::updatePrecomp(LottieComposition* comp, LottieLayer* precomp, float frameNo, Tween& tween)
{
//record & recover the tweening frame number before remapping
auto record = tween.frameNo;
tween.frameNo = precomp->remap(comp, record, exps);
updatePrecomp(comp, precomp, frameNo);
tween.frameNo = record;
}
void LottieBuilder::updateSolid(LottieLayer* layer)
{
auto solidFill = layer->statical.pooling(true);
@ -1056,7 +1068,7 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
ARRAY_FOREACH(p, glyph->children) {
auto group = static_cast<LottieGroup*>(*p);
ARRAY_FOREACH(p, group->children) {
if (static_cast<LottiePath*>(*p)->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, exps)) {
if (static_cast<LottiePath*>(*p)->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, tween, exps)) {
PAINT(shape)->update(RenderUpdateFlag::Path);
}
}
@ -1095,40 +1107,39 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
if (tvg::zero(f)) continue;
needGroup = true;
translation = translation + f * range->style.position(frameNo, exps);
scaling = scaling * (f * (range->style.scale(frameNo, exps) * 0.01f - Point{1.0f, 1.0f}) + Point{1.0f, 1.0f});
rotation += f * range->style.rotation(frameNo, exps);
translation = translation + f * range->style.position(frameNo, tween, exps);
scaling = scaling * (f * (range->style.scale(frameNo, tween, exps) * 0.01f - Point{1.0f, 1.0f}) + Point{1.0f, 1.0f});
rotation += f * range->style.rotation(frameNo, tween, exps);
opacity = (uint8_t)(opacity - f * (opacity - range->style.opacity(frameNo, exps)));
opacity = (uint8_t)(opacity - f * (opacity - range->style.opacity(frameNo, tween, exps)));
shape->opacity(opacity);
auto rangeColor = range->style.fillColor(frameNo, exps); //TODO: use flag to check whether it was really set
auto rangeColor = range->style.fillColor(frameNo, tween, exps); //TODO: use flag to check whether it was really set
if (tvg::equal(f, 1.0f)) color = rangeColor;
else {
color.rgb[0] = lerp<uint8_t>(color.rgb[0], rangeColor.rgb[0], f);
color.rgb[1] = lerp<uint8_t>(color.rgb[1], rangeColor.rgb[1], f);
color.rgb[2] = lerp<uint8_t>(color.rgb[2], rangeColor.rgb[2], f);
}
fillOpacity = (uint8_t)(fillOpacity - f * (fillOpacity - range->style.fillOpacity(frameNo, exps)));
fillOpacity = (uint8_t)(fillOpacity - f * (fillOpacity - range->style.fillOpacity(frameNo, tween, exps)));
shape->fill(color.rgb[0], color.rgb[1], color.rgb[2], fillOpacity);
shape->strokeWidth(f * range->style.strokeWidth(frameNo, exps) / scale);
shape->strokeWidth(f * range->style.strokeWidth(frameNo, tween, exps) / scale);
if (shape->strokeWidth() > 0.0f) {
auto rangeColor = range->style.strokeColor(frameNo, exps); //TODO: use flag to check whether it was really set
auto rangeColor = range->style.strokeColor(frameNo, tween, exps); //TODO: use flag to check whether it was really set
if (tvg::equal(f, 1.0f)) strokeColor = rangeColor;
else {
strokeColor.rgb[0] = lerp<uint8_t>(strokeColor.rgb[0], rangeColor.rgb[0], f);
strokeColor.rgb[1] = lerp<uint8_t>(strokeColor.rgb[1], rangeColor.rgb[1], f);
strokeColor.rgb[2] = lerp<uint8_t>(strokeColor.rgb[2], rangeColor.rgb[2], f);
}
strokeOpacity = (uint8_t)(strokeOpacity - f * (strokeOpacity - range->style.strokeOpacity(frameNo, exps)));
strokeOpacity = (uint8_t)(strokeOpacity - f * (strokeOpacity - range->style.strokeOpacity(frameNo, tween, exps)));
shape->strokeFill(strokeColor.rgb[0], strokeColor.rgb[1], strokeColor.rgb[2], strokeOpacity);
shape->order(doc.stroke.below);
}
cursor.x += f * range->style.letterSpacing(frameNo, tween, exps);
cursor.x += f * range->style.letterSpacing(frameNo, exps);
auto spacing = f * range->style.lineSpacing(frameNo, exps);
auto spacing = f * range->style.lineSpacing(frameNo, tween, exps);
if (spacing > lineSpacing) lineSpacing = spacing;
}
@ -1137,7 +1148,7 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
identity(&textGroupMatrix);
translate(&textGroupMatrix, cursor);
auto alignment = text->alignOption.anchor(frameNo, exps);
auto alignment = text->alignOption.anchor(frameNo, tween, exps);
// center pivoting
textGroupMatrix.e13 += alignment.x;
@ -1246,12 +1257,12 @@ void LottieBuilder::updateMasks(LottieLayer* layer, float frameNo)
//Default Masking
if (expand == 0.0f) {
mask->pathset(frameNo, SHAPE(pShape)->rs.path, nullptr, exps);
mask->pathset(frameNo, SHAPE(pShape)->rs.path, nullptr, tween, exps);
//Masking with Expansion (Offset)
} else {
//TODO: Once path direction support is implemented, ensure that the direction is ignored here
auto offset = LottieOffsetModifier(expand);
mask->pathset(frameNo, SHAPE(pShape)->rs.path, nullptr, exps, &offset);
mask->pathset(frameNo, SHAPE(pShape)->rs.path, nullptr, tween, exps, &offset);
}
if (fastTrack) return;
@ -1291,15 +1302,13 @@ void LottieBuilder::updateStrokeEffect(LottieLayer* layer, LottieFxStroke* effec
//FIXME: all mask
if (effect->allMask(frameNo)) {
ARRAY_FOREACH(p, layer->masks) {
auto mask = *p;
mask->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, exps);
(*p)->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, tween, exps);
}
//A specific mask
} else {
auto idx = static_cast<uint32_t>(effect->mask(frameNo) - 1);
if (idx < 0 || idx >= layer->masks.count) return;
auto mask = layer->masks[idx];
mask->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, exps);
layer->masks[idx]->pathset(frameNo, SHAPE(shape)->rs.path, nullptr, tween, exps);
}
shape->transform(layer->cache.matrix);
@ -1414,7 +1423,8 @@ void LottieBuilder::updateLayer(LottieComposition* comp, Scene* scene, LottieLay
switch (layer->type) {
case LottieLayer::Precomp: {
updatePrecomp(comp, layer, frameNo);
if (!tweening()) updatePrecomp(comp, layer, frameNo);
else updatePrecomp(comp, layer, frameNo, tween);
break;
}
case LottieLayer::Solid: {
@ -1558,9 +1568,13 @@ bool LottieBuilder::update(LottieComposition* comp, float frameNo)
{
if (comp->root->children.empty()) return false;
frameNo += comp->root->inFrame;
if (frameNo <comp->root->inFrame) frameNo = comp->root->inFrame;
if (frameNo >= comp->root->outFrame) frameNo = (comp->root->outFrame - 1);
comp->clamp(frameNo);
if (tweening()) {
comp->clamp(tween.frameNo);
//tweening is not necessary.
if (equal(frameNo, tween.frameNo)) offTween();
}
//update children layers
auto root = comp->root;

View file

@ -116,6 +116,23 @@ struct LottieBuilder
return exps ? true : false;
}
void offTween()
{
if (tween.active) tween.active = false;
}
void onTween(float to, float progress)
{
tween.frameNo = to;
tween.progress = progress;
tween.active = true;
}
bool tweening()
{
return tween.active;
}
bool update(LottieComposition* comp, float progress);
void build(LottieComposition* comp);
@ -128,6 +145,7 @@ private:
void updateLayer(LottieComposition* comp, Scene* scene, LottieLayer* layer, float frameNo);
bool updateMatte(LottieComposition* comp, float frameNo, Scene* scene, LottieLayer* layer);
void updatePrecomp(LottieComposition* comp, LottieLayer* precomp, float frameNo);
void updatePrecomp(LottieComposition* comp, LottieLayer* precomp, float frameNo, Tween& tween);
void updateSolid(LottieLayer* layer);
void updateImage(LottieGroup* layer);
void updateText(LottieLayer* layer, float frameNo);
@ -144,8 +162,8 @@ private:
void updateEllipse(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updatePath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updatePolystar(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updateStar(LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx);
void updatePolygon(LottieGroup* parent, LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx);
void updateStar(LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx, Tween& tween, LottieExpressions* exps);
void updatePolygon(LottieGroup* parent, LottiePolyStar* star, float frameNo, Matrix* transform, Shape* merging, RenderContext* ctx, Tween& tween, LottieExpressions* exps);
void updateTrimpath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updateRepeater(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
void updateRoundedCorner(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
@ -153,6 +171,7 @@ private:
RenderPath buffer; //resusable path
LottieExpressions* exps;
Tween tween;
};
#endif //_TVG_LOTTIE_BUILDER_H

View file

@ -72,6 +72,14 @@ struct TextDocument
};
struct Tween
{
float frameNo = 0.0f;
float progress = 0.0f; //greater than 0 and smaller than 1
bool active = false;
};
static inline int32_t REMAP255(float val)
{
return (int32_t)nearbyintf(val * 255.0f);

View file

@ -127,7 +127,7 @@ bool LottieLoader::header()
++p;
continue;
}
//version.
//version
if (!strncmp(p, "\"v\":", 4)) {
p += 4;
continue;
@ -329,21 +329,25 @@ bool LottieLoader::override(const char* slots, bool byDefault)
}
float LottieLoader::shorten(float frameNo)
{
//This ensures that the target frame number is reached.
return nearbyintf((frameNo + startFrame()) * 10000.0f) * 0.0001f;
}
bool LottieLoader::frame(float no)
{
auto frameNo = no + startFrame();
//This ensures that the target frame number is reached.
frameNo *= 10000.0f;
frameNo = nearbyintf(frameNo);
frameNo *= 0.0001f;
no = shorten(no);
//Skip update if frame diff is too small.
if (fabsf(this->frameNo - frameNo) <= 0.0009f) return false;
if (!builder->tweening() && fabsf(this->frameNo - no) <= 0.0009f) return false;
this->done();
this->frameNo = frameNo;
this->frameNo = no;
builder->offTween();
TaskScheduler::request(this);
@ -436,3 +440,21 @@ bool LottieLoader::ready()
if (comp) return true;
return false;
}
bool LottieLoader::tween(float from, float to, float progress)
{
//tweening is not necessary
if (tvg::zero(progress)) return frame(from);
else if (tvg::equal(progress, 1.0f)) return frame(to);
done();
frameNo = shorten(from);
builder->onTween(shorten(to), progress);
TaskScheduler::request(this);
return true;
}

View file

@ -45,7 +45,7 @@ public:
Key key;
char* dirName = nullptr; //base resource directory
bool copy = false; //"content" is owned by this loader
bool overridden = false; //overridden properties with slots
bool overridden = false; //overridden properties with slots
bool rebuild = false; //require building the lottie scene
LottieLoader();
@ -71,6 +71,9 @@ public:
bool segment(const char* marker, float& begin, float& end);
Result segment(float begin, float end) override;
float shorten(float frameNo); //Reduce the accuracy for performance
bool tween(float from, float to, float progress);
private:
bool ready();
bool header();

View file

@ -225,12 +225,10 @@ void LottieImage::update()
}
void LottieTrimpath::segment(float frameNo, float& start, float& end, LottieExpressions* exps)
void LottieTrimpath::segment(float frameNo, float& start, float& end, Tween& tween, LottieExpressions* exps)
{
start = tvg::clamp(this->start(frameNo, exps) * 0.01f, 0.0f, 1.0f);
end = tvg::clamp(this->end(frameNo, exps) * 0.01f, 0.0f, 1.0f);
auto o = fmodf(this->offset(frameNo, exps), 360.0f) / 360.0f; //0 ~ 1
start = tvg::clamp(this->start(frameNo, tween, exps) * 0.01f, 0.0f, 1.0f);
end = tvg::clamp(this->end(frameNo, tween, exps) * 0.01f, 0.0f, 1.0f);
auto diff = fabs(start - end);
if (tvg::zero(diff)) {
@ -245,6 +243,8 @@ void LottieTrimpath::segment(float frameNo, float& start, float& end, LottieExpr
}
if (start > end) std::swap(start, end);
auto o = fmodf(this->offset(frameNo, tween, exps), 360.0f) / 360.0f; //0 ~ 1
start += o;
end += o;
}
@ -337,14 +337,14 @@ uint32_t LottieGradient::populate(ColorStop& color, size_t count)
}
Fill* LottieGradient::fill(float frameNo, LottieExpressions* exps)
Fill* LottieGradient::fill(float frameNo, Tween& tween, LottieExpressions* exps)
{
auto opacity = this->opacity(frameNo);
auto opacity = this->opacity(frameNo, tween, exps);
if (opacity == 0) return nullptr;
Fill* fill = nullptr;
auto s = start(frameNo, exps);
auto e = end(frameNo, exps);
auto s = start(frameNo, tween, exps);
auto e = end(frameNo, tween, exps);
//Linear Graident
if (id == 1) {
@ -358,14 +358,14 @@ Fill* LottieGradient::fill(float frameNo, LottieExpressions* exps)
auto w = fabsf(e.x - s.x);
auto h = fabsf(e.y - s.y);
auto r = (w > h) ? (w + 0.375f * h) : (h + 0.375f * w);
auto progress = this->height(frameNo, exps) * 0.01f;
auto progress = this->height(frameNo, tween, exps) * 0.01f;
if (tvg::zero(progress)) {
static_cast<RadialGradient*>(fill)->radial(s.x, s.y, r, s.x, s.y, 0.0f);
} else {
if (tvg::equal(progress, 1.0f)) progress = 0.99f;
auto startAngle = rad2deg(tvg::atan2(e.y - s.y, e.x - s.x));
auto angle = deg2rad((startAngle + this->angle(frameNo, exps)));
auto angle = deg2rad((startAngle + this->angle(frameNo, tween, exps)));
auto fx = s.x + cos(angle) * progress * r;
auto fy = s.y + sin(angle) * progress * r;
// Lottie doesn't have any focal radius concept
@ -375,7 +375,7 @@ Fill* LottieGradient::fill(float frameNo, LottieExpressions* exps)
if (!fill) return nullptr;
colorStops(frameNo, fill, exps);
colorStops(frameNo, fill, tween, exps);
//multiply the current opacity with the fill
if (opacity < 255) {

View file

@ -52,21 +52,20 @@ struct LottieStroke
return dashattr->value[no];
}
float dashOffset(float frameNo, LottieExpressions* exps)
float dashOffset(float frameNo, Tween& tween, LottieExpressions* exps)
{
return dash(0)(frameNo, exps);
return dash(0)(frameNo, tween, exps);
}
float dashGap(float frameNo, LottieExpressions* exps)
float dashSize(float frameNo, Tween& tween, LottieExpressions* exps)
{
return dash(2)(frameNo, exps);
auto d = dash(1)(frameNo, tween, exps);
return (d > 0.0f) ? d : 0.0f;
}
float dashSize(float frameNo, LottieExpressions* exps)
float dashGap(float frameNo, Tween& tween, LottieExpressions* exps)
{
auto d = dash(1)(frameNo, exps);
if (d == 0.0f) return 0.1f;
else return d;
return dash(2)(frameNo, tween, exps);
}
LottieFloat width = 0.0f;
@ -382,7 +381,7 @@ struct LottieTrimpath : LottieObject
return nullptr;
}
void segment(float frameNo, float& start, float& end, LottieExpressions* exps);
void segment(float frameNo, float& start, float& end, Tween& tween, LottieExpressions* exps);
LottieFloat start = 0.0f;
LottieFloat end = 100.0f;
@ -690,7 +689,7 @@ struct LottieGradient : LottieObject
}
uint32_t populate(ColorStop& color, size_t count);
Fill* fill(float frameNo, LottieExpressions* exps);
Fill* fill(float frameNo, Tween& tween, LottieExpressions* exps);
LottieScalar start = Point{0.0f, 0.0f};
LottieScalar end = Point{0.0f, 0.0f};
@ -962,6 +961,13 @@ struct LottieComposition
return nullptr;
}
void clamp(float& frameNo)
{
frameNo += root->inFrame;
if (frameNo < root->inFrame) frameNo = root->inFrame;
if (frameNo >= root->outFrame) frameNo = root->outFrame - 1;
}
LottieLayer* root = nullptr;
char* version = nullptr;
char* name = nullptr;

View file

@ -36,6 +36,10 @@ struct LottieLayer;
struct LottieObject;
//default keyframe updates condition (no tweening)
#define DEFAULT_COND (!tween.active || !frames || (frames->count == 1))
template<typename T>
struct LottieScalarFrame
{
@ -159,32 +163,32 @@ struct LottieExpression
};
static void _copy(PathSet* pathset, Array<Point>& outPts, Matrix* transform)
static void _copy(PathSet* pathset, Array<Point>& out, Matrix* transform)
{
Array<Point> inPts;
Array<Point> in;
if (transform) {
for (int i = 0; i < pathset->ptsCnt; ++i) {
Point pt = pathset->pts[i];
pt *= *transform;
outPts.push(pt);
out.push(pt);
}
} else {
inPts.data = pathset->pts;
inPts.count = pathset->ptsCnt;
outPts.push(inPts);
inPts.data = nullptr;
in.data = pathset->pts;
in.count = pathset->ptsCnt;
out.push(in);
in.data = nullptr;
}
}
static void _copy(PathSet* pathset, Array<PathCommand>& outCmds)
static void _copy(PathSet* pathset, Array<PathCommand>& out)
{
Array<PathCommand> inCmds;
inCmds.data = pathset->cmds;
inCmds.count = pathset->cmdsCnt;
outCmds.push(inCmds);
inCmds.data = nullptr;
Array<PathCommand> in;
in.data = pathset->cmds;
in.count = pathset->cmdsCnt;
out.push(in);
in.data = nullptr;
}
@ -343,6 +347,12 @@ struct LottieGenericProperty : LottieProperty
return frame->interpolate(frame + 1, frameNo);
}
Value operator()(float frameNo, Tween& tween, LottieExpressions* exps)
{
if (DEFAULT_COND) return operator()(frameNo, exps);
return lerp(operator()(frameNo, exps), operator()(tween.frameNo, exps), tween.progress);
}
void copy(const LottieGenericProperty<Frame, Value, Scalar>& rhs, bool shallow = true)
{
if (rhs.frames) {
@ -370,6 +380,12 @@ struct LottieGenericProperty : LottieProperty
return frame->angle(frame + 1, frameNo);
}
float angle(float frameNo, Tween& tween)
{
if (DEFAULT_COND) return angle(frameNo);
return lerp(angle(frameNo), angle(tween.frameNo), tween.progress);
}
void prepare()
{
if (Scalar) return;
@ -456,7 +472,6 @@ struct LottiePathSet : LottieProperty
if (tvg::equal(frame->no, frameNo)) path = &frame->value;
else if (frame->value.ptsCnt != (frame + 1)->value.ptsCnt) {
path = &frame->value;
TVGLOG("LOTTIE", "Different numbers of points in consecutive frames - interpolation omitted.");
} else {
t = (frameNo - frame->no) / ((frame + 1)->no - frame->no);
if (frame->interpolator) t = frame->interpolator->progress(t);
@ -524,6 +539,27 @@ struct LottiePathSet : LottieProperty
return true;
}
bool tweening(float frameNo, RenderPath& out, Matrix* transform, LottieModifier* modifier, Tween& tween, LottieExpressions* exps)
{
RenderPath to; //used as temp as well.
auto pivot = out.pts.count;
if (!operator()(frameNo, out, transform, exps)) return false;
if (!operator()(tween.frameNo, to, transform, exps)) return false;
auto from = out.pts.data + pivot;
if (to.pts.count != out.pts.count - pivot) TVGLOG("LOTTIE", "Tweening has different numbers of points in consecutive frames.");
for (uint32_t i = 0; i < std::min(to.pts.count, (out.pts.count - pivot)); ++i) {
from[i] = lerp(from[i], to.pts[i], tween.progress);
}
if (!modifier) return true;
//Apply modifiers
to.clear();
return modifier->modifyPath(to.cmds.data, to.cmds.count, to.pts.data, to.pts.count, transform, out);
}
bool operator()(float frameNo, RenderPath& out, Matrix* transform, LottieExpressions* exps, LottieModifier* modifier = nullptr)
{
//overriding with expressions
@ -536,7 +572,11 @@ struct LottiePathSet : LottieProperty
else return defaultPath(frameNo, out, transform);
}
void prepare() {}
bool operator()(float frameNo, RenderPath& out, Matrix* transform, Tween& tween, LottieExpressions* exps, LottieModifier* modifier = nullptr)
{
if (DEFAULT_COND) return operator()(frameNo, out, transform, exps, modifier);
return tweening(frameNo, out, transform, modifier, tween, exps);
}
};
@ -616,6 +656,38 @@ struct LottieColorStop : LottieProperty
return (*frames)[frames->count];
}
Result tweening(float frameNo, Fill* fill, Tween& tween, LottieExpressions* exps)
{
auto frame = frames->data + _bsearch(frames, frameNo);
if (tvg::equal(frame->no, frameNo)) return fill->colorStops(frame->value.data, count);
//from
operator()(frameNo, fill, exps);
//to
auto dup = fill->duplicate();
operator()(tween.frameNo, dup, exps);
//interpolate
const Fill::ColorStop* from;
auto fromCnt = fill->colorStops(&from);
const Fill::ColorStop* to;
auto toCnt = fill->colorStops(&to);
if (fromCnt != toCnt) TVGLOG("LOTTIE", "Tweening has different numbers of color data in consecutive frames.");
for (uint32_t i = 0; i < std::min(fromCnt, toCnt); ++i) {
const_cast<Fill::ColorStop*>(from)->offset = lerp(from->offset, to->offset, tween.progress);
const_cast<Fill::ColorStop*>(from)->r = lerp(from->r, to->r, tween.progress);
const_cast<Fill::ColorStop*>(from)->g = lerp(from->g, to->g, tween.progress);
const_cast<Fill::ColorStop*>(from)->b = lerp(from->b, to->b, tween.progress);
const_cast<Fill::ColorStop*>(from)->a = lerp(from->a, to->a, tween.progress);
}
return Result::Success;
}
Result operator()(float frameNo, Fill* fill, LottieExpressions* exps = nullptr)
{
//overriding with expressions
@ -660,6 +732,12 @@ struct LottieColorStop : LottieProperty
return fill->colorStops(result.data, count);
}
Result operator()(float frameNo, Fill* fill, Tween& tween, LottieExpressions* exps)
{
if (DEFAULT_COND) return operator()(frameNo, fill, exps);
return tweening(frameNo, fill, tween, exps);
}
void copy(const LottieColorStop& rhs, bool shallow = true)
{
if (rhs.frames) {