diff --git a/src/common/tvgMath.h b/src/common/tvgMath.h index 65cdaac2..692ca2c1 100644 --- a/src/common/tvgMath.h +++ b/src/common/tvgMath.h @@ -233,6 +233,17 @@ static inline Point operator/(const Point& lhs, const float rhs) } +static inline Point normal(const Point& p1, const Point& p2) +{ + auto dir = p2 - p1; + auto len = length(dir); + if (tvg::zero(len)) return {}; + + auto unitDir = dir / len; + return {-unitDir.y, unitDir.x}; +} + + static inline void log(const Point& pt) { TVGLOG("COMMON", "Point: [%f %f]", pt.x, pt.y); diff --git a/src/loaders/lottie/tvgLottieBuilder.cpp b/src/loaders/lottie/tvgLottieBuilder.cpp index 07ab72f1..3bf1e1bf 100644 --- a/src/loaders/lottie/tvgLottieBuilder.cpp +++ b/src/loaders/lottie/tvgLottieBuilder.cpp @@ -381,7 +381,7 @@ static void _repeat(LottieGroup* parent, Shape* path, RenderContext* ctx) } -static void _appendRect(Shape* shape, float x, float y, float w, float h, float r, Matrix* transform, bool clockwise) +static void _appendRect(Shape* shape, float x, float y, float w, float h, float r, const LottieOffsetModifier* offsetPath, Matrix* transform, bool clockwise) { //sharp rect if (tvg::zero(r)) { @@ -407,7 +407,9 @@ static void _appendRect(Shape* shape, float x, float y, float w, float h, float points[i] *= *transform; } } - shape->appendPath(commands, 5, points, 4); + + if (offsetPath) offsetPath->modifyRect(commands, 5, points, 4, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, clockwise); + else shape->appendPath(commands, 5, points, 4); //round rect } else { constexpr int cmdCnt = 10; @@ -458,7 +460,9 @@ static void _appendRect(Shape* shape, float x, float y, float w, float h, float points[i] *= *transform; } } - shape->appendPath(commands, cmdCnt, points, ptsCnt); + + if (offsetPath) offsetPath->modifyRect(commands, cmdCnt, points, ptsCnt, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, clockwise); + else shape->appendPath(commands, cmdCnt, points, ptsCnt); } } @@ -479,17 +483,21 @@ void LottieBuilder::updateRect(LottieGroup* parent, LottieObject** child, float 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, r, ctx->transform, rect->clockwise); + _appendRect(shape, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, r, ctx->offsetPath, 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, r, ctx->transform, rect->clockwise); + _appendRect(ctx->merging, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, r, ctx->offsetPath, ctx->transform, rect->clockwise); } } -static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry, Matrix* transform, bool clockwise) +static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry, const LottieOffsetModifier* offsetPath, Matrix* transform, bool clockwise) { + if (offsetPath) offsetPath->modifyEllipse(rx, ry); + + if (rx == 0.0f || ry == 0.0f) return; + auto rxKappa = rx * PATH_KAPPA; auto ryKappa = ry * PATH_KAPPA; @@ -536,11 +544,11 @@ void LottieBuilder::updateEllipse(LottieGroup* parent, LottieObject** child, flo 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); + _appendCircle(shape, position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->offsetPath, 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); + _appendCircle(ctx->merging, position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->offsetPath, ctx->transform, ellipse->clockwise); } } @@ -548,22 +556,22 @@ void LottieBuilder::updateEllipse(LottieGroup* parent, LottieObject** child, flo void LottieBuilder::updatePath(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) { auto path = static_cast(*child); - + //TODO: use direction for paths' offsetPath 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); + path->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, ctx->transform, ctx->roundness, ctx->offsetPath, 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)) { + if (path->pathset(frameNo, P(ctx->merging)->rs.path.cmds, P(ctx->merging)->rs.path.pts, ctx->transform, ctx->roundness, ctx->offsetPath, exps)) { P(ctx->merging)->update(RenderUpdateFlag::Path); } } } -static void _updateStar(TVG_UNUSED LottieGroup* parent, LottiePolyStar* star, Matrix* transform, const LottieRoundnessModifier* roundness, float frameNo, Shape* merging, LottieExpressions* exps) +static void _updateStar(TVG_UNUSED LottieGroup* parent, LottiePolyStar* star, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath, float frameNo, Shape* merging, LottieExpressions* exps) { static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f; @@ -585,7 +593,7 @@ static void _updateStar(TVG_UNUSED LottieGroup* parent, LottiePolyStar* star, Ma bool roundedCorner = roundness && (tvg::zero(innerRoundness) || tvg::zero(outerRoundness)); Shape* shape; - if (roundedCorner) { + if (roundedCorner || offsetPath) { shape = star->pooling(); shape->reset(); } else { @@ -679,11 +687,19 @@ static void _updateStar(TVG_UNUSED LottieGroup* parent, LottiePolyStar* star, Ma } shape->close(); - if (roundedCorner) roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, outerRoundness, hasRoundness); + if (roundedCorner) { + if (offsetPath) { + auto intermediate = Shape::gen(); + roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(intermediate)->rs.path.cmds, P(intermediate)->rs.path.pts, outerRoundness, hasRoundness); + offsetPath->modifyPolystar(P(intermediate)->rs.path.cmds, P(intermediate)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, star->clockwise); + } else { + roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, outerRoundness, hasRoundness); + } + } else if (offsetPath) offsetPath->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, star->clockwise); } -static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, const LottieRoundnessModifier* roundness, float frameNo, Shape* merging, LottieExpressions* exps) +static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath, float frameNo, Shape* merging, LottieExpressions* exps) { static constexpr auto POLYGON_MAGIC_NUMBER = 0.25f; @@ -702,7 +718,7 @@ static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* tr angle += anglePerPoint * direction; Shape* shape; - if (roundedCorner) { + if (roundedCorner || offsetPath) { shape = star->pooling(); shape->reset(); } else { @@ -757,7 +773,15 @@ static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* tr } shape->close(); - if (roundedCorner) roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, 0.0f, false); + if (roundedCorner) { + if (offsetPath) { + auto intermediate = Shape::gen(); + roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(intermediate)->rs.path.cmds, P(intermediate)->rs.path.pts, 0.0f, false); + offsetPath->modifyPolystar(P(intermediate)->rs.path.cmds, P(intermediate)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, star->clockwise); + } else { + roundness->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, 0.0f, false); + } + } else if (offsetPath) offsetPath->modifyPolystar(P(shape)->rs.path.cmds, P(shape)->rs.path.pts, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, star->clockwise); } @@ -779,13 +803,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(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, shape, exps); - else _updatePolygon(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, shape, exps); + if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, ctx->offsetPath, frameNo, shape, exps); + else _updatePolygon(parent, star, identity ? nullptr : &matrix, ctx->roundness, ctx->offsetPath, 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, ctx->roundness, frameNo, ctx->merging, exps); + if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, ctx->offsetPath, frameNo, ctx->merging, exps); + else _updatePolygon(parent, star, identity ? nullptr : &matrix, ctx->roundness, ctx->offsetPath, frameNo, ctx->merging, exps); P(ctx->merging)->update(RenderUpdateFlag::Path); } } @@ -802,9 +826,16 @@ void LottieBuilder::updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieOb } +void LottieBuilder::updateOffsetPath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +{ + auto offsetPath = static_cast(*child); + if (!ctx->offsetPath) ctx->offsetPath = new LottieOffsetModifier(offsetPath->offset(frameNo, exps), offsetPath->miterLimit(frameNo, exps), offsetPath->join); +} + + void LottieBuilder::updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) { - auto repeater= static_cast(*child); + auto repeater = static_cast(*child); RenderRepeater r; r.cnt = static_cast(repeater->copies(frameNo, exps)); @@ -905,6 +936,10 @@ void LottieBuilder::updateChildren(LottieGroup* parent, float frameNo, Inlistpropagator->opacity() == 0) break; @@ -1008,7 +1043,7 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo) 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, nullptr)) { + if (static_cast(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, nullptr, nullptr)) { P(shape)->update(RenderUpdateFlag::Path); } } @@ -1097,7 +1132,7 @@ void LottieBuilder::updateMaskings(LottieLayer* layer, float frameNo) pShape->reset(); pShape->fill(255, 255, 255, pMask->opacity(frameNo)); pShape->transform(layer->cache.matrix); - pMask->pathset(frameNo, P(pShape)->rs.path.cmds, P(pShape)->rs.path.pts, nullptr, nullptr, exps); + pMask->pathset(frameNo, P(pShape)->rs.path.cmds, P(pShape)->rs.path.pts, nullptr, nullptr, nullptr, exps); if (pMethod == CompositeMethod::SubtractMask || pMethod == CompositeMethod::InvAlphaMask) { layer->scene->composite(tvg::cast(pShape), CompositeMethod::InvAlphaMask); @@ -1113,14 +1148,14 @@ void LottieBuilder::updateMaskings(LottieLayer* layer, float frameNo) //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, nullptr, exps); + mask->pathset(frameNo, P(pShape)->rs.path.cmds, P(pShape)->rs.path.pts, nullptr, nullptr, nullptr, exps); //Chain composition } else { auto shape = layer->pooling(); shape->reset(); shape->fill(255, 255, 255, mask->opacity(frameNo)); shape->transform(layer->cache.matrix); - mask->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, nullptr, exps); + mask->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, nullptr, nullptr, exps); pShape->composite(tvg::cast(shape), method); pShape = shape; pMethod = method; diff --git a/src/loaders/lottie/tvgLottieBuilder.h b/src/loaders/lottie/tvgLottieBuilder.h index a60f0161..d38ae402 100644 --- a/src/loaders/lottie/tvgLottieBuilder.h +++ b/src/loaders/lottie/tvgLottieBuilder.h @@ -57,6 +57,7 @@ struct RenderContext Array repeaters; Matrix* transform = nullptr; LottieRoundnessModifier* roundness = nullptr; + LottieOffsetModifier* offsetPath = nullptr; bool fragmenting = false; //render context has been fragmented by filling bool reqFragment = false; //requirement to fragment the render context @@ -72,6 +73,7 @@ struct RenderContext PP(propagator)->unref(); free(transform); delete(roundness); + delete(offsetPath); } RenderContext(const RenderContext& rhs, Shape* propagator, bool mergeable = false) @@ -81,6 +83,7 @@ struct RenderContext this->propagator = propagator; this->repeaters = rhs.repeaters; if (rhs.roundness) this->roundness = new LottieRoundnessModifier(rhs.roundness->r); + if (rhs.offsetPath) this->offsetPath = new LottieOffsetModifier(rhs.offsetPath->offset, rhs.offsetPath->miterLimit, rhs.offsetPath->join); } }; @@ -122,6 +125,7 @@ private: void updateTrimpath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx); void updateRepeater(LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx); void updateRoundedCorner(LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx); + void updateOffsetPath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx); LottieExpressions* exps; }; diff --git a/src/loaders/lottie/tvgLottieExpressions.h b/src/loaders/lottie/tvgLottieExpressions.h index 408c1dfe..4a3c2dcf 100644 --- a/src/loaders/lottie/tvgLottieExpressions.h +++ b/src/loaders/lottie/tvgLottieExpressions.h @@ -30,6 +30,7 @@ struct LottieExpression; struct LottieComposition; struct LottieLayer; struct LottieRoundnessModifier; +struct LottieOffsetModifier; #ifdef THORVG_LOTTIE_EXPRESSIONS_SUPPORT @@ -111,13 +112,13 @@ public: } template - bool result(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness, LottieExpression* exp) + bool result(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath, LottieExpression* exp) { auto bm_rt = evaluate(frameNo, exp); if (jerry_value_is_undefined(bm_rt)) return false; if (auto pathset = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { - (*pathset)(frameNo, cmds, pts, transform, roundness); + (*pathset)(frameNo, cmds, pts, transform, roundness, offsetPath); } jerry_value_free(bm_rt); return true; @@ -156,7 +157,7 @@ struct LottieExpressions template bool result(TVG_UNUSED float, TVG_UNUSED Point&, LottieExpression*) { return false; } template bool result(TVG_UNUSED float, TVG_UNUSED RGB24&, TVG_UNUSED LottieExpression*) { return false; } template bool result(TVG_UNUSED float, TVG_UNUSED Fill*, TVG_UNUSED LottieExpression*) { return false; } - template bool result(TVG_UNUSED float, TVG_UNUSED Array&, TVG_UNUSED Array&, TVG_UNUSED Matrix* transform, TVG_UNUSED const LottieRoundnessModifier*, TVG_UNUSED LottieExpression*) { return false; } + template bool result(TVG_UNUSED float, TVG_UNUSED Array&, TVG_UNUSED Array&, TVG_UNUSED Matrix* transform, TVG_UNUSED const LottieRoundnessModifier*, TVG_UNUSED const LottieOffsetModifier*, TVG_UNUSED LottieExpression*) { return false; } void update(TVG_UNUSED float) {} static LottieExpressions* instance() { return nullptr; } static void retrieve(TVG_UNUSED LottieExpressions* instance) {} diff --git a/src/loaders/lottie/tvgLottieModel.h b/src/loaders/lottie/tvgLottieModel.h index 653f5e87..8ba5fb3b 100644 --- a/src/loaders/lottie/tvgLottieModel.h +++ b/src/loaders/lottie/tvgLottieModel.h @@ -106,7 +106,8 @@ struct LottieObject Trimpath, Text, Repeater, - RoundedCorner + RoundedCorner, + OffsetPath }; virtual ~LottieObject() @@ -659,6 +660,19 @@ struct LottieRepeater : LottieObject }; +struct LottieOffsetPath : LottieObject +{ + void prepare() + { + LottieObject::type = LottieObject::OffsetPath; + } + + LottieFloat offset = 0.0f; + LottieFloat miterLimit = 4.0f; + StrokeJoin join = StrokeJoin::Miter; +}; + + struct LottieGroup : LottieObject, LottieRenderPooler { LottieGroup(); diff --git a/src/loaders/lottie/tvgLottieModifier.cpp b/src/loaders/lottie/tvgLottieModifier.cpp index b5476829..532c5d65 100644 --- a/src/loaders/lottie/tvgLottieModifier.cpp +++ b/src/loaders/lottie/tvgLottieModifier.cpp @@ -21,7 +21,6 @@ */ #include "tvgLottieModifier.h" -#include "tvgMath.h" /************************************************************************/ @@ -46,6 +45,115 @@ static void _roundCorner(Array& cmds, Array& pts, const Poin cmds.push(PathCommand::CubicTo); } + +static bool _zero(const Point& p1, const Point& p2) +{ + constexpr float epsilon = 1e-3f; + return fabsf(p1.x / p2.x - 1.0f) < epsilon && fabsf(p1.y / p2.y - 1.0f) < epsilon; +} + + +static bool _intersect(const Line& line1, const Line& line2, Point& intersection, bool& inside) +{ + if (_zero(line1.pt2, line2.pt1)) { + intersection = line1.pt2; + inside = true; + return true; + } + + constexpr float epsilon = 1e-3f; + float denom = (line1.pt2.x - line1.pt1.x) * (line2.pt2.y - line2.pt1.y) - (line1.pt2.y - line1.pt1.y) * (line2.pt2.x - line2.pt1.x); + if (fabsf(denom) < epsilon) return false; + + float t = ((line2.pt1.x - line1.pt1.x) * (line2.pt2.y - line2.pt1.y) - (line2.pt1.y - line1.pt1.y) * (line2.pt2.x - line2.pt1.x)) / denom; + float u = ((line2.pt1.x - line1.pt1.x) * (line1.pt2.y - line1.pt1.y) - (line2.pt1.y - line1.pt1.y) * (line1.pt2.x - line1.pt1.x)) / denom; + + intersection.x = line1.pt1.x + t * (line1.pt2.x - line1.pt1.x); + intersection.y = line1.pt1.y + t * (line1.pt2.y - line1.pt1.y); + inside = t >= -epsilon && t <= 1.0f + epsilon && u >= -epsilon && u <= 1.0f + epsilon; + + return true; +} + + +static Line _offset(const Point& p1, const Point& p2, float offset) +{ + auto scaledNormal = normal(p1, p2) * offset; + return {p1 - scaledNormal, p2 - scaledNormal}; +} + + +void LottieOffsetModifier::corner(const Line& line, const Line& nextLine, uint32_t movetoOutIndex, bool nextClose, Array& outCmds, Array& outPts) const +{ + bool inside{}; + Point intersect{}; + if (_intersect(line, nextLine, intersect, inside)) { + if (inside) { + if (nextClose) outPts[movetoOutIndex] = intersect; + outPts.push(intersect); + } else { + outPts.push(line.pt2); + if (join == StrokeJoin::Round) { + outCmds.push(PathCommand::CubicTo); + outPts.push((line.pt2 + intersect) * 0.5f); + outPts.push((nextLine.pt1 + intersect) * 0.5f); + outPts.push(nextLine.pt1); + } else if (join == StrokeJoin::Miter) { + auto norm = normal(line.pt1, line.pt2); + auto nextNorm = normal(nextLine.pt1, nextLine.pt2); + auto miterDirection = (norm + nextNorm) / length(norm + nextNorm); + outCmds.push(PathCommand::LineTo); + if (1.0f <= miterLimit * fabsf(miterDirection.x * norm.x + miterDirection.y * norm.y)) outPts.push(intersect); + else outPts.push(nextLine.pt1); + } else { + outCmds.push(PathCommand::LineTo); + outPts.push(nextLine.pt1); + } + } + } else outPts.push(line.pt2); +} + +void LottieOffsetModifier::line(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t& currentPt, uint32_t currentCmd, State& state, bool degenerated, Array& outCmds, Array& outPts, float offset) const +{ + if (tvg::zero(inPts[currentPt - 1] - inPts[currentPt])) { + ++currentPt; + return; + } + + if (inCmds[currentCmd - 1] != PathCommand::LineTo) state.line = _offset(inPts[currentPt - 1], inPts[currentPt], offset); + + if (state.moveto) { + outCmds.push(PathCommand::MoveTo); + state.movetoOutIndex = outPts.count; + outPts.push(state.line.pt1); + state.firstLine = state.line; + state.moveto = false; + } + + auto nonDegeneratedCubic = [&](uint32_t cmd, uint32_t pt) { + return inCmds[cmd] == PathCommand::CubicTo && !tvg::zero(inPts[pt] - inPts[pt + 1]) && !tvg::zero(inPts[pt + 2] - inPts[pt + 3]); + }; + + outCmds.push(PathCommand::LineTo); + + if (currentCmd + 1 == inCmdsCnt || inCmds[currentCmd + 1] == PathCommand::MoveTo || nonDegeneratedCubic(currentCmd + 1, currentPt + degenerated)) { + outPts.push(state.line.pt2); + ++currentPt; + return; + } + + Line nextLine = state.firstLine; + if (inCmds[currentCmd + 1] == PathCommand::LineTo) nextLine = _offset(inPts[currentPt + degenerated], inPts[currentPt + 1 + degenerated], offset); + else if (inCmds[currentCmd + 1] == PathCommand::CubicTo) nextLine = _offset(inPts[currentPt + 1 + degenerated], inPts[currentPt + 2 + degenerated], offset); + else if (inCmds[currentCmd + 1] == PathCommand::Close && !_zero(inPts[currentPt + degenerated], inPts[state.movetoInIndex + degenerated])) + nextLine = _offset(inPts[currentPt + degenerated], inPts[state.movetoInIndex + degenerated], offset); + + corner(state.line, nextLine, state.movetoOutIndex, inCmds[currentCmd + 1] == PathCommand::Close, outCmds, outPts); + + state.line = nextLine; + ++currentPt; +} + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ @@ -177,4 +285,81 @@ bool LottieRoundnessModifier::modifyRect(const Point& size, float& r) const { r = std::min(this->r, std::max(size.x, size.y) * 0.5f); return true; +} + + +bool LottieOffsetModifier::modifyPath(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array& outCmds, Array& outPts, bool clockwise) const +{ + outCmds.reserve(inCmdsCnt * 2); + outPts.reserve(inPtsCnt * (join == StrokeJoin::Round ? 4 : 2)); + + State state; + auto offset = clockwise ? this->offset : -this->offset; + + for (uint32_t iCmd = 0, iPt = 0; iCmd < inCmdsCnt; ++iCmd) { + if (inCmds[iCmd] == PathCommand::MoveTo) { + state.moveto = true; + state.movetoInIndex = iPt++; + } else if (inCmds[iCmd] == PathCommand::LineTo) { + line(inCmds, inCmdsCnt, inPts, iPt, iCmd, state, false, outCmds, outPts, offset); + } else if (inCmds[iCmd] == PathCommand::CubicTo) { + //cubic degenerated to a line + if (tvg::zero(inPts[iPt - 1] - inPts[iPt]) || tvg::zero(inPts[iPt + 1] - inPts[iPt + 2])) { + ++iPt; + line(inCmds, inCmdsCnt, inPts, iPt, iCmd, state, true, outCmds, outPts, offset); + ++iPt; + continue; + } + + auto line1 = _offset(inPts[iPt - 1], inPts[iPt], offset); + auto line2 = _offset(inPts[iPt], inPts[iPt + 1], offset); + auto line3 = _offset(inPts[iPt + 1], inPts[iPt + 2], offset); + + if (state.moveto) { + outCmds.push(PathCommand::MoveTo); + state.movetoOutIndex = outPts.count; + outPts.push(line1.pt1); + state.firstLine = line1; + state.moveto = false; + } + + bool inside{}; + Point intersect{}; + _intersect(line1, line2, intersect, inside); + outPts.push(intersect); + _intersect(line2, line3, intersect, inside); + outPts.push(intersect); + outPts.push(line3.pt2); + outCmds.push(PathCommand::CubicTo); + + iPt += 3; + } + else { + if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) { + outCmds.push(PathCommand::LineTo); + corner(state.line, state.firstLine, state.movetoOutIndex, true, outCmds, outPts); + } + outCmds.push(PathCommand::Close); + } + } + return true; +} + + +bool LottieOffsetModifier::modifyPolystar(const Array& inCmds, const Array& inPts, Array& outCmds, Array& outPts, bool clockwise) const { + return modifyPath(inCmds.data, inCmds.count, inPts.data, inPts.count, outCmds, outPts, clockwise); +} + + +bool LottieOffsetModifier::modifyRect(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array& outCmds, Array& outPts, bool clockwise) const +{ + return modifyPath(inCmds, inCmdsCnt, inPts, inPtsCnt, outCmds, outPts, clockwise); +} + + +bool LottieOffsetModifier::modifyEllipse(float& rx, float& ry) const +{ + rx += offset; + ry += offset; + return true; } \ No newline at end of file diff --git a/src/loaders/lottie/tvgLottieModifier.h b/src/loaders/lottie/tvgLottieModifier.h index ba95d727..69aa8e96 100644 --- a/src/loaders/lottie/tvgLottieModifier.h +++ b/src/loaders/lottie/tvgLottieModifier.h @@ -25,6 +25,7 @@ #include "tvgCommon.h" #include "tvgArray.h" +#include "tvgMath.h" struct LottieRoundnessModifier @@ -40,4 +41,31 @@ struct LottieRoundnessModifier }; +struct LottieOffsetModifier +{ + float offset; + float miterLimit; + StrokeJoin join; + + LottieOffsetModifier(float offset, float miter, StrokeJoin join) : offset(offset), miterLimit(miter), join(join) {}; + + bool modifyPath(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array& outCmds, Array& outPts, bool clockwise) const; + bool modifyPolystar(const Array& inCmds, const Array& inPts, Array& outCmds, Array& outPts, bool clockwise) const; + bool modifyRect(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array& outCmds, Array& outPts, bool clockwise) const; + bool modifyEllipse(float& rx, float& ry) const; + +private: + struct State + { + Line line{}; + Line firstLine{}; + bool moveto = false; + uint32_t movetoOutIndex = 0; + uint32_t movetoInIndex = 0; + }; + + void line(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t& currentPt, uint32_t currentCmd, State& state, bool degenerated, Array& cmds, Array& pts, float offset) const; + void corner(const Line& line, const Line& nextLine, uint32_t movetoIndex, bool nextClose, Array& cmds, Array& pts) const; +}; + #endif \ No newline at end of file diff --git a/src/loaders/lottie/tvgLottieParser.cpp b/src/loaders/lottie/tvgLottieParser.cpp index d1aae5cd..2eadaff3 100644 --- a/src/loaders/lottie/tvgLottieParser.cpp +++ b/src/loaders/lottie/tvgLottieParser.cpp @@ -866,6 +866,25 @@ LottieRepeater* LottieParser::parseRepeater() } +LottieOffsetPath* LottieParser::parseOffsetPath() +{ + auto offsetPath = new LottieOffsetPath; + + context.parent = offsetPath; + + while (auto key = nextObjectKey()) { + if (parseCommon(offsetPath, key)) continue; + else if (KEY_AS("a")) parseProperty(offsetPath->offset); + else if (KEY_AS("lj")) offsetPath->join = getStrokeJoin(); + else if (KEY_AS("ml")) parseProperty(offsetPath->miterLimit); + else skip(key); + } + offsetPath->prepare(); + + return offsetPath; +} + + LottieObject* LottieParser::parseObject() { auto type = getString(); @@ -887,7 +906,7 @@ LottieObject* LottieParser::parseObject() else if (!strcmp(type, "mm")) TVGERR("LOTTIE", "MergePath(mm) is not supported yet"); else if (!strcmp(type, "pb")) TVGERR("LOTTIE", "Puker/Bloat(pb) is not supported yet"); else if (!strcmp(type, "tw")) TVGERR("LOTTIE", "Twist(tw) is not supported yet"); - else if (!strcmp(type, "op")) TVGERR("LOTTIE", "Offset Path(op) is not supported yet"); + else if (!strcmp(type, "op")) return parseOffsetPath(); else if (!strcmp(type, "zz")) TVGERR("LOTTIE", "Zig Zag(zz) is not supported yet"); return nullptr; } diff --git a/src/loaders/lottie/tvgLottieParser.h b/src/loaders/lottie/tvgLottieParser.h index 32741909..2c8d4d36 100644 --- a/src/loaders/lottie/tvgLottieParser.h +++ b/src/loaders/lottie/tvgLottieParser.h @@ -90,6 +90,7 @@ private: LottieMask* parseMask(); LottieTrimpath* parseTrimpath(); LottieRepeater* parseRepeater(); + LottieOffsetPath* parseOffsetPath(); LottieFont* parseFont(); LottieMarker* parseMarker(); diff --git a/src/loaders/lottie/tvgLottieProperty.h b/src/loaders/lottie/tvgLottieProperty.h index 9e746c6f..ea6b3598 100644 --- a/src/loaders/lottie/tvgLottieProperty.h +++ b/src/loaders/lottie/tvgLottieProperty.h @@ -407,7 +407,7 @@ struct LottiePathSet : LottieProperty return (*frames)[frames->count]; } - bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness) + bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath) { PathSet* path = nullptr; LottieScalarFrame* frame = nullptr; @@ -432,7 +432,16 @@ struct LottiePathSet : LottieProperty } if (!interpolate) { - if (roundness) return roundness->modifyPath(path->cmds, path->cmdsCnt, path->pts, path->ptsCnt, cmds, pts, transform); + if (roundness) { + if (offsetPath) { + Array cmds1(path->cmdsCnt); + Array pts1(path->ptsCnt); + roundness->modifyPath(path->cmds, path->cmdsCnt, path->pts, path->ptsCnt, cmds1, pts1, transform); + return offsetPath->modifyPath(cmds1.data, cmds1.count, pts1.data, pts1.count, cmds, pts, true); + } + return roundness->modifyPath(path->cmds, path->cmdsCnt, path->pts, path->ptsCnt, cmds, pts, transform); + } + if (offsetPath) return offsetPath->modifyPath(path->cmds, path->cmdsCnt, path->pts, path->ptsCnt, cmds, pts, true); _copy(path, cmds); _copy(path, pts, transform); @@ -442,35 +451,45 @@ struct LottiePathSet : LottieProperty auto s = frame->value.pts; auto e = (frame + 1)->value.pts; - if (roundness) { - auto interpPts = (Point*)malloc(frame->value.ptsCnt * sizeof(Point)); - auto p = interpPts; - for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e, ++p) { - *p = lerp(*s, *e, t); - if (transform) *p *= *transform; + if (!roundness && !offsetPath) { + for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e) { + auto pt = lerp(*s, *e, t); + if (transform) pt *= *transform; + pts.push(pt); } - roundness->modifyPath(frame->value.cmds, frame->value.cmdsCnt, interpPts, frame->value.ptsCnt, cmds, pts, nullptr); - free(interpPts); + _copy(&frame->value, cmds); return true; } - for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e) { - auto pt = lerp(*s, *e, t); - if (transform) pt *= *transform; - pts.push(pt); + auto interpPts = (Point*)malloc(frame->value.ptsCnt * sizeof(Point)); + auto p = interpPts; + for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e, ++p) { + *p = lerp(*s, *e, t); + if (transform) *p *= *transform; } - _copy(&frame->value, cmds); + + if (roundness) { + if (offsetPath) { + Array cmds1; + Array pts1; + roundness->modifyPath(frame->value.cmds, frame->value.cmdsCnt, interpPts, frame->value.ptsCnt, cmds1, pts1, nullptr); + offsetPath->modifyPath(cmds1.data, cmds1.count, pts1.data, pts1.count, cmds, pts, true); + } else roundness->modifyPath(frame->value.cmds, frame->value.cmdsCnt, interpPts, frame->value.ptsCnt, cmds, pts, nullptr); + } else if (offsetPath) offsetPath->modifyPath(frame->value.cmds, frame->value.cmdsCnt, interpPts, frame->value.ptsCnt, cmds, pts, true); + + free(interpPts); + return true; } - bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness, LottieExpressions* exps) + bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath, LottieExpressions* exps) { if (exps && exp) { if (exp->loop.mode != LottieExpression::LoopMode::None) frameNo = _loop(frames, frameNo, exp); - if (exps->result(frameNo, cmds, pts, transform, roundness, exp)) return true; + if (exps->result(frameNo, cmds, pts, transform, roundness, offsetPath, exp)) return true; } - return operator()(frameNo, cmds, pts, transform, roundness); + return operator()(frameNo, cmds, pts, transform, roundness, offsetPath); } void prepare() {}