lottie: add offsetPath support

Due to the lack of an analytical solution for Bezier
curves offsetting, a simple and computationally cheap
approximation has been implemented. The algorithm shifts
the segments connecting control points and determines
new points based on their intersections.

@issue: https://github.com/thorvg/thorvg/issues/2230
This commit is contained in:
Mira Grudzinska 2024-08-22 02:26:25 +02:00 committed by Hermet Park
parent b521eee2be
commit 660484b12b
10 changed files with 368 additions and 50 deletions

View file

@ -233,6 +233,17 @@ static inline Point operator/(const Point& lhs, const float rhs)
}
static inline Point mathNormal(const Point& p1, const Point& p2)
{
auto dir = p2 - p1;
auto len = mathLength(dir);
if (mathZero(len)) return {};
auto unitDir = dir / len;
return {-unitDir.y, unitDir.x};
}
static inline void mathLog(const Point& pt)
{
TVGLOG("COMMON", "Point: [%f %f]", pt.x, pt.y);

View file

@ -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 (mathZero(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<RenderContext>& contexts, RenderContext* ctx)
{
auto path = static_cast<LottiePath*>(*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 && (mathZero(innerRoundness) || mathZero(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<RenderContext>& contexts, RenderContext* ctx)
{
auto offsetPath = static_cast<LottieOffsetPath*>(*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<RenderContext>& contexts, RenderContext* ctx)
{
auto repeater= static_cast<LottieRepeater*>(*child);
auto repeater = static_cast<LottieRepeater*>(*child);
RenderRepeater r;
r.cnt = static_cast<int>(repeater->copies(frameNo, exps));
@ -905,6 +936,10 @@ void LottieBuilder::updateChildren(LottieGroup* parent, float frameNo, Inlist<Re
updateRoundedCorner(parent, child, frameNo, contexts, ctx);
break;
}
case LottieObject::OffsetPath: {
updateOffsetPath(parent, child, frameNo, contexts, ctx);
break;
}
default: break;
}
if (ctx->propagator->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<LottieGroup*>(*g);
for (auto p = group->children.begin(); p < group->children.end(); ++p) {
if (static_cast<LottiePath*>(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, nullptr)) {
if (static_cast<LottiePath*>(*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;

View file

@ -57,6 +57,7 @@ struct RenderContext
Array<RenderRepeater> 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<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);
void updateOffsetPath(LottieGroup* parent, LottieObject** child, float frameNo, Inlist<RenderContext>& contexts, RenderContext* ctx);
LottieExpressions* exps;
};

View file

@ -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<typename Property>
bool result(float frameNo, Array<PathCommand>& cmds, Array<Point>& pts, Matrix* transform, const LottieRoundnessModifier* roundness, LottieExpression* exp)
bool result(float frameNo, Array<PathCommand>& cmds, Array<Point>& 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<Property*>(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<typename Property> bool result(TVG_UNUSED float, TVG_UNUSED Point&, LottieExpression*) { return false; }
template<typename Property> bool result(TVG_UNUSED float, TVG_UNUSED RGB24&, TVG_UNUSED LottieExpression*) { return false; }
template<typename Property> bool result(TVG_UNUSED float, TVG_UNUSED Fill*, TVG_UNUSED LottieExpression*) { return false; }
template<typename Property> bool result(TVG_UNUSED float, TVG_UNUSED Array<PathCommand>&, TVG_UNUSED Array<Point>&, TVG_UNUSED Matrix* transform, TVG_UNUSED const LottieRoundnessModifier*, TVG_UNUSED LottieExpression*) { return false; }
template<typename Property> bool result(TVG_UNUSED float, TVG_UNUSED Array<PathCommand>&, TVG_UNUSED Array<Point>&, 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) {}

View file

@ -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<tvg::Shape>
{
LottieGroup();

View file

@ -21,7 +21,6 @@
*/
#include "tvgLottieModifier.h"
#include "tvgMath.h"
/************************************************************************/
@ -46,6 +45,115 @@ static void _roundCorner(Array<PathCommand>& cmds, Array<Point>& 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 = mathNormal(p1, p2) * offset;
return {p1 - scaledNormal, p2 - scaledNormal};
}
void LottieOffsetModifier::corner(const Line& line, const Line& nextLine, uint32_t movetoOutIndex, bool nextClose, Array<PathCommand>& outCmds, Array<Point>& 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 = mathNormal(line.pt1, line.pt2);
auto nextNorm = mathNormal(nextLine.pt1, nextLine.pt2);
auto miterDirection = (norm + nextNorm) / mathLength(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<PathCommand>& outCmds, Array<Point>& outPts, float offset) const
{
if (mathZero(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 && !mathZero(inPts[pt] - inPts[pt + 1]) && !mathZero(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<PathCommand>& outCmds, Array<Point>& 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 (mathZero(inPts[iPt - 1] - inPts[iPt]) || mathZero(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<PathCommand>& inCmds, const Array<Point>& inPts, Array<PathCommand>& outCmds, Array<Point>& 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<PathCommand>& outCmds, Array<Point>& 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;
}

View file

@ -25,6 +25,8 @@
#include "tvgCommon.h"
#include "tvgArray.h"
#include "tvgLines.h"
#include "tvgMath.h"
struct LottieRoundnessModifier
@ -40,4 +42,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<PathCommand>& outCmds, Array<Point>& outPts, bool clockwise) const;
bool modifyPolystar(const Array<PathCommand>& inCmds, const Array<Point>& inPts, Array<PathCommand>& outCmds, Array<Point>& outPts, bool clockwise) const;
bool modifyRect(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array<PathCommand>& outCmds, Array<Point>& 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<PathCommand>& cmds, Array<Point>& pts, float offset) const;
void corner(const Line& line, const Line& nextLine, uint32_t movetoIndex, bool nextClose, Array<PathCommand>& cmds, Array<Point>& pts) const;
};
#endif

View file

@ -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<LottieProperty::Type::Float>(offsetPath->offset);
else if (KEY_AS("lj")) offsetPath->join = getStrokeJoin();
else if (KEY_AS("ml")) parseProperty<LottieProperty::Type::Float>(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;
}

View file

@ -90,6 +90,7 @@ private:
LottieMask* parseMask();
LottieTrimpath* parseTrimpath();
LottieRepeater* parseRepeater();
LottieOffsetPath* parseOffsetPath();
LottieFont* parseFont();
LottieMarker* parseMarker();

View file

@ -409,7 +409,7 @@ struct LottiePathSet : LottieProperty
return (*frames)[frames->count];
}
bool operator()(float frameNo, Array<PathCommand>& cmds, Array<Point>& pts, Matrix* transform, const LottieRoundnessModifier* roundness)
bool operator()(float frameNo, Array<PathCommand>& cmds, Array<Point>& pts, Matrix* transform, const LottieRoundnessModifier* roundness, const LottieOffsetModifier* offsetPath)
{
PathSet* path = nullptr;
LottieScalarFrame<PathSet>* frame = nullptr;
@ -434,7 +434,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<PathCommand> cmds1(path->cmdsCnt);
Array<Point> 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);
@ -444,35 +453,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 = mathLerp(*s, *e, t);
if (transform) *p *= *transform;
if (!roundness && !offsetPath) {
for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e) {
auto pt = mathLerp(*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 = mathLerp(*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 = mathLerp(*s, *e, t);
if (transform) *p *= *transform;
}
_copy(&frame->value, cmds);
if (roundness) {
if (offsetPath) {
Array<PathCommand> cmds1;
Array<Point> 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<PathCommand>& cmds, Array<Point>& pts, Matrix* transform, const LottieRoundnessModifier* roundness, LottieExpressions* exps)
bool operator()(float frameNo, Array<PathCommand>& cmds, Array<Point>& 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<LottiePathSet>(frameNo, cmds, pts, transform, roundness, exp)) return true;
if (exps->result<LottiePathSet>(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() {}