/* * Copyright (c) 2023 - 2024 the ThorVG project. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include "tvgCommon.h" #include "tvgMath.h" #include "tvgPaint.h" #include "tvgShape.h" #include "tvgInlist.h" #include "tvgLottieModel.h" #include "tvgLottieBuilder.h" #include "tvgLottieExpressions.h" /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ struct RenderRepeater { int cnt; Matrix transform; float offset; Point position; Point anchor; Point scale; float rotation; uint8_t startOpacity; uint8_t endOpacity; bool interpOpacity; bool inorder; }; struct RenderContext { INLIST_ITEM(RenderContext); Shape* propagator = nullptr; //for propagating the shape properties excluding paths Shape* merging = nullptr; //merging shapes if possible (if shapes have same properties) LottieObject** begin = nullptr; //iteration entry point Array repeaters; Matrix* transform = nullptr; float roundness = 0.0f; bool fragmenting = false; //render context has been fragmented by filling bool reqFragment = false; //requirment to fragment the render context bool ownPropagator = true; //this rendering context shares the propergator RenderContext() { propagator = Shape::gen().release(); } ~RenderContext() { if (ownPropagator) delete(propagator); free(transform); } RenderContext(const RenderContext& rhs, bool mergeable = false) { if (mergeable) { this->ownPropagator = false; propagator = rhs.propagator; merging = rhs.merging; } else { propagator = static_cast(rhs.propagator->duplicate()); } for (auto repeater = rhs.repeaters.begin(); repeater < rhs.repeaters.end(); ++repeater) { repeaters.push(*repeater); } roundness = rhs.roundness; } }; static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts, LottieExpressions* exps); static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps); static bool _buildComposition(LottieComposition* comp, LottieLayer* parent); static bool _draw(LottieGroup* parent, LottieShape* shape, RenderContext* ctx); static void _rotationXYZ(Matrix* m, float degreeX, float degreeY, float degreeZ) { auto radianX = mathDeg2Rad(degreeX); auto radianY = mathDeg2Rad(degreeY); auto radianZ = mathDeg2Rad(degreeZ); auto cx = cosf(radianX), sx = sinf(radianX); auto cy = cosf(radianY), sy = sinf(radianY);; auto cz = cosf(radianZ), sz = sinf(radianZ);; m->e11 = cy * cz; m->e12 = -cy * sz; m->e21 = sx * sy * cz + cx * sz; m->e22 = -sx * sy * sz + cx * cz; } static void _rotationZ(Matrix* m, float degree) { if (degree == 0.0f) return; auto radian = mathDeg2Rad(degree); m->e11 = cosf(radian); m->e12 = -sinf(radian); m->e21 = sinf(radian); m->e22 = cosf(radian); } static void _skew(Matrix* m, float angleDeg, float axisDeg) { auto angle = -mathDeg2Rad(angleDeg); float 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) { float cosVal = cosf(mathDeg2Rad(axisDeg)); auto B = cosVal * cosVal * tanVal; m->e12 += B * m->e11; m->e22 += B * m->e21; return; } else if (fabsf(axisDeg - 90.0f) < 0.01f || fabsf(axisDeg + 90.0f) < 0.01f) { float sinVal = -sinf(mathDeg2Rad(axisDeg)); auto C = sinVal * sinVal * tanVal; m->e11 -= C * m->e12; m->e21 -= C * m->e22; return; } auto axis = -mathDeg2Rad(axisDeg); float cosVal = cosf(axis); float sinVal = sinf(axis); auto A = sinVal * cosVal * tanVal; auto B = cosVal * cosVal * tanVal; auto C = sinVal * sinVal * tanVal; auto e11 = m->e11; auto e21 = m->e21; m->e11 = (1.0f - A) * e11 - C * m->e12; m->e12 = B * e11 + (1.0f + A) * m->e12; m->e21 = (1.0f - A) * e21 - C * m->e22; m->e22 = B * e21 + (1.0f + A) * m->e22; } static bool _updateTransform(LottieTransform* transform, float frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity, LottieExpressions* exps) { mathIdentity(&matrix); if (!transform) { opacity = 255; return false; } if (transform->coords) { mathTranslate(&matrix, transform->coords->x(frameNo), transform->coords->y(frameNo)); } else { auto position = transform->position(frameNo, exps); mathTranslate(&matrix, position.x, position.y); } auto angle = 0.0f; if (autoOrient) angle = transform->position.angle(frameNo); if (transform->rotationEx) _rotationXYZ(&matrix, transform->rotationEx->x(frameNo, exps), transform->rotationEx->y(frameNo, exps), transform->rotation(frameNo, exps) + angle); else _rotationZ(&matrix, transform->rotation(frameNo, exps) + angle); auto skewAngle = transform->skewAngle(frameNo, 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. skewAngle = fmod(skewAngle, 180.0f); if (fabsf(skewAngle - 90.0f) < 0.01f || fabsf(skewAngle + 90.0f) < 0.01f) return false; _skew(&matrix, skewAngle, transform->skewAxis(frameNo, exps)); } auto scale = transform->scale(frameNo, exps); mathScaleR(&matrix, scale.x * 0.01f, scale.y * 0.01f); //Lottie specific anchor transform. auto anchor = transform->anchor(frameNo, exps); mathTranslateR(&matrix, -anchor.x, -anchor.y); //invisible just in case. if (scale.x == 0.0f || scale.y == 0.0f) opacity = 0; else opacity = transform->opacity(frameNo, exps); return true; } static void _updateTransform(LottieLayer* layer, float frameNo, LottieExpressions* exps) { if (!layer || mathEqual(layer->cache.frameNo, frameNo)) return; auto transform = layer->transform; auto parent = layer->parent; if (parent) _updateTransform(parent, frameNo, exps); auto& matrix = layer->cache.matrix; _updateTransform(transform, frameNo, layer->autoOrient, matrix, layer->cache.opacity, exps); if (parent) { if (!mathIdentity((const Matrix*) &parent->cache.matrix)) { if (mathIdentity((const Matrix*) &matrix)) layer->cache.matrix = parent->cache.matrix; else layer->cache.matrix = parent->cache.matrix * matrix; } } layer->cache.frameNo = frameNo; } static void _updateTransform(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto transform = static_cast(*child); if (!transform) return; uint8_t opacity; if (parent->mergeable()) { if (!ctx->transform) ctx->transform = (Matrix*)malloc(sizeof(Matrix)); _updateTransform(transform, frameNo, false, *ctx->transform, opacity, exps); return; } ctx->merging = nullptr; Matrix matrix; if (!_updateTransform(transform, frameNo, false, matrix, opacity, exps)) return; auto pmatrix = PP(ctx->propagator)->transform(); ctx->propagator->transform(pmatrix ? (*pmatrix * matrix) : matrix); ctx->propagator->opacity(MULTIPLY(opacity, PP(ctx->propagator)->opacity)); //FIXME: preserve the stroke width. too workaround, need a better design. if (P(ctx->propagator)->rs.strokeWidth() > 0.0f) { auto denominator = sqrtf(matrix.e11 * matrix.e11 + matrix.e12 * matrix.e12); if (denominator > 1.0f) ctx->propagator->stroke(ctx->propagator->strokeWidth() / denominator); } } static void _updateGroup(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& pcontexts, RenderContext* ctx, LottieExpressions* exps) { auto group = static_cast(*child); if (group->children.empty()) return; //Prepare render data group->scene = parent->scene; group->reqFragment |= ctx->reqFragment; //generate a merging shape to consolidate partial shapes into a single entity if (group->mergeable()) _draw(parent, nullptr, ctx); Inlist contexts; contexts.back(new RenderContext(*ctx, group->mergeable())); _updateChildren(group, frameNo, contexts, exps); contexts.free(); } static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ctx, LottieExpressions* exps) { ctx->propagator->stroke(stroke->width(frameNo, exps)); ctx->propagator->stroke(stroke->cap); ctx->propagator->stroke(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); P(ctx->propagator)->strokeDash(dashes, 2, stroke->dashOffset(frameNo, exps)); } else { ctx->propagator->stroke(nullptr, 0); } } static bool _fragmented(LottieObject** child, Inlist& contexts, RenderContext* ctx) { if (!ctx->reqFragment) return false; if (ctx->fragmenting) return true; contexts.back(new RenderContext(*ctx)); auto fragment = contexts.tail; fragment->begin = child - 1; ctx->fragmenting = true; return false; } static void _updateSolidStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto stroke = static_cast(*child); ctx->merging = nullptr; auto color = stroke->color(frameNo, exps); ctx->propagator->stroke(color.rgb[0], color.rgb[1], color.rgb[2], stroke->opacity(frameNo, exps)); _updateStroke(static_cast(stroke), frameNo, ctx, exps); } static void _updateGradientStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto stroke = static_cast(*child); ctx->merging = nullptr; ctx->propagator->stroke(unique_ptr(stroke->fill(frameNo, exps))); _updateStroke(static_cast(stroke), frameNo, ctx, exps); } static void _updateSolidFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto fill = static_cast(*child); ctx->merging = nullptr; auto color = fill->color(frameNo); ctx->propagator->fill(color.rgb[0], color.rgb[1], color.rgb[2], fill->opacity(frameNo, exps)); ctx->propagator->fill(fill->rule); if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true); } static void _updateGradientFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto fill = static_cast(*child); ctx->merging = nullptr; //TODO: reuse the fill instance? ctx->propagator->fill(unique_ptr(fill->fill(frameNo, exps))); ctx->propagator->fill(fill->rule); ctx->propagator->opacity(MULTIPLY(fill->opacity(frameNo), PP(ctx->propagator)->opacity)); if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true); } static bool _draw(LottieGroup* parent, LottieShape* shape, RenderContext* ctx) { if (ctx->merging) return false; if (shape) { ctx->merging = shape->pooling(); PP(ctx->propagator)->duplicate(ctx->merging); } else { ctx->merging = static_cast(ctx->propagator->duplicate()); } parent->scene->push(cast(ctx->merging)); return true; } static void _repeat(LottieGroup* parent, Shape* path, RenderContext* ctx) { Array propagators; propagators.push(ctx->propagator); Array shapes; for (auto repeater = ctx->repeaters.end() - 1; repeater >= ctx->repeaters.begin(); --repeater) { shapes.reserve(repeater->cnt); for (int i = 0; i < repeater->cnt; ++i) { auto multiplier = repeater->offset + static_cast(i); for (auto propagator = propagators.begin(); propagator < propagators.end(); ++propagator) { auto shape = static_cast((*propagator)->duplicate()); P(shape)->rs.path = P(path)->rs.path; auto opacity = repeater->interpOpacity ? mathLerp(repeater->startOpacity, repeater->endOpacity, static_cast(i + 1) / repeater->cnt) : repeater->startOpacity; shape->opacity(opacity); Matrix m; mathIdentity(&m); mathTranslate(&m, repeater->position.x * multiplier + repeater->anchor.x, repeater->position.y * multiplier + repeater->anchor.y); mathScale(&m, powf(repeater->scale.x * 0.01f, multiplier), powf(repeater->scale.y * 0.01f, multiplier)); mathRotate(&m, repeater->rotation * multiplier); mathTranslateR(&m, -repeater->anchor.x, -repeater->anchor.y); m = repeater->transform * m; auto pm = PP(shape)->transform(); if (pm) { Matrix inverse; mathInverse(&repeater->transform, &inverse); *pm = inverse * *pm; } shape->transform(pm ? m * *pm : m); shapes.push(shape); } } propagators.clear(); propagators.reserve(shapes.count); //push repeat shapes in order. if (repeater->inorder) { for (auto shape = shapes.begin(); shape < shapes.end(); ++shape) { parent->scene->push(cast(*shape)); propagators.push(*shape); } } else if (!shapes.empty()) { for (auto shape = shapes.end() - 1; shape >= shapes.begin(); --shape) { parent->scene->push(cast(*shape)); propagators.push(*shape); } } shapes.clear(); } } static void _appendRect(Shape* shape, float x, float y, float w, float h, float r, Matrix* transform, bool clockwise) { //sharp rect if (mathZero(r)) { PathCommand commands[] = { PathCommand::MoveTo, PathCommand::LineTo, PathCommand::LineTo, PathCommand::LineTo, PathCommand::Close }; Point points[4]; if (clockwise) { points[0] = {x + w, y}; points[1] = {x + w, y + h}; points[2] = {x, y + h}; points[3] = {x, y}; } else { points[0] = {x + w, y}; points[1] = {x, y}; points[2] = {x, y + h}; points[3] = {x + w, y + h}; } if (transform) { for (int i = 0; i < 4; i++) { points[i] *= *transform; } } shape->appendPath(commands, 5, points, 4); //round rect } else { constexpr int cmdCnt = 10; PathCommand commands[cmdCnt]; auto halfW = w * 0.5f; auto halfH = h * 0.5f; auto rx = r > halfW ? halfW : r; auto ry = r > halfH ? halfH : r; auto hrx = rx * PATH_KAPPA; auto hry = ry * PATH_KAPPA; constexpr int ptsCnt = 17; Point points[ptsCnt]; if (clockwise) { commands[0] = PathCommand::MoveTo; commands[1] = PathCommand::LineTo; commands[2] = PathCommand::CubicTo; commands[3] = PathCommand::LineTo; commands[4] = PathCommand::CubicTo;commands[5] = PathCommand::LineTo; commands[6] = PathCommand::CubicTo; commands[7] = PathCommand::LineTo; commands[8] = PathCommand::CubicTo; commands[9] = PathCommand::Close; points[0] = {x + w, y + ry}; //moveTo points[1] = {x + w, y + h - ry}; //lineTo points[2] = {x + w, y + h - ry + hry}; points[3] = {x + w - rx + hrx, y + h}; points[4] = {x + w - rx, y + h}; //cubicTo points[5] = {x + rx, y + h}, //lineTo points[6] = {x + rx - hrx, y + h}; points[7] = {x, y + h - ry + hry}; points[8] = {x, y + h - ry}; //cubicTo points[9] = {x, y + ry}, //lineTo points[10] = {x, y + ry - hry}; points[11] = {x + rx - hrx, y}; points[12] = {x + rx, y}; //cubicTo points[13] = {x + w - rx, y}; //lineTo points[14] = {x + w - rx + hrx, y}; points[15] = {x + w, y + ry - hry}; points[16] = {x + w, y + ry}; //cubicTo } else { commands[0] = PathCommand::MoveTo; commands[1] = PathCommand::CubicTo; commands[2] = PathCommand::LineTo; commands[3] = PathCommand::CubicTo; commands[4] = PathCommand::LineTo; commands[5] = PathCommand::CubicTo; commands[6] = PathCommand::LineTo; commands[7] = PathCommand::CubicTo; commands[8] = PathCommand::LineTo; commands[9] = PathCommand::Close; points[0] = {x + w, y + ry}; //moveTo points[1] = {x + w, y + ry - hry}; points[2] = {x + w - rx + hrx, y}; points[3] = {x + w - rx, y}; //cubicTo points[4] = {x + rx, y}, //lineTo points[5] = {x + rx - hrx, y}; points[6] = {x, y + ry - hry}; points[7] = {x, y + ry}; //cubicTo points[8] = {x, y + h - ry}; //lineTo points[9] = {x, y + h - ry + hry}; points[10] = {x + rx - hrx, y + h}; points[11] = {x + rx, y + h}; //cubicTo points[12] = {x + w - rx, y + h}; //lineTo points[13] = {x + w - rx + hrx, y + h}; points[14] = {x + w, y + h - ry + hry}; points[15] = {x + w, y + h - ry}; //cubicTo points[16] = {x + w, y + ry}; //lineTo } if (transform) { for (int i = 0; i < ptsCnt; i++) { points[i] *= *transform; } } shape->appendPath(commands, cmdCnt, points, ptsCnt); } } static void _updateRect(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto rect = static_cast(*child); auto position = rect->position(frameNo, exps); auto size = rect->size(frameNo, exps); auto roundness = rect->radius(frameNo, exps); if (ctx->roundness > roundness) roundness = ctx->roundness; if (roundness > ROUNDNESS_EPSILON) { if (roundness > size.x * 0.5f) roundness = size.x * 0.5f; if (roundness > size.y * 0.5f) roundness = size.y * 0.5f; } if (!ctx->repeaters.empty()) { auto shape = rect->pooling(); shape->reset(); _appendRect(shape, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, ctx->transform, rect->clockwise); _repeat(parent, shape, ctx); } else { _draw(parent, rect, ctx); _appendRect(ctx->merging, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, ctx->transform, rect->clockwise); } } static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry, Matrix* transform, bool clockwise) { auto rxKappa = rx * PATH_KAPPA; auto ryKappa = ry * PATH_KAPPA; constexpr int cmdsCnt = 6; PathCommand commands[cmdsCnt] = { PathCommand::MoveTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::Close }; constexpr int ptsCnt = 13; Point points[ptsCnt]; if (clockwise) { points[0] = {cx, cy - ry}; //moveTo points[1] = {cx + rxKappa, cy - ry}; points[2] = {cx + rx, cy - ryKappa}; points[3] = {cx + rx, cy}; //cubicTo points[4] = {cx + rx, cy + ryKappa}; points[5] = {cx + rxKappa, cy + ry}; points[6] = {cx, cy + ry}; //cubicTo points[7] = {cx - rxKappa, cy + ry}; points[8] = {cx - rx, cy + ryKappa}; points[9] = {cx - rx, cy}; //cubicTo points[10] = {cx - rx, cy - ryKappa}; points[11] = {cx - rxKappa, cy - ry}; points[12] = {cx, cy - ry}; //cubicTo } else { points[0] = {cx, cy - ry}; //moveTo points[1] = {cx - rxKappa, cy - ry}; points[2] = {cx - rx, cy - ryKappa}; points[3] = {cx - rx, cy}; //cubicTo points[4] = {cx - rx, cy + ryKappa}; points[5] = {cx - rxKappa, cy + ry}; points[6] = {cx, cy + ry}; //cubicTo points[7] = {cx + rxKappa, cy + ry}; points[8] = {cx + rx, cy + ryKappa}; points[9] = {cx + rx, cy}; //cubicTo points[10] = {cx + rx, cy - ryKappa}; points[11] = {cx + rxKappa, cy - ry}; points[12] = {cx, cy - ry}; //cubicTo } if (transform) { for (int i = 0; i < ptsCnt; ++i) { points[i] *= *transform; } } shape->appendPath(commands, cmdsCnt, points, ptsCnt); } static void _updateEllipse(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto ellipse = static_cast(*child); auto position = ellipse->position(frameNo, exps); auto size = ellipse->size(frameNo, exps); if (!ctx->repeaters.empty()) { auto shape = ellipse->pooling(); shape->reset(); _appendCircle(shape, position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->transform, ellipse->clockwise); _repeat(parent, shape, ctx); } else { _draw(parent, ellipse, ctx); _appendCircle(ctx->merging, position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->transform, ellipse->clockwise); } } static void _updatePath(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto path = static_cast(*child); if (!ctx->repeaters.empty()) { auto shape = path->pooling(); shape->reset(); path->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, ctx->transform, ctx->roundness, exps); _repeat(parent, shape, ctx); } else { _draw(parent, path, ctx); if (path->pathset(frameNo, P(ctx->merging)->rs.path.cmds, P(ctx->merging)->rs.path.pts, ctx->transform, ctx->roundness, exps)) { P(ctx->merging)->update(RenderUpdateFlag::Path); } } } static void _applyRoundedCorner(Shape* star, Shape* merging, float outerRoundness, float roundness, bool hasRoundness) { static constexpr auto ROUNDED_POLYSTAR_MAGIC_NUMBER = 0.47829f; auto cmdCnt = star->pathCommands(nullptr); const Point *pts = nullptr; auto ptsCnt = star->pathCoords(&pts); auto len = mathLength(pts[1] - pts[2]); auto r = len > 0.0f ? ROUNDED_POLYSTAR_MAGIC_NUMBER * mathMin(len * 0.5f, roundness) / len : 0.0f; if (hasRoundness) { P(merging)->rs.path.cmds.grow((uint32_t)(1.5 * cmdCnt)); P(merging)->rs.path.pts.grow((uint32_t)(4.5 * cmdCnt)); int start = 3 * mathZero(outerRoundness); merging->moveTo(pts[start].x, pts[start].y); for (uint32_t i = 1 + start; i < ptsCnt; i += 6) { auto& prev = pts[i]; auto& curr = pts[i + 2]; auto& next = (i < ptsCnt - start) ? pts[i + 4] : pts[2]; auto& nextCtrl = (i < ptsCnt - start) ? pts[i + 5] : pts[3]; auto dNext = r * (curr - next); auto dPrev = r * (curr - prev); auto p0 = curr - 2.0f * dPrev; auto p1 = curr - dPrev; auto p2 = curr - dNext; auto p3 = curr - 2.0f * dNext; merging->cubicTo(prev.x, prev.y, p0.x, p0.y, p0.x, p0.y); merging->cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); merging->cubicTo(p3.x, p3.y, next.x, next.y, nextCtrl.x, nextCtrl.y); } } else { P(merging)->rs.path.cmds.grow(2 * cmdCnt); P(merging)->rs.path.pts.grow(4 * cmdCnt); auto dPrev = r * (pts[1] - pts[0]); auto p = pts[0] + 2.0f * dPrev; merging->moveTo(p.x, p.y); for (uint32_t i = 1; i < ptsCnt; ++i) { auto& curr = pts[i]; auto& next = (i == ptsCnt - 1) ? pts[1] : pts[i + 1]; auto dNext = r * (curr - next); auto p0 = curr - 2.0f * dPrev; auto p1 = curr - dPrev; auto p2 = curr - dNext; auto p3 = curr - 2.0f * dNext; merging->lineTo(p0.x, p0.y); merging->cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); dPrev = -1.0f * dNext; } } merging->close(); } static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float roundness, float frameNo, Shape* merging, 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 angle = mathDeg2Rad(-90.0f); auto partialPointRadius = 0.0f; auto anglePerPoint = (2.0f * MATH_PI / ptsCnt); auto halfAnglePerPoint = anglePerPoint * 0.5f; auto partialPointAmount = ptsCnt - floorf(ptsCnt); auto longSegment = false; auto numPoints = size_t(ceilf(ptsCnt) * 2); auto direction = star->clockwise ? 1.0f : -1.0f; auto hasRoundness = false; bool roundedCorner = (roundness > ROUNDNESS_EPSILON) && (mathZero(innerRoundness) || mathZero(outerRoundness)); Shape* shape; if (roundedCorner) { shape = star->pooling(); shape->reset(); } else { shape = merging; } float x, y; if (!mathZero(partialPointAmount)) { angle += halfAnglePerPoint * (1.0f - partialPointAmount) * direction; } if (!mathZero(partialPointAmount)) { partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius); x = partialPointRadius * cosf(angle); y = partialPointRadius * sinf(angle); angle += anglePerPoint * partialPointAmount * 0.5f * direction; } else { x = outerRadius * cosf(angle); y = outerRadius * sinf(angle); angle += halfAnglePerPoint * direction; } if (mathZero(innerRoundness) && mathZero(outerRoundness)) { P(shape)->rs.path.pts.reserve(numPoints + 2); P(shape)->rs.path.cmds.reserve(numPoints + 3); } else { P(shape)->rs.path.pts.reserve(numPoints * 3 + 2); P(shape)->rs.path.cmds.reserve(numPoints + 3); hasRoundness = true; } Point in = {x, y}; if (transform) in *= *transform; shape->moveTo(in.x, in.y); for (size_t i = 0; i < numPoints; i++) { auto radius = longSegment ? outerRadius : innerRadius; auto dTheta = halfAnglePerPoint; if (!mathZero(partialPointRadius) && i == numPoints - 2) { dTheta = anglePerPoint * partialPointAmount * 0.5f; } if (!mathZero(partialPointRadius) && i == numPoints - 1) { radius = partialPointRadius; } auto previousX = x; auto previousY = y; x = radius * cosf(angle); y = radius * sinf(angle); if (hasRoundness) { auto cp1Theta = (mathAtan2(previousY, previousX) - MATH_PI2 * direction); auto cp1Dx = cosf(cp1Theta); auto cp1Dy = sinf(cp1Theta); auto cp2Theta = (mathAtan2(y, x) - MATH_PI2 * direction); auto cp2Dx = cosf(cp2Theta); auto cp2Dy = sinf(cp2Theta); auto cp1Roundness = longSegment ? innerRoundness : outerRoundness; auto cp2Roundness = longSegment ? outerRoundness : innerRoundness; auto cp1Radius = longSegment ? innerRadius : outerRadius; auto cp2Radius = longSegment ? outerRadius : innerRadius; auto cp1x = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dx / ptsCnt; auto cp1y = cp1Radius * cp1Roundness * POLYSTAR_MAGIC_NUMBER * cp1Dy / ptsCnt; auto cp2x = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dx / ptsCnt; auto cp2y = cp2Radius * cp2Roundness * POLYSTAR_MAGIC_NUMBER * cp2Dy / ptsCnt; if (!mathZero(partialPointAmount) && ((i == 0) || (i == numPoints - 1))) { cp1x *= partialPointAmount; cp1y *= partialPointAmount; cp2x *= partialPointAmount; cp2y *= partialPointAmount; } Point in2 = {previousX - cp1x, previousY - cp1y}; Point in3 = {x + cp2x, y + cp2y}; Point in4 = {x, y}; if (transform) { in2 *= *transform; in3 *= *transform; in4 *= *transform; } shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y); } else { Point in = {x, y}; if (transform) in *= *transform; shape->lineTo(in.x, in.y); } angle += dTheta * direction; longSegment = !longSegment; } shape->close(); if (roundedCorner) _applyRoundedCorner(shape, merging, outerRoundness, roundness, hasRoundness); } static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging, 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 roundness = star->outerRoundness(frameNo, exps) * 0.01f; auto angle = mathDeg2Rad(-90.0f); auto anglePerPoint = 2.0f * MATH_PI / float(ptsCnt); auto direction = star->clockwise ? 1.0f : -1.0f; auto hasRoundness = false; auto x = radius * cosf(angle); auto y = radius * sinf(angle); angle += anglePerPoint * direction; if (mathZero(roundness)) { P(merging)->rs.path.pts.reserve(ptsCnt + 2); P(merging)->rs.path.cmds.reserve(ptsCnt + 3); } else { P(merging)->rs.path.pts.reserve(ptsCnt * 3 + 2); P(merging)->rs.path.cmds.reserve(ptsCnt + 3); hasRoundness = true; } Point in = {x, y}; if (transform) in *= *transform; merging->moveTo(in.x, in.y); for (size_t i = 0; i < ptsCnt; i++) { auto previousX = x; auto previousY = y; x = (radius * cosf(angle)); y = (radius * sinf(angle)); if (hasRoundness) { auto cp1Theta = mathAtan2(previousY, previousX) - MATH_PI2 * direction; auto cp1Dx = cosf(cp1Theta); auto cp1Dy = sinf(cp1Theta); auto cp2Theta = mathAtan2(y, x) - MATH_PI2 * direction; auto cp2Dx = cosf(cp2Theta); auto cp2Dy = sinf(cp2Theta); auto cp1x = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dx; auto cp1y = radius * roundness * POLYGON_MAGIC_NUMBER * cp1Dy; auto cp2x = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dx; auto cp2y = radius * roundness * POLYGON_MAGIC_NUMBER * cp2Dy; Point in2 = {previousX - cp1x, previousY - cp1y}; Point in3 = {x + cp2x, y + cp2y}; Point in4 = {x, y}; if (transform) { in2 *= *transform; in3 *= *transform; in4 *= *transform; } merging->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y); } else { Point in = {x, y}; if (transform) in *= *transform; merging->lineTo(in.x, in.y); } angle += anglePerPoint * direction; } merging->close(); } static void _updatePolystar(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto star = static_cast(*child); //Optimize: Can we skip the individual coords transform? Matrix matrix; mathIdentity(&matrix); auto position = star->position(frameNo, exps); mathTranslate(&matrix, position.x, position.y); mathRotate(&matrix, star->rotation(frameNo, exps)); if (ctx->transform) matrix = *ctx->transform * matrix; auto identity = mathIdentity((const Matrix*)&matrix); if (!ctx->repeaters.empty()) { auto shape = star->pooling(); shape->reset(); if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, shape, exps); else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, shape, exps); _repeat(parent, shape, ctx); } else { _draw(parent, star, ctx); if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, ctx->merging, exps); else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, ctx->merging, exps); P(ctx->merging)->update(RenderUpdateFlag::Path); } } static void _updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto roundedCorner= static_cast(*child); auto roundness = roundedCorner->radius(frameNo, exps); if (ctx->roundness < roundness) ctx->roundness = roundness; } static void _updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto repeater= static_cast(*child); RenderRepeater r; r.cnt = static_cast(repeater->copies(frameNo, exps)); if (auto tr = PP(ctx->propagator)->transform()) r.transform = *tr; else mathIdentity(&r.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.inorder = repeater->inorder; r.interpOpacity = (r.startOpacity == r.endOpacity) ? false : true; ctx->repeaters.push(r); ctx->merging = nullptr; } static void _updateTrimpath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto trimpath = static_cast(*child); float begin, end; trimpath->segment(frameNo, begin, end, exps); if (P(ctx->propagator)->rs.stroke) { auto pbegin = P(ctx->propagator)->rs.stroke->trim.begin; auto pend = P(ctx->propagator)->rs.stroke->trim.end; auto length = fabsf(pend - pbegin); begin = (length * begin) + pbegin; end = (length * end) + pbegin; } P(ctx->propagator)->strokeTrim(begin, end, trimpath->type == LottieTrimpath::Type::Simultaneous); } static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts, LottieExpressions* exps) { contexts.head->begin = parent->children.end() - 1; while (!contexts.empty()) { auto ctx = contexts.front(); ctx->reqFragment = parent->reqFragment; for (auto child = ctx->begin; child >= parent->children.data; --child) { //Here switch-case statements are more performant than virtual methods. switch ((*child)->type) { case LottieObject::Group: { _updateGroup(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Transform: { _updateTransform(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::SolidFill: { _updateSolidFill(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::SolidStroke: { _updateSolidStroke(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::GradientFill: { _updateGradientFill(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::GradientStroke: { _updateGradientStroke(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Rect: { _updateRect(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Ellipse: { _updateEllipse(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Path: { _updatePath(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Polystar: { _updatePolystar(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Trimpath: { _updateTrimpath(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Repeater: { _updateRepeater(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::RoundedCorner: { _updateRoundedCorner(parent, child, frameNo, contexts, ctx, exps); break; } default: break; } } delete(ctx); } } static void _updatePrecomp(LottieLayer* precomp, float frameNo, LottieExpressions* exps) { if (precomp->children.empty()) return; frameNo = precomp->remap(frameNo, exps); for (auto c = precomp->children.end() - 1; c >= precomp->children.begin(); --c) { auto child = static_cast(*c); if (!child->matteSrc) _updateLayer(precomp, child, frameNo, exps); } //TODO: remove the intermediate scene.... if (precomp->scene->composite(nullptr) != CompositeMethod::None) { auto cscene = Scene::gen().release(); cscene->push(cast(precomp->scene)); precomp->scene = cscene; } //clip the layer viewport auto clipper = precomp->pooling(); clipper->transform(precomp->cache.matrix); precomp->scene->composite(cast(clipper), CompositeMethod::ClipPath); } static void _updateSolid(LottieLayer* layer) { auto solidFill = layer->pooling(); solidFill->opacity(layer->cache.opacity); layer->scene->push(cast(solidFill)); } static void _updateImage(LottieGroup* layer) { auto image = static_cast(layer->children.first()); layer->scene->push(tvg::cast(image->pooling(true))); } static void _updateText(LottieLayer* layer, float frameNo) { auto text = static_cast(layer->children.first()); auto& doc = text->doc(frameNo); auto p = doc.text; if (!p || !text->font) return; auto scale = doc.size; Point cursor = {0.0f, 0.0f}; auto scene = Scene::gen(); int line = 0; //text string int idx = 0; auto totalChars = strlen(p); while (true) { //TODO: remove nested scenes. //end of text, new line of the cursor position if (*p == 13 || *p == 3 || *p == '\0') { //text layout position auto ascent = text->font->ascent * scale; if (ascent > doc.bbox.size.y) ascent = doc.bbox.size.y; Point layout = {doc.bbox.pos.x, doc.bbox.pos.y + ascent - doc.shift}; //adjust the layout if (doc.justify == 1) layout.x += doc.bbox.size.x - (cursor.x * scale); //right aligned else if (doc.justify == 2) layout.x += (doc.bbox.size.x * 0.5f) - (cursor.x * 0.5f * scale); //center aligned scene->translate(layout.x, layout.y); scene->scale(scale); layer->scene->push(std::move(scene)); if (*p == '\0') break; ++p; //new text group, single scene for each line scene = Scene::gen(); cursor.x = 0.0f; cursor.y = ++line * (doc.height / scale); } //find the glyph bool found = false; for (auto g = text->font->chars.begin(); g < text->font->chars.end(); ++g) { auto glyph = *g; //draw matched glyphs if (!strncmp(glyph->code, p, glyph->len)) { auto shape = text->pooling(); shape->reset(); for (auto g = glyph->children.begin(); g < glyph->children.end(); ++g) { auto group = static_cast(*g); for (auto p = group->children.begin(); p < group->children.end(); ++p) { if (static_cast(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, 0.0f)) { P(shape)->update(RenderUpdateFlag::Path); } } } shape->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]); shape->translate(cursor.x, cursor.y); if (doc.stroke.render) { shape->stroke(StrokeJoin::Round); shape->stroke(doc.stroke.width / scale); shape->stroke(doc.stroke.color.rgb[0], doc.stroke.color.rgb[1], doc.stroke.color.rgb[2]); } //text range process for (auto s = text->ranges.begin(); s < text->ranges.end(); ++s) { float divisor = (*s)->rangeUnit == LottieTextRange::Unit::Percent ? (100.0f / totalChars) : 1; auto offset = (*s)->offset(frameNo) / divisor; auto start = nearbyintf((*s)->start(frameNo) / divisor) + offset; auto end = nearbyintf((*s)->end(frameNo) / divisor) + offset; if (start > end) std::swap(start, end); if (idx < start || idx >= end) continue; auto matrix = shape->transform(); shape->opacity((*s)->style.opacity(frameNo)); auto color = (*s)->style.fillColor(frameNo); shape->fill(color.rgb[0], color.rgb[1], color.rgb[2], (*s)->style.fillOpacity(frameNo)); mathRotate(&matrix, (*s)->style.rotation(frameNo)); auto glyphScale = (*s)->style.scale(frameNo) * 0.01f; mathScale(&matrix, glyphScale.x, glyphScale.y); auto position = (*s)->style.position(frameNo); mathTranslate(&matrix, position.x, position.y); shape->transform(matrix); if (doc.stroke.render) { auto strokeColor = (*s)->style.strokeColor(frameNo); shape->stroke((*s)->style.strokeWidth(frameNo) / scale); shape->stroke(strokeColor.rgb[0], strokeColor.rgb[1], strokeColor.rgb[2], (*s)->style.strokeOpacity(frameNo)); } cursor.x += (*s)->style.letterSpacing(frameNo); } scene->push(cast(shape)); p += glyph->len; idx += glyph->len; //advance the cursor position horizontally cursor.x += glyph->width + doc.tracking; found = true; break; } } if (!found) { ++p; ++idx; } } } static void _updateMaskings(LottieLayer* layer, float frameNo, LottieExpressions* exps) { if (layer->masks.count == 0) return; //Apply the base mask auto pMask = static_cast(layer->masks[0]); auto pMethod = pMask->method; auto pShape = Shape::gen().release(); pShape->fill(255, 255, 255, pMask->opacity(frameNo)); pShape->transform(layer->cache.matrix); if (pMask->pathset(frameNo, P(pShape)->rs.path.cmds, P(pShape)->rs.path.pts, nullptr, 0.0f, exps)) { P(pShape)->update(RenderUpdateFlag::Path); } if (pMethod == CompositeMethod::SubtractMask || pMethod == CompositeMethod::InvAlphaMask) { layer->scene->composite(tvg::cast(pShape), CompositeMethod::InvAlphaMask); } else { layer->scene->composite(tvg::cast(pShape), CompositeMethod::AlphaMask); } //Apply the subsquent masks for (auto m = layer->masks.begin() + 1; m < layer->masks.end(); ++m) { auto mask = static_cast(*m); auto method = mask->method; if (method == CompositeMethod::None) continue; //Append the mask shape if (pMethod == method && (method == CompositeMethod::SubtractMask || method == CompositeMethod::DifferenceMask)) { mask->pathset(frameNo, P(pShape)->rs.path.cmds, P(pShape)->rs.path.pts, nullptr, 0.0f, exps); //Chain composition } else { auto shape = Shape::gen().release(); shape->fill(255, 255, 255, mask->opacity(frameNo)); shape->transform(layer->cache.matrix); if (mask->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, 0.0f, exps)) { P(shape)->update(RenderUpdateFlag::Path); } pShape->composite(tvg::cast(shape), method); pShape = shape; pMethod = method; } } } static bool _updateMatte(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps) { auto target = layer->matteTarget; if (!target) return true; _updateLayer(root, target, frameNo, exps); if (target->scene) { layer->scene->composite(cast(target->scene), layer->matteType); } else if (layer->matteType == CompositeMethod::AlphaMask || layer->matteType == CompositeMethod::LumaMask) { //matte target is not exist. alpha blending definitely bring an invisible result delete(layer->scene); layer->scene = nullptr; return false; } return true; } static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps) { layer->scene = nullptr; //visibility if (frameNo < layer->inFrame || frameNo >= layer->outFrame) return; _updateTransform(layer, frameNo, exps); //full transparent scene. no need to perform if (layer->type != LottieLayer::Null && layer->cache.opacity == 0) return; //Prepare render data layer->scene = Scene::gen().release(); //ignore opacity when Null layer? if (layer->type != LottieLayer::Null) layer->scene->opacity(layer->cache.opacity); layer->scene->transform(layer->cache.matrix); if (layer->matteTarget && layer->masks.count > 0) TVGERR("LOTTIE", "FIXME: Matte + Masking??"); if (!_updateMatte(root, layer, frameNo, exps)) return; _updateMaskings(layer, frameNo, exps); switch (layer->type) { case LottieLayer::Precomp: { _updatePrecomp(layer, frameNo, exps); break; } case LottieLayer::Solid: { _updateSolid(layer); break; } case LottieLayer::Image: { _updateImage(layer); break; } case LottieLayer::Text: { _updateText(layer, frameNo); break; } default: { if (!layer->children.empty()) { Inlist contexts; contexts.back(new RenderContext); _updateChildren(layer, frameNo, contexts, exps); contexts.free(); } break; } } layer->scene->blend(layer->blendMethod); //the given matte source was composited by the target earlier. if (!layer->matteSrc) root->scene->push(cast(layer->scene)); } static void _buildReference(LottieComposition* comp, LottieLayer* layer) { for (auto asset = comp->assets.begin(); asset < comp->assets.end(); ++asset) { if (layer->rid != (*asset)->id) continue; if (layer->type == LottieLayer::Precomp) { auto assetLayer = static_cast(*asset); if (_buildComposition(comp, assetLayer)) { layer->children = assetLayer->children; layer->reqFragment = assetLayer->reqFragment; } } else if (layer->type == LottieLayer::Image) { layer->children.push(*asset); } break; } } static void _buildHierarchy(LottieGroup* parent, LottieLayer* child) { if (child->pidx == -1) return; if (child->matteTarget && child->pidx == child->matteTarget->idx) { child->parent = child->matteTarget; return; } for (auto p = parent->children.begin(); p < parent->children.end(); ++p) { auto parent = static_cast(*p); if (child == parent) continue; if (child->pidx == parent->idx) { child->parent = parent; break; } if (parent->matteTarget && parent->matteTarget->idx == child->pidx) { child->parent = parent->matteTarget; break; } } } static void _attachFont(LottieComposition* comp, LottieLayer* parent) { //TODO: Consider to migrate this attachment to the frame update time. for (auto c = parent->children.begin(); c < parent->children.end(); ++c) { auto text = static_cast(*c); auto& doc = text->doc(0); if (!doc.name) continue; auto len = strlen(doc.name); for (uint32_t i = 0; i < comp->fonts.count; ++i) { auto font = comp->fonts[i]; auto len2 = strlen(font->name); if (len == len2 && !strcmp(font->name, doc.name)) { text->font = font; break; } } } } static bool _buildComposition(LottieComposition* comp, LottieLayer* parent) { if (parent->children.count == 0) return false; if (parent->buildDone) return true; parent->buildDone = true; for (auto c = parent->children.begin(); c < parent->children.end(); ++c) { auto child = static_cast(*c); //attach the precomp layer. if (child->rid) _buildReference(comp, child); if (child->matteType != CompositeMethod::None) { //no index of the matte layer is provided: the layer above is used as the matte source if (child->mid == -1) { if (c > parent->children.begin()) { child->matteTarget = static_cast(*(c - 1)); } //matte layer is specified by an index. } else child->matteTarget = parent->layerByIdx(child->mid); } if (child->matteTarget) { //parenting _buildHierarchy(parent, child->matteTarget); //precomp referencing if (child->matteTarget->rid) _buildReference(comp, child->matteTarget); } _buildHierarchy(parent, child); //attach the necessary font data if (child->type == LottieLayer::Text) _attachFont(comp, child); } return true; } /************************************************************************/ /* External Class Implementation */ /************************************************************************/ bool LottieBuilder::update(LottieComposition* comp, float frameNo) { if (comp->root->children.empty()) return false; frameNo += comp->startFrame; if (frameNo < comp->startFrame) frameNo = comp->startFrame; if (frameNo >= comp->endFrame) frameNo = (comp->endFrame - 1); //update children layers auto root = comp->root; root->scene->clear(); if (exps && comp->expressions) exps->update(comp->timeAtFrame(frameNo)); for (auto child = root->children.end() - 1; child >= root->children.begin(); --child) { auto layer = static_cast(*child); if (!layer->matteSrc) _updateLayer(root, layer, frameNo, exps); } return true; } void LottieBuilder::build(LottieComposition* comp) { if (!comp) return; comp->root->scene = Scene::gen().release(); if (!comp->root->scene) return; _buildComposition(comp, comp->root); if (!update(comp, 0)) return; //viewport clip auto clip = Shape::gen(); clip->appendRect(0, 0, static_cast(comp->w), static_cast(comp->h)); comp->root->scene->composite(std::move(clip), CompositeMethod::ClipPath); }