From 30662afba769676d591444174927ade3b6cbd6fc Mon Sep 17 00:00:00 2001 From: Mira Grudzinska Date: Mon, 23 Jun 2025 14:38:33 +0200 Subject: [PATCH] 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. --- inc/thorvg.h | 2 ++ src/bindings/capi/thorvg_capi.h | 2 ++ src/loaders/lottie/tvgLottieModel.cpp | 3 +- src/renderer/gl_engine/tvgGlRenderer.cpp | 2 ++ src/renderer/sw_engine/tvgSwFill.cpp | 29 ++--------------- src/renderer/tvgFill.h | 36 +++++++++++++++++++++ src/renderer/wg_engine/tvgWgShaderTypes.cpp | 5 ++- 7 files changed, 51 insertions(+), 28 deletions(-) diff --git a/inc/thorvg.h b/inc/thorvg.h index b8cf56e7..f5c2db3a 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -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) diff --git a/src/bindings/capi/thorvg_capi.h b/src/bindings/capi/thorvg_capi.h index 06782744..da8733b7 100644 --- a/src/bindings/capi/thorvg_capi.h +++ b/src/bindings/capi/thorvg_capi.h @@ -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) diff --git a/src/loaders/lottie/tvgLottieModel.cpp b/src/loaders/lottie/tvgLottieModel.cpp index 714a86f4..c8850d85 100644 --- a/src/loaders/lottie/tvgLottieModel.cpp +++ b/src/loaders/lottie/tvgLottieModel.cpp @@ -519,7 +519,8 @@ Fill* LottieGradient::fill(float frameNo, uint8_t opacity, Tween& tween, LottieE if (tvg::zero(progress)) { static_cast(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; diff --git a/src/renderer/gl_engine/tvgGlRenderer.cpp b/src/renderer/gl_engine/tvgGlRenderer.cpp index 56d050a2..a8879c4b 100644 --- a/src/renderer/gl_engine/tvgGlRenderer.cpp +++ b/src/renderer/gl_engine/tvgGlRenderer.cpp @@ -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; diff --git a/src/renderer/sw_engine/tvgSwFill.cpp b/src/renderer/sw_engine/tvgSwFill.cpp index 1f186345..368f68b1 100644 --- a/src/renderer/sw_engine/tvgSwFill.cpp +++ b/src/renderer/sw_engine/tvgSwFill.cpp @@ -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(); diff --git a/src/renderer/tvgFill.h b/src/renderer/tvgFill.h index 86325871..76fe1c00 100644 --- a/src/renderer/tvgFill.h +++ b/src/renderer/tvgFill.h @@ -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; + } }; diff --git a/src/renderer/wg_engine/tvgWgShaderTypes.cpp b/src/renderer/wg_engine/tvgWgShaderTypes.cpp index 7997c408..6616f8c9 100644 --- a/src/renderer/wg_engine/tvgWgShaderTypes.cpp +++ b/src/renderer/wg_engine/tvgWgShaderTypes.cpp @@ -23,6 +23,7 @@ #include "tvgWgShaderTypes.h" #include #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]); + } } //************************************************************************