lottie/text: Enable advanced text features with local font

Previously, local fonts had their own rendering process, which did not support advanced features such as Range Selector, Alignment Options and Follow Path.

To enable these features and unify the logic, local fonts are now rendered glyph by glyph.
This commit is contained in:
Jinny You 2025-03-20 19:31:59 +09:00
parent 4c5ce9862e
commit 7edd7c71bb

View file

@ -26,7 +26,7 @@
#include "tvgLottieModel.h" #include "tvgLottieModel.h"
#include "tvgLottieBuilder.h" #include "tvgLottieBuilder.h"
#include "tvgLottieExpressions.h" #include "tvgLottieExpressions.h"
#include "tvgText.h"
/************************************************************************/ /************************************************************************/
/* Internal Class Implementation */ /* Internal Class Implementation */
@ -899,45 +899,6 @@ void LottieBuilder::updateImage(LottieGroup* layer)
} }
//TODO: unify with the updateText() building logic
static void _fontText(LottieText* text, Scene* scene, float frameNo, LottieExpressions* exps)
{
auto& doc = text->doc(frameNo, exps);
if (!doc.text) return;
auto delim = "\r\n";
auto size = doc.size * 75.0f; //1 pt = 1/72; 1 in = 96 px; -> 72/96 = 0.75
auto lineHeight = doc.size * 100.0f;
auto buf = (char*)alloca(strlen(doc.text) + 1);
strcpy(buf, doc.text);
auto token = std::strtok(buf, delim);
auto cnt = 0;
while (token) {
auto txt = Text::gen();
if (txt->font(doc.name, size) != Result::Success) {
//fallback to any available font
txt->font(nullptr, size);
}
txt->text(token);
txt->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]);
float width;
txt->bounds(nullptr, nullptr, &width, nullptr);
auto cursorX = width * doc.justify;
auto cursorY = lineHeight * cnt;
txt->translate(cursorX, -lineHeight + cursorY);
token = std::strtok(nullptr, delim);
scene->push(txt);
cnt++;
}
}
void LottieBuilder::updateText(LottieLayer* layer, float frameNo) void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
{ {
auto text = static_cast<LottieText*>(layer->children.first()); auto text = static_cast<LottieText*>(layer->children.first());
@ -947,11 +908,6 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
if (!p || !text->font) return; if (!p || !text->font) return;
if (text->font->origin != LottieFont::Origin::Embedded) {
_fontText(text, layer->scene, frameNo, exps);
return;
}
auto scale = doc.size; auto scale = doc.size;
Point cursor{}; Point cursor{};
//TODO: Need to revise to alloc scene / textgroup when they are really necessary //TODO: Need to revise to alloc scene / textgroup when they are really necessary
@ -966,6 +922,7 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
//text string //text string
int idx = 0; int idx = 0;
float spaceWidth = 0.0f;
auto totalChars = strlen(p); auto totalChars = strlen(p);
while (true) { while (true) {
//TODO: remove nested scenes. //TODO: remove nested scenes.
@ -1027,11 +984,14 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
} }
//find the glyph //find the glyph
bool found = false; LottieGlyph* glyph = nullptr;
ARRAY_FOREACH(g, text->font->chars) { ARRAY_FOREACH(g, text->font->chars) {
auto glyph = *g; if (!strncmp((*g)->code, code, (*g)->len)) {
//draw matched glyphs glyph = *g;
if (!strncmp(glyph->code, code, glyph->len)) { break;
}
}
if (textGrouping == LottieText::AlignOption::Group::Chars || textGrouping == LottieText::AlignOption::Group::All) { if (textGrouping == LottieText::AlignOption::Group::Chars || textGrouping == LottieText::AlignOption::Group::All) {
//new text group, single scene for each characters //new text group, single scene for each characters
scene->push(textGroup); scene->push(textGroup);
@ -1039,8 +999,14 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
textGroup->translate(cursor.x, cursor.y); textGroup->translate(cursor.x, cursor.y);
} }
//draw the glyph
auto& textGroupMatrix = textGroup->transform(); auto& textGroupMatrix = textGroup->transform();
auto shape = text->pooling(); float glyphSpacing = 0.0f;
Shape* shape = nullptr;
Text* txt = nullptr;
if (glyph) {
shape = text->pooling();
shape->reset(); shape->reset();
ARRAY_FOREACH(p, glyph->children) { ARRAY_FOREACH(p, glyph->children) {
auto group = static_cast<LottieGroup*>(*p); auto group = static_cast<LottieGroup*>(*p);
@ -1050,6 +1016,59 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
} }
} }
} }
} else {
txt = tvg::Text::gen();
if (text->font->origin == LottieFont::Origin::Embedded || txt->font(doc.name, 75.0f) != Result::Success) {
//fallback to any available font
txt->font(nullptr, 75.0f);
}
float letterSpacing = 0.0f;
if (*p == ' ') {
if (spaceWidth > 0.0f) glyphSpacing = spaceWidth;
else {
// Calculate space width by measuring "a a" vs "aa"
const char* withSpace = "a a";
const char* withoutSpace = "aa";
txt->text(withSpace);
txt->bounds(nullptr, nullptr, &spaceWidth, nullptr);
float widthNoSpace;
txt->text(withoutSpace);
txt->bounds(nullptr, nullptr, &widthNoSpace, nullptr);
spaceWidth = glyphSpacing = spaceWidth - widthNoSpace;
}
} else if (*(p + 1) != '\0') {
// Get width of current char + next char
char twoChars[3] = {*p, *(p + 1), '\0'};
txt->text(twoChars);
float twoCharWidth;
txt->bounds(nullptr, nullptr, &twoCharWidth, nullptr);
// Get width of next char alone
char nextChar[2] = {*(p + 1), '\0'};
txt->text(nextChar);
float nextCharWidth;
txt->bounds(nullptr, nullptr, &nextCharWidth, nullptr);
// Calculate spacing between chars
letterSpacing = twoCharWidth - nextCharWidth; // Caching might help performance?
}
char targetChar[2] = {*p, '\0'};
txt->text(targetChar);
if (letterSpacing > 0.0f) {
float width;
txt->bounds(nullptr, nullptr, &width, nullptr);
glyphSpacing = letterSpacing - width;
}
shape = TEXT(txt)->shape;
}
shape->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]); shape->fill(doc.color.rgb[0], doc.color.rgb[1], doc.color.rgb[2]);
shape->translate(cursor.x - textGroupMatrix.e13, cursor.y - textGroupMatrix.e23); shape->translate(cursor.x - textGroupMatrix.e13, cursor.y - textGroupMatrix.e23);
shape->opacity(255); shape->opacity(255);
@ -1141,24 +1160,34 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
textGroup->transform(textGroupMatrix); textGroup->transform(textGroupMatrix);
} }
auto& matrix = shape->transform(); auto& matrix = txt ? txt->transform() : shape->transform();
tvg::identity(&matrix); tvg::identity(&matrix);
translate(&matrix, (translation / scale + cursor) - Point{textGroupMatrix.e13, textGroupMatrix.e23}); translate(&matrix, (translation / scale + cursor) - Point{textGroupMatrix.e13, textGroupMatrix.e23});
tvg::scale(&matrix, scaling * capScale); tvg::scale(&matrix, scaling * capScale);
shape->transform(matrix);
if (txt) {
matrix.e23 -= 100.0f; // Align line height for the local font
txt->transform(matrix);
} else shape->transform(matrix);
} }
//glyph width
float glyphWidth = 0.0f;
if (glyph) glyphWidth = glyph->width;
else if (txt) txt->bounds(nullptr, nullptr, &glyphWidth, nullptr);
if (needGroup) { if (needGroup) {
textGroup->push(shape); if (txt) textGroup->push(txt);
else textGroup->push(shape);
} else { } else {
// When text isn't selected, exclude the shape from the text group // When text isn't selected, exclude the shape from the text group
// Cases with matrix scaling factors =! 1 handled in the 'needGroup' scenario // Cases with matrix scaling factors =! 1 handled in the 'needGroup' scenario
auto& matrix = shape->transform(); auto& matrix = txt ? txt->transform() : shape->transform();
if (followPath) { if (followPath) {
identity(&matrix); identity(&matrix);
auto angle = 0.0f; auto angle = 0.0f;
auto halfGlyphWidth = glyph->width * 0.5f; auto halfGlyphWidth = glyphWidth * 0.5f;
auto position = followPath->position(cursor.x + halfGlyphWidth + firstMargin, angle); auto position = followPath->position(cursor.x + halfGlyphWidth + firstMargin, angle);
matrix.e11 = matrix.e22 = capScale; matrix.e11 = matrix.e22 = capScale;
matrix.e13 = position.x - halfGlyphWidth * matrix.e11; matrix.e13 = position.x - halfGlyphWidth * matrix.e11;
@ -1169,25 +1198,26 @@ void LottieBuilder::updateText(LottieLayer* layer, float frameNo)
matrix.e23 = cursor.y; matrix.e23 = cursor.y;
} }
if (txt) {
matrix.e23 -= 100.0f; // Align line height for the local font
txt->transform(matrix);
scene->push(txt);
} else {
shape->transform(matrix); shape->transform(matrix);
scene->push(shape); scene->push(shape);
} }
}
if (glyph) {
p += glyph->len; p += glyph->len;
idx += glyph->len; idx += glyph->len;
} else {
//advance the cursor position horizontally
cursor.x += (glyph->width + doc.tracking) * capScale;
found = true;
break;
}
}
if (!found) {
++p; ++p;
++idx; ++idx;
} }
//advance the cursor position horizontally
cursor.x += (glyphWidth + glyphSpacing + doc.tracking) * capScale;
} }
delete(scene); delete(scene);