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]); + } } //************************************************************************