lottie: add support for text follow path

Added support for cases without text grouping and range selector.

Co-Authored-By: Hemet Park <hermet@lottiefiles.com>

@Issue: https://github.com/thorvg/thorvg/issues/2888
This commit is contained in:
Mira Grudzinska 2025-02-25 11:31:04 +01:00 committed by Hermet Park
parent 24378cc200
commit ac080ffabc
5 changed files with 181 additions and 9 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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<tvg::Shape>
{
struct AlignOption
@ -364,6 +387,7 @@ struct LottieText : LottieObject, LottieRenderPooler<tvg::Shape>
LottieTextDoc doc;
LottieFont* font;
LottieTextFollowPath* followPath = nullptr;
Array<LottieTextRange*> ranges;
~LottieText()

View file

@ -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<LottieProperty::Type::Float>(text->followPath->firstMargin);
else skip();
} while ((key = nextObjectKey()));
}
void LottieParser::parseText(Array<LottieObject*>& parent)
{
enterObject();
@ -1188,11 +1202,7 @@ void LottieParser::parseText(Array<LottieObject*>& parent)
if (KEY_AS("d")) parseProperty<LottieProperty::Type::TextDoc>(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);

View file

@ -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<LottieGlyph*>& glyphs);