renderer: improve radial gradient handling

This change ensures at the api level that if the focal
point lies outside the end circle, it is projected onto
the edge of the end circle.
Additionally, if the start circle does not fully fit
inside the end circle (after possible repositioning), its
radius is reduced accordingly.

The modification aligns with the SVG 1.1 standard (for fr = 0).
Cases with fr > 0 are not covered by the SVG 1.1, and edge
cases have been handled here to avoid numerical issues.

Note:
This update replaces previous behavior where gl handled
the SVG 2.0 standard.
This commit is contained in:
Mira Grudzinska 2025-06-23 14:38:33 +02:00 committed by Hermet Park
parent 22742863f4
commit 30662afba7
7 changed files with 51 additions and 28 deletions

View file

@ -886,6 +886,8 @@ public:
* @retval Result::InvalidArguments in case the radius @p r or @p fr value is negative.
*
* @note In case the radius @p r is zero, an object is filled with a single color using the last color specified in the colorStops().
* @note In case the focal point (@p fx and @p fy) lies outside the end circle, it is projected onto the edge of the end circle.
* @note If the start circle doesn't fully fit inside the end circle (after possible repositioning), the @p fr is reduced accordingly.
* @note By manipulating the position and size of the focal point, a wide range of visual effects can be achieved, such as directing
* the gradient focus towards a specific edge or enhancing the depth and complexity of shading patterns.
* If a focal effect is not desired, simply align the focal point (@p fx and @p fy) with the center of the end circle (@p cx and @p cy)

View file

@ -1663,6 +1663,8 @@ TVG_API Tvg_Result tvg_linear_gradient_get(Tvg_Gradient* grad, float* x1, float*
* @retval TVG_RESULT_INVALID_ARGUMENT An invalid Tvg_Gradient pointer or the radius @p r or @p fr value is negative.
*
* @note In case the radius @p r is zero, an object is filled with a single color using the last color specified in the specified in the tvg_gradient_set_color_stops().
* @note In case the focal point (@p fx and @p fy) lies outside the end circle, it is projected onto the edge of the end circle.
* @note If the start circle doesn't fully fit inside the end circle (after possible repositioning), the @p fr is reduced accordingly.
* @note By manipulating the position and size of the focal point, a wide range of visual effects can be achieved, such as directing
* the gradient focus towards a specific edge or enhancing the depth and complexity of shading patterns.
* If a focal effect is not desired, simply align the focal point (@p fx and @p fy) with the center of the end circle (@p cx and @p cy)

View file

@ -519,7 +519,8 @@ Fill* LottieGradient::fill(float frameNo, uint8_t opacity, Tween& tween, LottieE
if (tvg::zero(progress)) {
static_cast<RadialGradient*>(fill)->radial(s.x, s.y, r, s.x, s.y, 0.0f);
} else {
if (tvg::equal(progress, 1.0f)) progress = 0.99f;
//TODO: apply if SVG2.0 std is applied to the radial gradient in the engines
//progress = tvg::clamp(progress, -0.99f, 0.99f);
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;

View file

@ -27,6 +27,7 @@
#include "tvgGlRenderTask.h"
#include "tvgGlProgram.h"
#include "tvgGlShaderSrc.h"
#include "tvgFill.h"
/************************************************************************/
/* Internal Class Implementation */
@ -412,6 +413,7 @@ void GlRenderer::drawPrimitive(GlShape& sdata, const Fill* fill, RenderUpdateFla
float x, y, r, fx, fy, fr;
radialFill->radial(&x, &y, &r, &fx, &fy, &fr);
CONST_RADIAL(radialFill)->correct(fx, fy, fr);
gradientBlock.centerPos[0] = fx;
gradientBlock.centerPos[1] = fy;

View file

@ -245,11 +245,7 @@ bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix& pT
{
float cx, cy, r, fx, fy, fr;
radial->radial(&cx, &cy, &r, &fx, &fy, &fr);
if (tvg::zero(r)) {
fill->solid = true;
return true;
}
if ((fill->solid = !CONST_RADIAL(radial)->correct(fx, fy, fr))) return true;
fill->radial.dr = r - fr;
fill->radial.dx = cx - fx;
@ -258,28 +254,9 @@ bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix& pT
fill->radial.fx = fx;
fill->radial.fy = fy;
fill->radial.a = fill->radial.dr * fill->radial.dr - fill->radial.dx * fill->radial.dx - fill->radial.dy * fill->radial.dy;
//This condition fulfills the SVG 1.1 std:
//the focal point, if outside the end circle, is moved to be on the end circle
//See: the SVG 2 std requirements: https://www.w3.org/TR/SVG2/pservers.html#RadialGradientNotes
constexpr float precision = 0.01f;
if (fill->radial.a <= precision) {
auto dist = sqrtf(fill->radial.dx * fill->radial.dx + fill->radial.dy * fill->radial.dy);
//retract focal point slightly from edge to avoid numerical errors:
fill->radial.fx = cx + r * (1.0f - precision) * (fx - cx) / dist;
fill->radial.fy = cy + r * (1.0f - precision) * (fy - cy) / dist;
fill->radial.dx = cx - fill->radial.fx;
fill->radial.dy = cy - fill->radial.fy;
// Prevent loss of precision on Apple Silicon when dr=dy and dx=0 due to FMA
// https://github.com/thorvg/thorvg/issues/2014
auto dr2 = fill->radial.dr * fill->radial.dr;
auto dx2 = fill->radial.dx * fill->radial.dx;
auto dy2 = fill->radial.dy * fill->radial.dy;
fill->radial.a = dr2 - dx2 - dy2;
}
if (fill->radial.a > 0) fill->radial.invA = 1.0f / fill->radial.a;
if (fill->radial.a < precision) fill->radial.a = precision;
fill->radial.invA = 1.0f / fill->radial.a;
const auto& transform = pTransform * radial->transform();

View file

@ -130,6 +130,42 @@ struct RadialGradientImpl : RadialGradient
return Result::Success;
}
//TODO: remove this logic once SVG 2.0 is adopted by sw and wg engines (gl already supports it); lottie-specific handling will then be delegated entirely to the loader
//clamp focal point and shrink start circle if needed to avoid invalid gradient setup
bool correct(float& fx, float& fy, float& fr) const
{
constexpr float precision = 0.01f;
//a solid fill case. It can be handled by engine.
if (this->r < precision) return false;
fx = this->fx;
fy = this->fy;
fr = this->fr;
auto dx = this->cx - this->fx;
auto dy = this->cy - this->fy;
auto dist = sqrtf(dx * dx + dy * dy);
//move the focal point to the edge (just inside) if it's outside the end circle
if (this->r - dist < precision) {
//handle special case: small radius and small distance -> shift focal point to avoid div-by-zero
if (dist < precision) dist = dx = precision;
auto scale = this->r * (1.0f - precision) / dist;
fx = this->cx - dx * scale;
fy = this->cy - dy * scale;
dx = this->cx - fx;
dy = this->cy - fy;
dist = sqrtf(dx * dx + dy * dy);
}
//if the start circle doesn't fit entirely within the end circle, shrink it (with epsilon margin)
auto maxFr = (this->r - dist) * (1.0f - precision);
if (this->fr > maxFr) fr = std::max(0.0f, maxFr);
return true;
}
};

View file

@ -23,6 +23,7 @@
#include "tvgWgShaderTypes.h"
#include <cassert>
#include "tvgMath.h"
#include "tvgFill.h"
//************************************************************************
// WgShaderTypeMat4x4f
@ -139,8 +140,10 @@ void WgShaderTypeGradSettings::update(const Fill* fill)
// update gradient base points
if (fill->type() == Type::LinearGradient)
((LinearGradient*)fill)->linear(&coords.vec[0], &coords.vec[1], &coords.vec[2], &coords.vec[3]);
else if (fill->type() == Type::RadialGradient)
else if (fill->type() == Type::RadialGradient) {
((RadialGradient*)fill)->radial(&coords.vec[0], &coords.vec[1], &coords.vec[2], &focal.vec[0], &focal.vec[1], &focal.vec[2]);
CONST_RADIAL(fill)->correct(focal.vec[0], focal.vec[1], focal.vec[2]);
}
}
//************************************************************************