/* * Copyright (c) 2023 - 2025 the ThorVG project. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "tvgMath.h" #include "tvgTaskScheduler.h" #include "tvgLottieModel.h" #include "tvgCompressor.h" /************************************************************************/ /* 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}; path.clear(); 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) { //position before the start of the curve if (lenSearched <= 0.0f) { //shape is closed -> wrapping if (path.cmds.last() == PathCommand::Close) { while (lenSearched < 0.0f) lenSearched += totalLen; pts = path.pts.data; cmds = path.cmds.data; cmdsCnt = path.cmds.count; currentLen = 0.0f; //linear interpolation } else { if (cmds >= path.cmds.data + path.cmds.count - 1) return *start; switch (*(cmds + 1)) { case PathCommand::LineTo: { auto dp = *(pts + 1) - *pts; angle = tvg::atan2(dp.y, dp.x); return {pts->x + lenSearched * cos(angle), pts->y + lenSearched * sin(angle)}; } case PathCommand::CubicTo: { angle = deg2rad(Bezier{*pts, *(pts + 1), *(pts + 2), *(pts + 3)}.angle(0.0001f)); return {pts->x + lenSearched * cos(angle), pts->y + lenSearched * sin(angle)}; } default: angle = 0.0f; return *start; } } } 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; }; //position beyond the end of the curve if (lenSearched >= totalLen) { //shape is closed -> wrapping if (path.cmds.last() == PathCommand::Close) { while (lenSearched > totalLen) 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)}; } } } } //reset required if text partially crosses curve start if (lenSearched < currentLen) { pts = path.pts.data; cmds = path.cmds.data; cmdsCnt = path.cmds.count; currentLen = 0.0f; } 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); default: return 0.0f; } }; 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; auto shallow = pairs.count == 1 ? true : false; ARRAY_FOREACH(pair, pairs) { pair->obj->override(pair->prop, shallow, true); delete(pair->prop); pair->prop = nullptr; } overridden = false; } void LottieSlot::assign(LottieObject* target, bool byDefault) { auto copy = !overridden && !byDefault; auto shallow = pairs.count == 1 ? true : false; //apply slot object to all targets ARRAY_FOREACH(pair, pairs) { //backup the original properties before overwriting switch (type) { case LottieProperty::Type::Float: { if (copy) pair->prop = new LottieFloat(static_cast(pair->obj)->rotation); pair->obj->override(&static_cast(target)->rotation, shallow, !copy); break; } case LottieProperty::Type::Scalar: { if (copy) pair->prop = new LottieScalar(static_cast(pair->obj)->scale); pair->obj->override(&static_cast(target)->scale, shallow, !copy); break; } case LottieProperty::Type::Vector: { if (copy) pair->prop = new LottieVector(static_cast(pair->obj)->position); pair->obj->override(&static_cast(target)->position, shallow, !copy); break; } case LottieProperty::Type::Color: { if (copy) pair->prop = new LottieColor(static_cast(pair->obj)->color); pair->obj->override(&static_cast(target)->color, shallow, !copy); break; } case LottieProperty::Type::Opacity: { if (copy) { if (pair->obj->type == LottieObject::Type::Transform) pair->prop = new LottieOpacity(static_cast(pair->obj)->opacity); else pair->prop = new LottieOpacity(static_cast(pair->obj)->opacity); } pair->obj->override(&static_cast(target)->opacity, shallow, !copy); break; } case LottieProperty::Type::ColorStop: { if (copy) pair->prop = new LottieColorStop(static_cast(pair->obj)->colorStops); pair->obj->override(&static_cast(target)->colorStops, shallow, !copy); break; } case LottieProperty::Type::TextDoc: { if (copy) pair->prop = new LottieTextDoc(static_cast(pair->obj)->doc); pair->obj->override(&static_cast(target)->doc, shallow, !copy); break; } case LottieProperty::Type::Image: { if (copy) pair->prop = new LottieBitmap(static_cast(pair->obj)->data); pair->obj->override(&static_cast(target)->data, shallow, !copy); break; } default: break; } } if (!byDefault) overridden = true; } float LottieTextRange::factor(float frameNo, float totalLen, float idx) { auto offset = this->offset(frameNo); auto start = this->start(frameNo) + offset; auto end = this->end(frameNo) + offset; if (random > 0) { auto range = end - start; auto len = (rangeUnit == Unit::Percent) ? 100.0f : totalLen; start = static_cast(random % int(len - range)); end = start + range; } auto divisor = (rangeUnit == Unit::Percent) ? (100.0f / totalLen) : 1.0f; start /= divisor; end /= divisor; auto f = 0.0f; switch (this->shape) { case Square: { auto smoothness = this->smoothness(frameNo); if (tvg::zero(smoothness)) f = idx >= nearbyintf(start) && idx < nearbyintf(end) ? 1.0f : 0.0f; else { if (idx >= std::floor(start)) { auto diff = idx - start; f = diff < 0.0f ? std::min(end, 1.0f) + diff : end - idx; } smoothness *= 0.01f; f = (f - (1.0f - smoothness) * 0.5f) / smoothness; } break; } case RampUp: { f = tvg::equal(start, end) ? (idx >= end ? 1.0f : 0.0f) : (0.5f + idx - start) / (end - start); break; } case RampDown: { f = tvg::equal(start, end) ? (idx >= end ? 0.0f : 1.0f) : 1.0f - (0.5f + idx - start) / (end - start); break; } case Triangle: { f = tvg::equal(start, end) ? 0.0f : 2.0f * (0.5f + idx - start) / (end - start); f = f < 1.0f ? f : 2.0f - f; break; } case Round: { idx = tvg::clamp(idx + (0.5f - start), 0.0f, end - start); auto range = 0.5f * (end - start); auto t = idx - range; f = tvg::equal(start, end) ? 0.0f : sqrtf(1.0f - t * t / (range * range)); break; } case Smooth: { idx = tvg::clamp(idx + (0.5f - start), 0.0f, end - start); f = tvg::equal(start, end) ? 0.0f : 0.5f * (1.0f + cosf(MATH_PI * (1.0f + 2.0f * idx / (end - start)))); break; } } f = tvg::clamp(f, 0.0f, 1.0f); //apply easing auto minEase = tvg::clamp(this->minEase(frameNo), -100.0f, 100.0f); auto maxEase = tvg::clamp(this->maxEase(frameNo), -100.0f, 100.0f); if (!tvg::zero(minEase) || !tvg::zero(maxEase)) { Point in{1.0f, 1.0f}; Point out{0.0f, 0.0f}; if (maxEase > 0.0f) in.x = 1.0f - maxEase * 0.01f; else in.y = 1.0f + maxEase * 0.01f; if (minEase > 0.0f) out.x = minEase * 0.01f; else out.y = -minEase * 0.01f; interpolator->set(nullptr, in, out); f = interpolator->progress(f); } f = tvg::clamp(f, 0.0f, 1.0f); return f * this->maxAmount(frameNo) * 0.01f; } void LottieFont::prepare() { if (!data.b64src || !name) return; Text::load(name, data.b64src, data.size, "ttf", false); } void LottieImage::prepare() { LottieObject::type = LottieObject::Image; auto picture = Picture::gen(); //force to load a picture on the same thread if (data.size > 0) picture->load((const char*)data.b64Data, data.size, data.mimeType); else picture->load(data.path); picture->size(data.width, data.height); picture->ref(); pooler.push(picture); } void LottieImage::update() { //Update the picture data ARRAY_FOREACH(p, pooler) { if (data.size > 0) (*p)->load((const char*)data.b64Data, data.size, data.mimeType); else (*p)->load(data.path); (*p)->size(data.width, data.height); } } void LottieTrimpath::segment(float frameNo, float& start, float& end, Tween& tween, LottieExpressions* exps) { start = tvg::clamp(this->start(frameNo, tween, exps) * 0.01f, 0.0f, 1.0f); end = tvg::clamp(this->end(frameNo, tween, exps) * 0.01f, 0.0f, 1.0f); auto diff = fabs(start - end); if (tvg::zero(diff)) { start = 0.0f; end = 0.0f; return; } //Even if the start and end values do not cause trimming, an offset > 0 can still affect dashing starting point auto o = fmodf(this->offset(frameNo, tween, exps), 360.0f) / 360.0f; //0 ~ 1 if (tvg::zero(o) && diff >= 1.0f) { start = 0.0f; end = 1.0f; return; } if (start > end) std::swap(start, end); start += o; end += o; } uint32_t LottieGradient::populate(ColorStop& color, size_t count) { if (!color.input) return 0; uint32_t alphaCnt = (color.input->count - (count * 4)) / 2; Array output(count + alphaCnt); uint32_t cidx = 0; //color count uint32_t clast = count * 4; if (clast > color.input->count) clast = color.input->count; uint32_t aidx = clast; //alpha count Fill::ColorStop cs; //merge color stops. for (uint32_t i = 0; i < color.input->count; ++i) { if (cidx == clast || aidx == color.input->count) break; if ((*color.input)[cidx] == (*color.input)[aidx]) { cs.offset = (*color.input)[cidx]; cs.r = (uint8_t)nearbyint((*color.input)[cidx + 1] * 255.0f); cs.g = (uint8_t)nearbyint((*color.input)[cidx + 2] * 255.0f); cs.b = (uint8_t)nearbyint((*color.input)[cidx + 3] * 255.0f); cs.a = (uint8_t)nearbyint((*color.input)[aidx + 1] * 255.0f); cidx += 4; aidx += 2; } else if ((*color.input)[cidx] < (*color.input)[aidx]) { cs.offset = (*color.input)[cidx]; cs.r = (uint8_t)nearbyint((*color.input)[cidx + 1] * 255.0f); cs.g = (uint8_t)nearbyint((*color.input)[cidx + 2] * 255.0f); cs.b = (uint8_t)nearbyint((*color.input)[cidx + 3] * 255.0f); //generate alpha value if (output.count > 0) { auto p = ((*color.input)[cidx] - output.last().offset) / ((*color.input)[aidx] - output.last().offset); cs.a = tvg::lerp(output.last().a, (uint8_t)nearbyint((*color.input)[aidx + 1] * 255.0f), p); } else cs.a = (uint8_t)nearbyint((*color.input)[aidx + 1] * 255.0f); cidx += 4; } else { cs.offset = (*color.input)[aidx]; cs.a = (uint8_t)nearbyint((*color.input)[aidx + 1] * 255.0f); //generate color value if (output.count > 0) { auto p = ((*color.input)[aidx] - output.last().offset) / ((*color.input)[cidx] - output.last().offset); cs.r = tvg::lerp(output.last().r, (uint8_t)nearbyint((*color.input)[cidx + 1] * 255.0f), p); cs.g = tvg::lerp(output.last().g, (uint8_t)nearbyint((*color.input)[cidx + 2] * 255.0f), p); cs.b = tvg::lerp(output.last().b, (uint8_t)nearbyint((*color.input)[cidx + 3] * 255.0f), p); } else { cs.r = (uint8_t)nearbyint((*color.input)[cidx + 1] * 255.0f); cs.g = (uint8_t)nearbyint((*color.input)[cidx + 2] * 255.0f); cs.b = (uint8_t)nearbyint((*color.input)[cidx + 3] * 255.0f); } aidx += 2; } if (cs.a < 255) opaque = false; output.push(cs); } //color remains while (cidx + 3 < clast) { cs.offset = (*color.input)[cidx]; cs.r = (uint8_t)nearbyint((*color.input)[cidx + 1] * 255.0f); cs.g = (uint8_t)nearbyint((*color.input)[cidx + 2] * 255.0f); cs.b = (uint8_t)nearbyint((*color.input)[cidx + 3] * 255.0f); cs.a = (output.count > 0) ? output.last().a : 255; if (cs.a < 255) opaque = false; output.push(cs); cidx += 4; } //alpha remains while (aidx < color.input->count) { cs.offset = (*color.input)[aidx]; cs.a = (uint8_t)nearbyint((*color.input)[aidx + 1] * 255.0f); if (cs.a < 255) opaque = false; if (output.count > 0) { cs.r = output.last().r; cs.g = output.last().g; cs.b = output.last().b; } else cs.r = cs.g = cs.b = 255; output.push(cs); aidx += 2; } color.data = output.data; output.data = nullptr; color.input->reset(); delete(color.input); return output.count; } Fill* LottieGradient::fill(float frameNo, uint8_t opacity, Tween& tween, LottieExpressions* exps) { if (opacity == 0) return nullptr; Fill* fill; auto s = start(frameNo, tween, exps); auto e = end(frameNo, tween, exps); //Linear Graident if (id == 1) { fill = LinearGradient::gen(); static_cast(fill)->linear(s.x, s.y, e.x, e.y); //Radial Gradient } else { fill = RadialGradient::gen(); auto w = fabsf(e.x - s.x); auto h = fabsf(e.y - s.y); auto r = (w > h) ? (w + 0.375f * h) : (h + 0.375f * w); auto progress = this->height(frameNo, tween, exps) * 0.01f; if (tvg::zero(progress)) { static_cast(fill)->radial(s.x, s.y, r, s.x, s.y, 0.0f); } else { auto startAngle = rad2deg(tvg::atan2(e.y - s.y, e.x - s.x)); auto angle = deg2rad((startAngle + this->angle(frameNo, tween, exps))); auto fx = s.x + cos(angle) * progress * r; auto fy = s.y + sin(angle) * progress * r; // Lottie doesn't have any focal radius concept static_cast(fill)->radial(s.x, s.y, r, fx, fy, 0.0f); } } colorStops(frameNo, fill, tween, exps); //multiply the current opacity with the fill if (opacity < 255) { const Fill::ColorStop* colorStops; auto cnt = fill->colorStops(&colorStops); for (uint32_t i = 0; i < cnt; ++i) { const_cast(&colorStops[i])->a = MULTIPLY(colorStops[i].a, opacity); } } return fill; } LottieGroup::LottieGroup() { reqFragment = false; buildDone = false; trimpath = false; visible = false; allowMerge = true; } LottieProperty* LottieGroup::property(uint16_t ix) { ARRAY_FOREACH(p, children) { auto child = static_cast(*p); if (auto property = child->property(ix)) return property; } return nullptr; } void LottieGroup::prepare(LottieObject::Type type) { LottieObject::type = type; if (children.count == 0) return; size_t strokeCnt = 0; size_t fillCnt = 0; ARRAY_REVERSE_FOREACH(c, children) { auto child = static_cast(*c); if (child->type == LottieObject::Type::Trimpath) trimpath = true; /* Figure out if this group is a simple path drawing. In that case, the rendering context can be sharable with the parent's. */ if (allowMerge && !child->mergeable()) allowMerge = false; //Figure out this group has visible contents switch (child->type) { case LottieObject::Group: { visible |= static_cast(child)->visible; break; } case LottieObject::Rect: case LottieObject::Ellipse: case LottieObject::Path: case LottieObject::Polystar: case LottieObject::Image: case LottieObject::Text: { visible = true; break; } default: break; } if (reqFragment) continue; /* Figure out if the rendering context should be fragmented. Multiple stroking or grouping with a stroking would occur this. This fragment resolves the overlapped stroke outlines. */ if (child->type == LottieObject::Group && !child->mergeable()) { if (strokeCnt > 0 || fillCnt > 0) reqFragment = true; } else if (child->type == LottieObject::SolidStroke || child->type == LottieObject::GradientStroke) { if (strokeCnt > 0) reqFragment = true; else ++strokeCnt; } else if (child->type == LottieObject::SolidFill || child->type == LottieObject::GradientFill) { if (fillCnt > 0) reqFragment = true; else ++fillCnt; } } //Reverse the drawing order if this group has a trimpath. if (!trimpath) return; for (uint32_t i = 0; i < children.count - 1; ) { auto child2 = children[i + 1]; if (!child2->mergeable() || child2->type == LottieObject::Transform) { i += 2; continue; } auto child = children[i]; if (!child->mergeable() || child->type == LottieObject::Transform) { i++; continue; } children[i] = child2; children[i + 1] = child; i++; } } LottieLayer::~LottieLayer() { //No need to free assets children because the Composition owns them. if (rid) children.clear(); ARRAY_FOREACH(p, masks) delete(*p); ARRAY_FOREACH(p, effects) delete(*p); delete(transform); tvg::free(name); } LottieProperty* LottieLayer::property(uint16_t ix) { if (transform) { if (auto property = transform->property(ix)) return property; } return LottieGroup::property(ix); } void LottieLayer::prepare(RGB32* color) { /* if layer is hidden, only useful data is its transform matrix. so force it to be a Null Layer and release all resource. */ if (hidden) { type = LottieLayer::Null; ARRAY_FOREACH(p, children) delete(*p); children.reset(); return; } //prepare the viewport clipper if (type == LottieLayer::Precomp) { auto clipper = Shape::gen(); clipper->appendRect(0.0f, 0.0f, w, h); clipper->ref(); statical.pooler.push(clipper); //prepare solid fill in advance if it is a layer type. } else if (color && type == LottieLayer::Solid) { auto solidFill = Shape::gen(); solidFill->appendRect(0, 0, static_cast(w), static_cast(h)); solidFill->fill(color->r, color->g, color->b); solidFill->ref(); statical.pooler.push(solidFill); } LottieGroup::prepare(LottieObject::Layer); } float LottieLayer::remap(LottieComposition* comp, float frameNo, LottieExpressions* exp) { if (timeRemap.frames || timeRemap.value >= 0.0f) { frameNo = comp->frameAtTime(timeRemap(frameNo, exp)); } else { frameNo -= startFrame; } return (frameNo / timeStretch); } bool LottieLayer::assign(const char* layer, uint32_t ix, const char* var, float val) { //find the target layer by name auto target = layerById(djb2Encode(layer)); if (!target) return false; //find the target property by ix auto property = target->property(ix); if (property && property->exp) return property->exp->assign(var, val); return false; } LottieComposition::~LottieComposition() { if (!initiated && root) delete(root->scene); delete(root); tvg::free(version); tvg::free(name); ARRAY_FOREACH(p, interpolators) { tvg::free((*p)->key); tvg::free(*p); } ARRAY_FOREACH(p, assets) delete(*p); ARRAY_FOREACH(p, fonts) delete(*p); ARRAY_FOREACH(p, slots) delete(*p); ARRAY_FOREACH(p, markers) delete(*p); }