diff --git a/src/loaders/lottie/tvgLottieBuilder.cpp b/src/loaders/lottie/tvgLottieBuilder.cpp index 67b8c0cd..db53d315 100644 --- a/src/loaders/lottie/tvgLottieBuilder.cpp +++ b/src/loaders/lottie/tvgLottieBuilder.cpp @@ -974,6 +974,8 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo) int space = 0; auto lineSpacing = 0.0f; auto totalLineSpacing = 0.0f; + auto followPath = (text->followPath && ((uint32_t)text->followPath->maskIdx < layer->masks.count)) ? text->followPath : nullptr; + auto firstMargin = followPath ? followPath->prepare(layer->masks[followPath->maskIdx], frameNo, scale, tween, exps) : 0.0f; //text string int idx = 0; @@ -1163,10 +1165,23 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo) textGroup->push(shape); } else { // When text isn't selected, exclude the shape from the text group + // Cases with matrix scaling factors =! 1 handled in the 'needGroup' scenario auto& matrix = shape->transform(); - matrix.e13 = cursor.x; - matrix.e23 = cursor.y; - matrix.e11 = matrix.e22 = capScale; //cases with matrix scaling factors =! 1 handled in the 'needGroup' scenario + + if (followPath) { + identity(&matrix); + auto angle = 0.0f; + auto halfGlyphWidth = glyph->width * 0.5f; + auto position = followPath->position(cursor.x + halfGlyphWidth + firstMargin, angle); + matrix.e11 = matrix.e22 = capScale; + matrix.e13 = position.x - halfGlyphWidth * matrix.e11; + matrix.e23 = position.y - halfGlyphWidth * matrix.e21; + } else { + matrix.e11 = matrix.e22 = capScale; + matrix.e13 = cursor.x; + matrix.e23 = cursor.y; + } + shape->transform(matrix); scene->push(shape); } diff --git a/src/loaders/lottie/tvgLottieModel.cpp b/src/loaders/lottie/tvgLottieModel.cpp index ca43bea4..d090c586 100644 --- a/src/loaders/lottie/tvgLottieModel.cpp +++ b/src/loaders/lottie/tvgLottieModel.cpp @@ -29,12 +29,134 @@ /* Internal Class Implementation */ /************************************************************************/ - +Point LottieTextFollowPath::split(float dLen, float lenSearched, float& angle) +{ + switch (*cmds) { + case PathCommand::MoveTo: { + angle = 0.0f; + break; + } + case PathCommand::LineTo: { + auto dp = *pts - *(pts - 1); + angle = tvg::atan2(dp.y, dp.x); + break; + } + case PathCommand::CubicTo: { + auto bz = Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}; + float t = bz.at(lenSearched - currentLen, dLen); + angle = deg2rad(bz.angle(t)); + return bz.at(t); + } + case PathCommand::Close: { + auto dp = *start - *(pts - 1); + angle = tvg::atan2(dp.y, dp.x); + break; + } + } + return {}; +} /************************************************************************/ /* External Class Implementation */ /************************************************************************/ +float LottieTextFollowPath::prepare(LottieMask* mask, float frameNo, float scale, Tween& tween, LottieExpressions* exps) +{ + this->mask = mask; + Matrix m{1.0f / scale, 0.0f, 0.0f, 0.0f, 1.0f / scale, 0.0f, 0.0f, 0.0f, 1.0f}; + mask->pathset(frameNo, path, &m, tween, exps); + + pts = path.pts.data; + cmds = path.cmds.data; + cmdsCnt = path.cmds.count; + totalLen = tvg::length(cmds, cmdsCnt, pts, path.pts.count); + currentLen = 0.0f; + start = pts; + + return firstMargin(frameNo, tween, exps) / scale; +} + +Point LottieTextFollowPath::position(float lenSearched, float& angle) +{ + auto shift = [&]() -> void { + switch (*cmds) { + case PathCommand::MoveTo: + start = pts; + ++pts; + break; + case PathCommand::LineTo: + ++pts; + break; + case PathCommand::CubicTo: + pts += 3; + break; + case PathCommand::Close: + break; + } + ++cmds; + --cmdsCnt; + }; + + auto length = [&]() -> float { + switch (*cmds) { + case PathCommand::MoveTo: return 0.0f; + case PathCommand::LineTo: return tvg::length(pts - 1, pts); + case PathCommand::CubicTo: return Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}.length(); + case PathCommand::Close: return tvg::length(pts - 1, start); + } + return 0.0f; + }; + + //beyond the curve + if (lenSearched > totalLen) { + //shape is closed -> wrapping + if (path.cmds.last() == PathCommand::Close) { + lenSearched -= totalLen; + pts = path.pts.data; + cmds = path.cmds.data; + cmdsCnt = path.cmds.count; + currentLen = 0.0f; + //linear interpolation + } else { + while (cmdsCnt > 1) shift(); + switch (*cmds) { + case PathCommand::MoveTo: + angle = 0.0f; + return *pts; + case PathCommand::LineTo: { + auto len = lenSearched - totalLen; + auto dp = *pts - *(pts - 1); + angle = tvg::atan2(dp.y, dp.x); + return {pts->x + len * cos(angle), pts->y + len * sin(angle)}; + } + case PathCommand::CubicTo: { + auto len = lenSearched - totalLen; + angle = deg2rad(Bezier{*(pts - 1), *pts, *(pts + 1), *(pts + 2)}.angle(0.999f)); + return {(pts + 2)->x + len * cos(angle), (pts + 2)->y + len * sin(angle)}; + } + case PathCommand::Close: { + auto len = lenSearched - totalLen; + auto dp = *start - *(pts - 1); + angle = tvg::atan2(dp.y, dp.x); + return {(pts - 1)->x + len * cos(angle), (pts - 1)->y + len * sin(angle)}; + } + } + } + } + + while (cmdsCnt > 0) { + auto dLen = length(); + if (currentLen + dLen <= lenSearched) { + shift(); + currentLen += dLen; + continue; + } + return split(dLen, lenSearched, angle); + } + return {}; +} + + void LottieSlot::reset() { if (!overridden) return; diff --git a/src/loaders/lottie/tvgLottieModel.h b/src/loaders/lottie/tvgLottieModel.h index b3e1a022..03a01ea0 100644 --- a/src/loaders/lottie/tvgLottieModel.h +++ b/src/loaders/lottie/tvgLottieModel.h @@ -336,6 +336,29 @@ struct LottieMarker } }; + +struct LottieTextFollowPath +{ +private: + RenderPath path; + PathCommand* cmds; + uint32_t cmdsCnt; + Point* pts; + Point* start; + float totalLen; + float currentLen; + Point split(float dLen, float lenSearched, float& angle); + +public: + LottieFloat firstMargin = 0.0f; + LottieMask* mask; + int8_t maskIdx = -1; + + Point position(float lenSearched, float& angle); + float prepare(LottieMask* mask, float frameNo, float scale, Tween& tween, LottieExpressions* exps); +}; + + struct LottieText : LottieObject, LottieRenderPooler { struct AlignOption @@ -364,6 +387,7 @@ struct LottieText : LottieObject, LottieRenderPooler LottieTextDoc doc; LottieFont* font; + LottieTextFollowPath* followPath = nullptr; Array ranges; ~LottieText() diff --git a/src/loaders/lottie/tvgLottieParser.cpp b/src/loaders/lottie/tvgLottieParser.cpp index 4f46b06a..d97e8b92 100644 --- a/src/loaders/lottie/tvgLottieParser.cpp +++ b/src/loaders/lottie/tvgLottieParser.cpp @@ -1178,6 +1178,20 @@ void LottieParser::parseTextRange(LottieText* text) } +void LottieParser::parseTextFollowPath(LottieText* text) +{ + enterObject(); + auto key = nextObjectKey(); + if (!key) return; + if (!text->followPath) text->followPath = new LottieTextFollowPath; + do { + if (KEY_AS("m")) text->followPath->maskIdx = getInt(); + else if (KEY_AS("f")) parseProperty(text->followPath->firstMargin); + else skip(); + } while ((key = nextObjectKey())); +} + + void LottieParser::parseText(Array& parent) { enterObject(); @@ -1188,11 +1202,7 @@ void LottieParser::parseText(Array& parent) if (KEY_AS("d")) parseProperty(text->doc, text); else if (KEY_AS("a")) parseTextRange(text); else if (KEY_AS("m")) parseTextAlignmentOption(text); - else if (KEY_AS("p")) - { - TVGLOG("LOTTIE", "Text Follow Path (p) is not supported"); - skip(); - } + else if (KEY_AS("p")) parseTextFollowPath(text); else skip(); } parent.push(text); diff --git a/src/loaders/lottie/tvgLottieParser.h b/src/loaders/lottie/tvgLottieParser.h index bba13b72..01b8b780 100644 --- a/src/loaders/lottie/tvgLottieParser.h +++ b/src/loaders/lottie/tvgLottieParser.h @@ -120,6 +120,7 @@ private: void parseColorStop(LottieGradient* gradient); void parseTextRange(LottieText* text); void parseTextAlignmentOption(LottieText* text); + void parseTextFollowPath(LottieText* text); void parseAssets(); void parseFonts(); void parseChars(Array& glyphs);