mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 13:11:19 +01:00
fix(scripting): search first parent transform component to build scri… (#11443) 99ca3a30cc
fix(scripting): search first parent transform component to build script node feature: modulate opacity (#11427) 128d9d61e0 * feature: modulate opacity * fix: clang-format * fix: rust renderer has a no-op modulateOpacity * fix: no-op modulateOpacity for canvas android * feature: modulate opacity on android canvas * fix: rcp ref * fix: missing override * fix: gms * fix: make flutter_renderer match cg one * fix: josh pr feedback * fix: remove CG transparency layer * fix: save modulated gradient up-front * fix: store only one gradient ref * fix: remove specific constructor * fix: use GradDataArray! * fix: expose currentModulatedOpacity * fix: cg_factory modulated opacity value * fix: modulate negative opacity test * fix: verify double modulate negative also clamps Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com> Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
@@ -1 +1 @@
|
||||
620000211e23da8be6c8603bf69f2b4d682817fd
|
||||
99ca3a30cc79d88ccc740e9f1a90f3690660e2e7
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "rive/renderer.hpp"
|
||||
#include "utils/auto_cf.hpp"
|
||||
#include <vector>
|
||||
|
||||
#if defined(RIVE_BUILD_FOR_OSX)
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
@@ -21,6 +22,7 @@ class CGRenderer : public Renderer
|
||||
{
|
||||
protected:
|
||||
CGContextRef m_ctx;
|
||||
std::vector<float> m_opacityStack{1.0f};
|
||||
|
||||
public:
|
||||
CGRenderer(CGContextRef ctx, int width, int height);
|
||||
@@ -29,6 +31,7 @@ public:
|
||||
void save() override;
|
||||
void restore() override;
|
||||
void transform(const Mat2D& transform) override;
|
||||
void modulateOpacity(float opacity) override;
|
||||
void clipPath(RenderPath* path) override;
|
||||
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
||||
void drawImage(const RenderImage*,
|
||||
|
||||
@@ -379,21 +379,38 @@ CGRenderer::CGRenderer(CGContextRef ctx, int width, int height) : m_ctx(ctx)
|
||||
|
||||
CGRenderer::~CGRenderer() { CGContextRestoreGState(m_ctx); }
|
||||
|
||||
void CGRenderer::save() { CGContextSaveGState(m_ctx); }
|
||||
void CGRenderer::save()
|
||||
{
|
||||
CGContextSaveGState(m_ctx);
|
||||
m_opacityStack.push_back(m_opacityStack.back());
|
||||
}
|
||||
|
||||
void CGRenderer::restore() { CGContextRestoreGState(m_ctx); }
|
||||
void CGRenderer::restore()
|
||||
{
|
||||
CGContextRestoreGState(m_ctx);
|
||||
if (m_opacityStack.size() > 1)
|
||||
{
|
||||
m_opacityStack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void CGRenderer::transform(const Mat2D& m)
|
||||
{
|
||||
CGContextConcatCTM(m_ctx, convert(m));
|
||||
}
|
||||
|
||||
void CGRenderer::modulateOpacity(float opacity)
|
||||
{
|
||||
m_opacityStack.back() = std::max(0.0f, m_opacityStack.back() * opacity);
|
||||
}
|
||||
|
||||
void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
||||
{
|
||||
LITE_RTTI_CAST_OR_RETURN(cgpaint, CGRenderPaint*, paint);
|
||||
LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
|
||||
|
||||
cgpaint->apply(m_ctx);
|
||||
CGContextSetAlpha(m_ctx, m_opacityStack.back());
|
||||
|
||||
CGContextBeginPath(m_ctx);
|
||||
CGContextAddPath(m_ctx, cgpath->path());
|
||||
@@ -407,8 +424,9 @@ void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
||||
CGContextSaveGState(m_ctx);
|
||||
CGContextClip(m_ctx);
|
||||
|
||||
// so the gradient modulates with the color's alpha
|
||||
CGContextSetAlpha(m_ctx, cgpaint->opacity());
|
||||
// so the gradient modulates with the color's alpha and the modulated
|
||||
// opacity
|
||||
CGContextSetAlpha(m_ctx, cgpaint->opacity() * m_opacityStack.back());
|
||||
|
||||
sh->draw(m_ctx);
|
||||
CGContextRestoreGState(m_ctx);
|
||||
@@ -438,9 +456,10 @@ void CGRenderer::drawImage(const RenderImage* image,
|
||||
LITE_RTTI_CAST_OR_RETURN(cgimg, const CGRenderImage*, image);
|
||||
|
||||
auto bounds = CGRectMake(0, 0, image->width(), image->height());
|
||||
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||
|
||||
CGContextSaveGState(m_ctx);
|
||||
CGContextSetAlpha(m_ctx, opacity);
|
||||
CGContextSetAlpha(m_ctx, finalOpacity);
|
||||
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
||||
cgimg->applyLocalMatrix(m_ctx);
|
||||
CGContextDrawImage(m_ctx, bounds, cgimg->m_image);
|
||||
@@ -482,13 +501,15 @@ void CGRenderer::drawImageMesh(const RenderImage* image,
|
||||
auto pts = cgvertices->vecs();
|
||||
auto uvs = cguvcoords->vecs();
|
||||
|
||||
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||
|
||||
// We use the path to set the clip for each triangle. Since calling
|
||||
// CGContextClip() resets the path, we only need to this once at
|
||||
// the beginning.
|
||||
CGContextBeginPath(m_ctx);
|
||||
|
||||
CGContextSaveGState(m_ctx);
|
||||
CGContextSetAlpha(m_ctx, opacity);
|
||||
CGContextSetAlpha(m_ctx, finalOpacity);
|
||||
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
||||
CGContextSetShouldAntialias(m_ctx, false);
|
||||
|
||||
|
||||
@@ -220,6 +220,12 @@ public:
|
||||
BlendMode,
|
||||
float opacity) = 0;
|
||||
|
||||
// Modulate the opacity of subsequent draw calls. The opacity is stacked
|
||||
// multiplicatively (e.g., modulateOpacity(0.5) followed by
|
||||
// modulateOpacity(0.2) = 0.1 effective opacity). The modulated opacity is
|
||||
// captured by save() and restored by restore().
|
||||
virtual void modulateOpacity(float opacity) = 0;
|
||||
|
||||
// helpers
|
||||
|
||||
void translate(float x, float y);
|
||||
|
||||
@@ -16,6 +16,7 @@ class RenderPaint;
|
||||
class ShapePaintMutator;
|
||||
class Feather;
|
||||
class ShapePaintContainer;
|
||||
class TransformComponent;
|
||||
class ShapePaint : public ShapePaintBase,
|
||||
public EffectsContainer,
|
||||
public PathProvider
|
||||
@@ -83,6 +84,8 @@ public:
|
||||
void update(ComponentDirt value) override;
|
||||
virtual ShapePaintType paintType() const = 0;
|
||||
|
||||
TransformComponent* parentTransformComponent() const;
|
||||
|
||||
private:
|
||||
Feather* m_feather = nullptr;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
BlendMode,
|
||||
float) override
|
||||
{}
|
||||
void modulateOpacity(float) override {}
|
||||
};
|
||||
|
||||
} // namespace rive
|
||||
|
||||
@@ -195,6 +195,7 @@ public:
|
||||
rcp<const RiveRenderPath>,
|
||||
FillRule,
|
||||
const RiveRenderPaint*,
|
||||
float modulatedOpacity,
|
||||
RawPath* scratchPath);
|
||||
|
||||
// Determines how coverage is calculated for antialiasing and feathers.
|
||||
@@ -216,6 +217,7 @@ public:
|
||||
rcp<const RiveRenderPath>,
|
||||
FillRule,
|
||||
const RiveRenderPaint*,
|
||||
float modulatedOpacity,
|
||||
CoverageType,
|
||||
const RenderContext::FrameDescriptor&);
|
||||
|
||||
@@ -356,7 +358,7 @@ protected:
|
||||
|
||||
const RiveRenderPath* const m_pathRef;
|
||||
const FillRule m_pathFillRule; // Fill rule can mutate on RenderPath.
|
||||
const Gradient* m_gradientRef;
|
||||
const Gradient* m_gradientRef; // Already modulated if opacity != 1.0
|
||||
const gpu::PaintType m_paintType;
|
||||
const CoverageType m_coverageType;
|
||||
float m_strokeRadius = 0;
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
uint32_t indexCount,
|
||||
BlendMode,
|
||||
float opacity) override;
|
||||
void modulateOpacity(float opacity) override;
|
||||
|
||||
// Determines if a path is an axis-aligned rectangle that can be represented
|
||||
// by rive::AABB.
|
||||
@@ -62,6 +63,10 @@ public:
|
||||
{
|
||||
return m_stack.back().clipRectMatrix;
|
||||
}
|
||||
float currentModulatedOpacity() const
|
||||
{
|
||||
return m_stack.back().modulatedOpacity;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
@@ -93,6 +98,7 @@ private:
|
||||
Mat2D clipRectMatrix;
|
||||
const gpu::ClipRectInverseMatrix* clipRectInverseMatrix = nullptr;
|
||||
bool clipIsEmpty = false;
|
||||
float modulatedOpacity = 1.0f;
|
||||
};
|
||||
std::vector<RenderState> m_stack{1};
|
||||
|
||||
|
||||
@@ -422,6 +422,7 @@ DrawUniquePtr PathDraw::Make(RenderContext* context,
|
||||
rcp<const RiveRenderPath> path,
|
||||
FillRule fillRule,
|
||||
const RiveRenderPaint* paint,
|
||||
float modulatedOpacity,
|
||||
RawPath* scratchPath)
|
||||
{
|
||||
RIVE_PROF_SCOPE();
|
||||
@@ -512,6 +513,7 @@ DrawUniquePtr PathDraw::Make(RenderContext* context,
|
||||
std::move(path),
|
||||
fillRule,
|
||||
paint,
|
||||
modulatedOpacity,
|
||||
coverageType,
|
||||
context->frameDescriptor());
|
||||
if (doTriangulation)
|
||||
@@ -536,6 +538,7 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
||||
rcp<const RiveRenderPath> path,
|
||||
FillRule initialFillRule,
|
||||
const RiveRenderPaint* paint,
|
||||
float modulatedOpacity,
|
||||
CoverageType coverageType,
|
||||
const RenderContext::FrameDescriptor& frameDesc) :
|
||||
Draw(pixelBounds,
|
||||
@@ -547,7 +550,7 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
||||
m_pathRef(path.release()),
|
||||
m_pathFillRule(frameDesc.clockwiseFillOverride ? FillRule::clockwise
|
||||
: initialFillRule),
|
||||
m_gradientRef(safe_ref(paint->getGradient())),
|
||||
m_gradientRef(paint->getGradientWithOpacity(modulatedOpacity).release()),
|
||||
m_paintType(paint->getType()),
|
||||
m_coverageType(coverageType)
|
||||
{
|
||||
@@ -685,6 +688,27 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
||||
|
||||
m_simplePaintValue = paint->getSimpleValue();
|
||||
|
||||
// Apply modulated opacity to the paint value.
|
||||
// Gradient modulation is handled upfront in the gradient initialization.
|
||||
if (modulatedOpacity != 1.0f)
|
||||
{
|
||||
switch (m_paintType)
|
||||
{
|
||||
case gpu::PaintType::solidColor:
|
||||
m_simplePaintValue.color =
|
||||
colorModulateOpacity(m_simplePaintValue.color,
|
||||
modulatedOpacity);
|
||||
break;
|
||||
case gpu::PaintType::image:
|
||||
m_simplePaintValue.imageOpacity *= modulatedOpacity;
|
||||
break;
|
||||
case gpu::PaintType::linearGradient:
|
||||
case gpu::PaintType::radialGradient:
|
||||
case gpu::PaintType::clipUpdate:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_coverageType == CoverageType::atlas)
|
||||
{
|
||||
// Reserve two triangles for our on-screen rectangle that reads coverage
|
||||
@@ -1364,11 +1388,13 @@ bool PathDraw::allocateResources(RenderContext::LogicalFlush* flush)
|
||||
|
||||
// Allocate a gradient if needed. Do this first since it's more expensive to
|
||||
// fail after setting up an atlas draw than a gradient draw.
|
||||
if (m_gradientRef != nullptr &&
|
||||
!flush->allocateGradient(m_gradientRef,
|
||||
&m_simplePaintValue.colorRampLocation))
|
||||
if (m_gradientRef != nullptr)
|
||||
{
|
||||
return false;
|
||||
if (!flush->allocateGradient(m_gradientRef,
|
||||
&m_simplePaintValue.colorRampLocation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate a coverage buffer range or atlas region if needed.
|
||||
|
||||
@@ -188,4 +188,39 @@ bool Gradient::isOpaque() const
|
||||
return m_isOpaque == gpu::TriState::yes;
|
||||
}
|
||||
|
||||
rcp<Gradient> Gradient::getModulated(float opacity) const
|
||||
{
|
||||
// Fast path: no modulation needed
|
||||
if (opacity == 1.0f)
|
||||
{
|
||||
return ref_rcp(const_cast<Gradient*>(this));
|
||||
}
|
||||
|
||||
// Check single-entry cache
|
||||
if (m_lastModulatedOpacity == opacity && m_lastModulatedGradient)
|
||||
{
|
||||
return m_lastModulatedGradient;
|
||||
}
|
||||
|
||||
// Create new modulated gradient
|
||||
GradDataArray<ColorInt> newColors(m_count);
|
||||
for (size_t i = 0; i < m_count; ++i)
|
||||
{
|
||||
newColors[i] = colorModulateOpacity(m_colors[i], opacity);
|
||||
}
|
||||
|
||||
GradDataArray<float> newStops(m_stops.get(), m_count);
|
||||
|
||||
m_lastModulatedGradient = rcp(new Gradient(m_paintType,
|
||||
std::move(newColors),
|
||||
std::move(newStops),
|
||||
m_count,
|
||||
m_coeffs[0],
|
||||
m_coeffs[1],
|
||||
m_coeffs[2]));
|
||||
m_lastModulatedOpacity = opacity;
|
||||
|
||||
return m_lastModulatedGradient;
|
||||
}
|
||||
|
||||
} // namespace rive::gpu
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
namespace rive::gpu
|
||||
{
|
||||
|
||||
// Copies an array of colors or stops for a gradient.
|
||||
// Stores the data locally if there are 4 values or fewer.
|
||||
// Spills onto the heap if there are >4 values.
|
||||
@@ -25,6 +26,13 @@ public:
|
||||
memcpy(m_data, data, count * sizeof(T));
|
||||
}
|
||||
|
||||
// Allocate without initializing (caller must fill in data).
|
||||
explicit GradDataArray(size_t count)
|
||||
{
|
||||
m_data =
|
||||
count <= m_localData.size() ? m_localData.data() : new T[count];
|
||||
}
|
||||
|
||||
GradDataArray(GradDataArray&& other)
|
||||
{
|
||||
if (other.m_data == other.m_localData.data())
|
||||
@@ -83,6 +91,11 @@ public:
|
||||
size_t count() const { return m_count; }
|
||||
bool isOpaque() const;
|
||||
|
||||
// Get or create a modulated variant of this gradient.
|
||||
// Caches the last-used modulated gradient for efficient reuse when the same
|
||||
// opacity is requested multiple times (e.g., multiple draws in one frame).
|
||||
rcp<Gradient> getModulated(float opacity) const;
|
||||
|
||||
private:
|
||||
Gradient(PaintType paintType,
|
||||
GradDataArray<ColorInt>&& colors, // [count]
|
||||
@@ -107,6 +120,11 @@ private:
|
||||
size_t m_count;
|
||||
std::array<float, 3> m_coeffs;
|
||||
mutable gpu::TriState m_isOpaque = gpu::TriState::unknown;
|
||||
|
||||
// Single-entry cache for last-used modulated gradient
|
||||
mutable rcp<Gradient> m_lastModulatedGradient;
|
||||
mutable float m_lastModulatedOpacity =
|
||||
-1.0f; // -1 as sentinel (valid range is 0..1)
|
||||
};
|
||||
|
||||
} // namespace rive::gpu
|
||||
|
||||
@@ -31,6 +31,15 @@ void RiveRenderPaint::shader(rcp<RenderShader> shader)
|
||||
m_imageTexture.reset();
|
||||
}
|
||||
|
||||
rcp<gpu::Gradient> RiveRenderPaint::getGradientWithOpacity(float opacity) const
|
||||
{
|
||||
if (m_gradient)
|
||||
{
|
||||
return m_gradient->getModulated(opacity);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RiveRenderPaint::image(rcp<gpu::Texture> imageTexture, float opacity)
|
||||
{
|
||||
m_paintType = gpu::PaintType::image;
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
bool getIsStroked() const { return m_stroked; }
|
||||
ColorInt getColor() const { return m_simpleValue.color; }
|
||||
const gpu::Gradient* getGradient() const { return m_gradient.get(); }
|
||||
rcp<gpu::Gradient> getGradientWithOpacity(float opacity) const;
|
||||
gpu::Texture* getImageTexture() const { return m_imageTexture.get(); }
|
||||
ImageSampler getImageSampler() const { return m_imageSampler; }
|
||||
float getImageOpacity() const { return m_simpleValue.imageOpacity; }
|
||||
|
||||
@@ -108,6 +108,12 @@ void RiveRenderer::transform(const Mat2D& matrix)
|
||||
m_stack.back().matrix = m_stack.back().matrix * matrix;
|
||||
}
|
||||
|
||||
void RiveRenderer::modulateOpacity(float opacity)
|
||||
{
|
||||
m_stack.back().modulatedOpacity =
|
||||
std::max(0.0f, m_stack.back().modulatedOpacity * opacity);
|
||||
}
|
||||
|
||||
void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
||||
{
|
||||
RIVE_PROF_SCOPE()
|
||||
@@ -161,6 +167,7 @@ void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
||||
matrixMaxScale),
|
||||
path->getFillRule(),
|
||||
paint,
|
||||
m_stack.back().modulatedOpacity,
|
||||
&m_scratchPath));
|
||||
return;
|
||||
}
|
||||
@@ -171,6 +178,7 @@ void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
||||
ref_rcp(path),
|
||||
path->getFillRule(),
|
||||
paint,
|
||||
m_stack.back().modulatedOpacity,
|
||||
&m_scratchPath));
|
||||
}
|
||||
|
||||
@@ -343,6 +351,10 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply modulated opacity (clamp to prevent negative values)
|
||||
float finalOpacity =
|
||||
std::max(0.0f, opacity * m_stack.back().modulatedOpacity);
|
||||
|
||||
// Scale the view matrix so we can draw this image as the rect [0, 0, 1, 1].
|
||||
save();
|
||||
scale(image->width(), image->height());
|
||||
@@ -362,7 +374,7 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
||||
blendMode,
|
||||
std::move(imageTexture),
|
||||
imageSampler,
|
||||
opacity)));
|
||||
finalOpacity)));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -378,7 +390,7 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
||||
}
|
||||
|
||||
RiveRenderPaint paint;
|
||||
paint.image(std::move(imageTexture), opacity);
|
||||
paint.image(std::move(imageTexture), finalOpacity);
|
||||
paint.blendMode(blendMode);
|
||||
paint.imageSampler(imageSampler);
|
||||
drawPath(m_unitRectPath.get(), &paint);
|
||||
@@ -418,6 +430,10 @@ void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply modulated opacity (clamp to prevent negative values)
|
||||
float finalOpacity =
|
||||
std::max(0.0f, opacity * m_stack.back().modulatedOpacity);
|
||||
|
||||
clipAndPushDraw(gpu::DrawUniquePtr(
|
||||
m_context->make<gpu::ImageMeshDraw>(gpu::Draw::FULLSCREEN_PIXEL_BOUNDS,
|
||||
m_stack.back().matrix,
|
||||
@@ -428,7 +444,7 @@ void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
|
||||
std::move(uvCoords_f32),
|
||||
std::move(indices_u16),
|
||||
indexCount,
|
||||
opacity)));
|
||||
finalOpacity)));
|
||||
}
|
||||
|
||||
void RiveRenderer::clipAndPushDraw(gpu::DrawUniquePtr draw)
|
||||
@@ -573,12 +589,14 @@ RiveRenderer::ApplyClipResult RiveRenderer::applyClip(gpu::Draw* draw)
|
||||
RiveRenderPaint clipUpdatePaint;
|
||||
clipUpdatePaint.clipUpdate(/*clip THIS clipDraw against:*/ lastClipID);
|
||||
|
||||
gpu::DrawUniquePtr clipDraw = gpu::PathDraw::Make(m_context,
|
||||
clip.matrix,
|
||||
clip.path,
|
||||
clip.fillRule,
|
||||
&clipUpdatePaint,
|
||||
&m_scratchPath);
|
||||
gpu::DrawUniquePtr clipDraw =
|
||||
gpu::PathDraw::Make(m_context,
|
||||
clip.matrix,
|
||||
clip.path,
|
||||
clip.fillRule,
|
||||
&clipUpdatePaint,
|
||||
1.0f, // no opacity modulation for clips
|
||||
&m_scratchPath);
|
||||
|
||||
if (clipDraw == nullptr)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define _RIVE_SKIA_RENDERER_HPP_
|
||||
|
||||
#include "rive/renderer.hpp"
|
||||
#include <vector>
|
||||
|
||||
class SkCanvas;
|
||||
|
||||
@@ -15,12 +16,14 @@ class SkiaRenderer : public Renderer
|
||||
{
|
||||
protected:
|
||||
SkCanvas* m_Canvas;
|
||||
std::vector<float> m_opacityStack{1.0f};
|
||||
|
||||
public:
|
||||
SkiaRenderer(SkCanvas* canvas) : m_Canvas(canvas) {}
|
||||
void save() override;
|
||||
void restore() override;
|
||||
void transform(const Mat2D& transform) override;
|
||||
void modulateOpacity(float opacity) override;
|
||||
void clipPath(RenderPath* path) override;
|
||||
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
||||
void drawImage(const RenderImage*,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "include/core/SkVertices.h"
|
||||
#include "include/effects/SkGradientShader.h"
|
||||
#include "include/effects/SkImageFilters.h"
|
||||
#include "include/core/SkColorFilter.h"
|
||||
|
||||
#include "rive/math/vec2d.hpp"
|
||||
#include "rive/shapes/paint/color.hpp"
|
||||
@@ -218,19 +219,70 @@ void SkiaRenderPaint::shader(rcp<RenderShader> rsh)
|
||||
m_Paint.setShader(sksh ? sksh->shader : nullptr);
|
||||
}
|
||||
|
||||
void SkiaRenderer::save() { m_Canvas->save(); }
|
||||
void SkiaRenderer::restore() { m_Canvas->restore(); }
|
||||
void SkiaRenderer::save()
|
||||
{
|
||||
m_Canvas->save();
|
||||
m_opacityStack.push_back(m_opacityStack.back());
|
||||
}
|
||||
void SkiaRenderer::restore()
|
||||
{
|
||||
m_Canvas->restore();
|
||||
if (m_opacityStack.size() > 1)
|
||||
{
|
||||
m_opacityStack.pop_back();
|
||||
}
|
||||
}
|
||||
void SkiaRenderer::transform(const Mat2D& transform)
|
||||
{
|
||||
m_Canvas->concat(ToSkia::convert(transform));
|
||||
}
|
||||
void SkiaRenderer::modulateOpacity(float opacity)
|
||||
{
|
||||
m_opacityStack.back() = std::max(0.0f, m_opacityStack.back() * opacity);
|
||||
}
|
||||
void SkiaRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
||||
{
|
||||
LITE_RTTI_CAST_OR_RETURN(skiaRenderPath, SkiaRenderPath*, path);
|
||||
LITE_RTTI_CAST_OR_RETURN(skiaRenderPaint, SkiaRenderPaint*, paint);
|
||||
|
||||
SkiaRenderPaint::OverrideStrokeParamsForFeather ospff(skiaRenderPaint);
|
||||
m_Canvas->drawPath(skiaRenderPath->path(), skiaRenderPaint->paint());
|
||||
|
||||
float modulatedOpacity = m_opacityStack.back();
|
||||
if (modulatedOpacity != 1.0f)
|
||||
{
|
||||
// Apply modulated opacity using a color filter on the paint.
|
||||
// This is more efficient than saveLayer as it doesn't allocate
|
||||
// an offscreen buffer.
|
||||
// We scale all color components (RGBA) by opacity since Skia uses
|
||||
// pre-multiplied alpha.
|
||||
SkPaint modulatedPaint(skiaRenderPaint->paint());
|
||||
|
||||
float matrix[20] = {
|
||||
// clang-format off
|
||||
modulatedOpacity, 0, 0, 0, 0, // R
|
||||
0, modulatedOpacity, 0, 0, 0, // G
|
||||
0, 0, modulatedOpacity, 0, 0, // B
|
||||
0, 0, 0, modulatedOpacity, 0, // A
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
auto opacityFilter = SkColorFilters::Matrix(matrix);
|
||||
auto existingFilter = modulatedPaint.refColorFilter();
|
||||
if (existingFilter)
|
||||
{
|
||||
modulatedPaint.setColorFilter(
|
||||
opacityFilter->makeComposed(existingFilter));
|
||||
}
|
||||
else
|
||||
{
|
||||
modulatedPaint.setColorFilter(opacityFilter);
|
||||
}
|
||||
m_Canvas->drawPath(skiaRenderPath->path(), modulatedPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Canvas->drawPath(skiaRenderPath->path(), skiaRenderPaint->paint());
|
||||
}
|
||||
}
|
||||
|
||||
void SkiaRenderer::clipPath(RenderPath* path)
|
||||
@@ -245,8 +297,9 @@ void SkiaRenderer::drawImage(const RenderImage* image,
|
||||
float opacity)
|
||||
{
|
||||
LITE_RTTI_CAST_OR_RETURN(skiaImage, const SkiaRenderImage*, image);
|
||||
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||
SkPaint paint;
|
||||
paint.setAlphaf(opacity);
|
||||
paint.setAlphaf(finalOpacity);
|
||||
paint.setBlendMode(ToSkia::convert(blendMode));
|
||||
m_Canvas->drawImage(skiaImage->skImage(), 0.0f, 0.0f, gSampling, &paint);
|
||||
}
|
||||
@@ -301,8 +354,9 @@ void SkiaRenderer::drawImageMesh(const RenderImage* image,
|
||||
gSampling,
|
||||
&scaleM);
|
||||
|
||||
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||
SkPaint paint;
|
||||
paint.setAlphaf(opacity);
|
||||
paint.setAlphaf(finalOpacity);
|
||||
paint.setBlendMode(ToSkia::convert(blendMode));
|
||||
paint.setShader(shader);
|
||||
|
||||
|
||||
@@ -53,10 +53,9 @@ void ScriptedPathEffect::updateEffect(PathProvider* pathProvider,
|
||||
// Stack: [self, "update", self]
|
||||
lua_newrive<ScriptedPathData>(state, source->rawPath());
|
||||
// Stack: [self, "update", self, pathData]
|
||||
lua_newrive<ScriptedNode>(
|
||||
state,
|
||||
nullptr,
|
||||
shapePaint->parent()->as<TransformComponent>());
|
||||
lua_newrive<ScriptedNode>(state,
|
||||
nullptr,
|
||||
shapePaint->parentTransformComponent());
|
||||
auto scriptedNode = lua_torive<ScriptedNode>(state, -1);
|
||||
scriptedNode->shapePaint(shapePaint);
|
||||
// Stack: [self, "update", self, pathData, node]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "rive/shapes/shape_paint_container.hpp"
|
||||
#include "rive/shapes/paint/feather.hpp"
|
||||
#include "rive/artboard.hpp"
|
||||
#include "rive/transform_component.hpp"
|
||||
#include "rive/factory.hpp"
|
||||
#include "rive/shapes/paint/fill.hpp"
|
||||
#include "rive/profiler/profiler_macros.h"
|
||||
@@ -186,4 +187,18 @@ void ShapePaint::addStrokeEffect(StrokeEffect* effect)
|
||||
{
|
||||
effect->addPathProvider(this);
|
||||
EffectsContainer::addStrokeEffect(effect);
|
||||
}
|
||||
|
||||
TransformComponent* ShapePaint::parentTransformComponent() const
|
||||
{
|
||||
auto _parent = parent();
|
||||
while (_parent)
|
||||
{
|
||||
if (_parent->is<TransformComponent>())
|
||||
{
|
||||
return _parent->as<TransformComponent>();
|
||||
}
|
||||
_parent = _parent->parent();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "rive/tess/sub_path.hpp"
|
||||
#include "rive/math/mat2d.hpp"
|
||||
#include "rive/math/mat4.hpp"
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
|
||||
@@ -19,6 +20,7 @@ struct RenderState
|
||||
{
|
||||
Mat2D transform;
|
||||
std::vector<SubPath> clipPaths;
|
||||
float modulatedOpacity = 1.0f;
|
||||
};
|
||||
|
||||
class TessRenderer : public Renderer
|
||||
@@ -42,7 +44,9 @@ public:
|
||||
void save() override;
|
||||
void restore() override;
|
||||
void transform(const Mat2D& transform) override;
|
||||
void modulateOpacity(float opacity) override;
|
||||
const Mat2D& transform() { return m_Stack.back().transform; }
|
||||
float modulatedOpacity() const { return m_Stack.back().modulatedOpacity; }
|
||||
void clipPath(RenderPath* path) override;
|
||||
void drawImage(const RenderImage*,
|
||||
ImageSampler,
|
||||
|
||||
@@ -28,6 +28,12 @@ void TessRenderer::transform(const Mat2D& transform)
|
||||
stackMat = stackMat * transform;
|
||||
}
|
||||
|
||||
void TessRenderer::modulateOpacity(float opacity)
|
||||
{
|
||||
m_Stack.back().modulatedOpacity =
|
||||
std::max(0.0f, m_Stack.back().modulatedOpacity * opacity);
|
||||
}
|
||||
|
||||
void TessRenderer::clipPath(RenderPath* path)
|
||||
{
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ public:
|
||||
void save() override {}
|
||||
void restore() override {}
|
||||
void transform(const Mat2D& matrix) override {}
|
||||
void modulateOpacity(float) override {}
|
||||
void drawPath(RenderPath* path, RenderPaint* paint) override
|
||||
{
|
||||
auto renderPath = static_cast<RiveRenderPath*>(path);
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
? FillRule::clockwise
|
||||
: FillRule::nonZero,
|
||||
paint,
|
||||
1.0f, // modulatedOpacity
|
||||
SelectCoverageType(paint,
|
||||
1,
|
||||
context->platformFeatures(),
|
||||
|
||||
518
tests/unit_tests/renderer/modulate_opacity_test.cpp
Normal file
518
tests/unit_tests/renderer/modulate_opacity_test.cpp
Normal file
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* Copyright 2025 Rive
|
||||
*/
|
||||
|
||||
#include "common/render_context_null.hpp"
|
||||
#include "rive/renderer/rive_renderer.hpp"
|
||||
#include "rive/shapes/paint/color.hpp"
|
||||
#include "rive/math/raw_path.hpp"
|
||||
#include "gradient.hpp"
|
||||
#include <catch.hpp>
|
||||
|
||||
using namespace rive;
|
||||
using namespace rive::gpu;
|
||||
|
||||
static RenderContext::FrameDescriptor s_frameDescriptor = {
|
||||
.renderTargetWidth = 100,
|
||||
.renderTargetHeight = 100,
|
||||
};
|
||||
|
||||
// Helper to create a rectangular RawPath
|
||||
static RawPath& make_rect(const AABB& bounds)
|
||||
{
|
||||
static RawPath path;
|
||||
path.rewind();
|
||||
path.addRect(bounds);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Helper to create an oval RawPath
|
||||
static RawPath& make_oval(const AABB& bounds)
|
||||
{
|
||||
static RawPath path;
|
||||
path.rewind();
|
||||
path.addOval(bounds);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Helper to create a line path
|
||||
static RawPath& make_line(float x1, float y1, float x2, float y2)
|
||||
{
|
||||
static RawPath path;
|
||||
path.rewind();
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Helper to flush a frame
|
||||
static void flushFrame(RenderContext* ctx)
|
||||
{
|
||||
auto renderTarget =
|
||||
ctx->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
|
||||
s_frameDescriptor.renderTargetWidth,
|
||||
s_frameDescriptor.renderTargetHeight);
|
||||
ctx->flush({.renderTarget = renderTarget.get()});
|
||||
}
|
||||
|
||||
// Test that modulateOpacity stacks correctly with save/restore.
|
||||
TEST_CASE("modulate-opacity-save-restore", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
// Initial opacity should be 1.0
|
||||
CHECK(renderer.currentModulatedOpacity() == 1.0f);
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
|
||||
// Nested save/restore should multiply opacity
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.5f);
|
||||
// Now effective opacity should be 0.5 * 0.5 = 0.25
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.25f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
// After restore, opacity should be back to 0.5
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
// After final restore, opacity should be back to 1.0
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(1.0f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that multiple calls to modulateOpacity within the same save level
|
||||
// multiply together.
|
||||
TEST_CASE("modulate-opacity-multiple-calls", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.25f));
|
||||
renderer.modulateOpacity(0.5f);
|
||||
// Effective opacity should be 0.5 * 0.5 * 0.5 = 0.125
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.125f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that opacity of 1.0 doesn't modify anything (fast path).
|
||||
TEST_CASE("modulate-opacity-identity", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
// Calling modulateOpacity(1.0f) should have no effect
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(1.0f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(1.0f));
|
||||
renderer.modulateOpacity(1.0f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(1.0f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that opacity of 0 results in fully transparent draw.
|
||||
TEST_CASE("modulate-opacity-zero", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.0f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.0f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test Gradient::getModulated returns the same gradient when opacity is 1.0.
|
||||
TEST_CASE("gradient-modulated-identity", "[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0xffff0000, 0xff00ff00, 0xff0000ff};
|
||||
float stops[] = {0.0f, 0.5f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeLinear(0, 0, 100, 0, colors, stops, 3);
|
||||
REQUIRE(gradient != nullptr);
|
||||
|
||||
// Modulating with 1.0 should return the same gradient instance
|
||||
auto modulated = gradient->getModulated(1.0f);
|
||||
CHECK(modulated.get() == gradient.get());
|
||||
}
|
||||
|
||||
// Test Gradient::getModulated creates a new gradient with modulated colors.
|
||||
TEST_CASE("gradient-modulated-colors", "[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0xffff0000, 0xff00ff00};
|
||||
float stops[] = {0.0f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeLinear(0, 0, 100, 0, colors, stops, 2);
|
||||
REQUIRE(gradient != nullptr);
|
||||
|
||||
// Modulating with 0.5 should create a new gradient
|
||||
auto modulated = gradient->getModulated(0.5f);
|
||||
CHECK(modulated.get() != gradient.get());
|
||||
|
||||
// The modulated gradient should have reduced alpha
|
||||
// 255 * 0.5 = 127.5, which rounds to 128
|
||||
const ColorInt* modColors = modulated->colors();
|
||||
CHECK(colorAlpha(modColors[0]) == 128);
|
||||
CHECK(colorAlpha(modColors[1]) == 128);
|
||||
}
|
||||
|
||||
// Test Gradient::getModulated caches the last-used modulated gradient.
|
||||
TEST_CASE("gradient-modulated-caching", "[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0xffff0000, 0xff00ff00};
|
||||
float stops[] = {0.0f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeLinear(0, 0, 100, 0, colors, stops, 2);
|
||||
REQUIRE(gradient != nullptr);
|
||||
|
||||
// First call with 0.5 creates a new modulated gradient
|
||||
auto modulated1 = gradient->getModulated(0.5f);
|
||||
CHECK(modulated1.get() != gradient.get());
|
||||
|
||||
// Second call with same opacity should return the cached version
|
||||
auto modulated2 = gradient->getModulated(0.5f);
|
||||
CHECK(modulated2.get() == modulated1.get());
|
||||
|
||||
// Different opacity should create a different gradient (replaces cache)
|
||||
auto modulated3 = gradient->getModulated(0.25f);
|
||||
CHECK(modulated3.get() != modulated1.get());
|
||||
|
||||
// Calling with 0.25 again returns cached version
|
||||
auto modulated4 = gradient->getModulated(0.25f);
|
||||
CHECK(modulated4.get() == modulated3.get());
|
||||
|
||||
// Calling with 0.5 again creates a new gradient (cache was replaced)
|
||||
auto modulated5 = gradient->getModulated(0.5f);
|
||||
CHECK(modulated5.get() != modulated1.get()); // New instance
|
||||
CHECK(modulated5.get() != modulated3.get()); // Different from 0.25 version
|
||||
}
|
||||
|
||||
// Test that modulated gradients preserve stops.
|
||||
TEST_CASE("gradient-modulated-preserves-stops",
|
||||
"[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0xffff0000, 0xff00ff00, 0xff0000ff};
|
||||
float stops[] = {0.0f, 0.3f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeLinear(0, 0, 100, 0, colors, stops, 3);
|
||||
REQUIRE(gradient != nullptr);
|
||||
|
||||
auto modulated = gradient->getModulated(0.5f);
|
||||
REQUIRE(modulated != nullptr);
|
||||
|
||||
// Count should be preserved
|
||||
CHECK(modulated->count() == gradient->count());
|
||||
|
||||
// Stops should be preserved (may be normalized but relative order kept)
|
||||
const float* modStops = modulated->stops();
|
||||
CHECK(modStops[0] <= modStops[1]);
|
||||
CHECK(modStops[1] <= modStops[2]);
|
||||
}
|
||||
|
||||
// Test radial gradient modulation.
|
||||
TEST_CASE("gradient-radial-modulated", "[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0x80ff0000, 0x80ffffff};
|
||||
float stops[] = {0.0f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeRadial(50, 50, 50, colors, stops, 2);
|
||||
REQUIRE(gradient != nullptr);
|
||||
CHECK(gradient->paintType() == PaintType::radialGradient);
|
||||
|
||||
auto modulated = gradient->getModulated(0.5f);
|
||||
REQUIRE(modulated != nullptr);
|
||||
CHECK(modulated->paintType() == PaintType::radialGradient);
|
||||
|
||||
// Original alpha was 0x80 (128), modulating by 0.5 gives ~64
|
||||
const ColorInt* modColors = modulated->colors();
|
||||
CHECK(colorAlpha(modColors[0]) == 64);
|
||||
CHECK(colorAlpha(modColors[1]) == 64);
|
||||
}
|
||||
|
||||
// Test that drawing with gradient and modulated opacity works.
|
||||
TEST_CASE("draw-path-gradient-modulated", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
|
||||
ColorInt colors[] = {0xffff0000, 0xff0000ff};
|
||||
float stops[] = {0.0f, 1.0f};
|
||||
auto gradient = ctx->makeLinearGradient(0, 0, 100, 0, colors, stops, 2);
|
||||
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->shader(gradient);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
// Draw with modulated opacity
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
// Draw without modulated opacity
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(1.0f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test deeply nested opacity modulation.
|
||||
TEST_CASE("modulate-opacity-deeply-nested", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
// Create deep nesting of opacity modulation
|
||||
const int depth = 10;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.9f);
|
||||
}
|
||||
|
||||
// Effective opacity is 0.9^10 ≈ 0.3486784401
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.3486784401f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
|
||||
// Unwind the stack
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
renderer.restore();
|
||||
}
|
||||
|
||||
// Now draw at full opacity
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(1.0f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that modulateOpacity works correctly with clipping.
|
||||
TEST_CASE("modulate-opacity-with-clipping", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto clipPath =
|
||||
ctx->makeRenderPath(make_oval({10, 10, 90, 90}), FillRule::nonZero);
|
||||
auto drawPath =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.clipPath(clipPath.get());
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
renderer.drawPath(drawPath.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that modulateOpacity works with transforms.
|
||||
TEST_CASE("modulate-opacity-with-transform", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 50, 50}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.transform(Mat2D::fromTranslate(25, 25));
|
||||
renderer.transform(Mat2D::fromScale(0.5f, 0.5f));
|
||||
renderer.modulateOpacity(0.75f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.75f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test gradient single-entry cache behavior with repeated access.
|
||||
TEST_CASE("gradient-modulated-cache-repeated-access",
|
||||
"[RiveRenderer][opacity][gradient]")
|
||||
{
|
||||
ColorInt colors[] = {0xffff0000, 0xff00ff00};
|
||||
float stops[] = {0.0f, 1.0f};
|
||||
|
||||
auto gradient = Gradient::MakeLinear(0, 0, 100, 0, colors, stops, 2);
|
||||
REQUIRE(gradient != nullptr);
|
||||
|
||||
// Create a modulated gradient with opacity 0.5
|
||||
auto modulated05 = gradient->getModulated(0.5f);
|
||||
CHECK(modulated05 != nullptr);
|
||||
|
||||
// Access same opacity multiple times - should return cached version
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
auto m = gradient->getModulated(0.5f);
|
||||
CHECK(m.get() == modulated05.get());
|
||||
}
|
||||
|
||||
// Switch to different opacity - replaces cache
|
||||
auto modulated025 = gradient->getModulated(0.25f);
|
||||
CHECK(modulated025 != nullptr);
|
||||
CHECK(modulated025.get() != modulated05.get());
|
||||
|
||||
// Repeated access to new opacity returns cached version
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
auto m = gradient->getModulated(0.25f);
|
||||
CHECK(m.get() == modulated025.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Test that stroke paths work with modulated opacity.
|
||||
TEST_CASE("modulate-opacity-stroke", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_line(10, 50, 90, 50), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->style(RenderPaintStyle::stroke);
|
||||
paint->thickness(5.0f);
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.5f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test edge case: very small opacity values.
|
||||
TEST_CASE("modulate-opacity-very-small", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(0.001f);
|
||||
CHECK(renderer.currentModulatedOpacity() == Approx(0.001f));
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
|
||||
// Test that negative opacity clamps to 0.
|
||||
TEST_CASE("modulate-opacity-negative-clamps-to-zero", "[RiveRenderer][opacity]")
|
||||
{
|
||||
auto ctx = RenderContextNULL::MakeContext();
|
||||
ctx->beginFrame(s_frameDescriptor);
|
||||
|
||||
auto path =
|
||||
ctx->makeRenderPath(make_rect({0, 0, 100, 100}), FillRule::nonZero);
|
||||
auto paint = ctx->makeRenderPaint();
|
||||
paint->color(0xffffffff);
|
||||
|
||||
RiveRenderer renderer(ctx.get());
|
||||
|
||||
// Negative opacity should clamp to 0
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(-0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == 0.0f);
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
|
||||
// Very negative value should also clamp to 0
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(-100.0f);
|
||||
CHECK(renderer.currentModulatedOpacity() == 0.0f);
|
||||
renderer.restore();
|
||||
|
||||
// After restore, opacity should be back to 1.0
|
||||
CHECK(renderer.currentModulatedOpacity() == 1.0f);
|
||||
|
||||
renderer.save();
|
||||
renderer.modulateOpacity(-0.5f);
|
||||
renderer.modulateOpacity(-0.5f);
|
||||
CHECK(renderer.currentModulatedOpacity() == 0.0f);
|
||||
renderer.drawPath(path.get(), paint.get());
|
||||
renderer.restore();
|
||||
// After restore, opacity should be back to 1.0
|
||||
CHECK(renderer.currentModulatedOpacity() == 1.0f);
|
||||
|
||||
flushFrame(ctx.get());
|
||||
}
|
||||
@@ -50,6 +50,7 @@ enum class SerializeOp : unsigned char
|
||||
|
||||
frame = 28,
|
||||
frameSize = 29,
|
||||
modulateOpacity = 30,
|
||||
|
||||
};
|
||||
|
||||
@@ -120,6 +121,8 @@ static const char* opToName(SerializeOp op)
|
||||
return "frame";
|
||||
case SerializeOp::frameSize:
|
||||
return "frameSize";
|
||||
case SerializeOp::modulateOpacity:
|
||||
return "modulateOpacity";
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
@@ -558,6 +561,12 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void modulateOpacity(float opacity) override
|
||||
{
|
||||
m_writer->writeVarUint((uint32_t)SerializeOp::modulateOpacity);
|
||||
m_writer->writeFloat(opacity);
|
||||
}
|
||||
|
||||
void drawPath(RenderPath* path, RenderPaint* paint) override
|
||||
{
|
||||
m_writer->writeVarUint((uint32_t)SerializeOp::drawPath);
|
||||
@@ -1312,6 +1321,16 @@ bool advancedMatch(std::vector<uint8_t>& fileA, std::vector<uint8_t>& fileB)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case SerializeOp::modulateOpacity:
|
||||
if (!floatMatches(opA,
|
||||
"modulateopacity_value",
|
||||
readerA,
|
||||
readerB))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!readerB.reachedEnd())
|
||||
|
||||
Reference in New Issue
Block a user