mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +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 "rive/renderer.hpp"
|
||||||
#include "utils/auto_cf.hpp"
|
#include "utils/auto_cf.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#if defined(RIVE_BUILD_FOR_OSX)
|
#if defined(RIVE_BUILD_FOR_OSX)
|
||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
@@ -21,6 +22,7 @@ class CGRenderer : public Renderer
|
|||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
CGContextRef m_ctx;
|
CGContextRef m_ctx;
|
||||||
|
std::vector<float> m_opacityStack{1.0f};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CGRenderer(CGContextRef ctx, int width, int height);
|
CGRenderer(CGContextRef ctx, int width, int height);
|
||||||
@@ -29,6 +31,7 @@ public:
|
|||||||
void save() override;
|
void save() override;
|
||||||
void restore() override;
|
void restore() override;
|
||||||
void transform(const Mat2D& transform) override;
|
void transform(const Mat2D& transform) override;
|
||||||
|
void modulateOpacity(float opacity) override;
|
||||||
void clipPath(RenderPath* path) override;
|
void clipPath(RenderPath* path) override;
|
||||||
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
||||||
void drawImage(const RenderImage*,
|
void drawImage(const RenderImage*,
|
||||||
|
|||||||
@@ -379,21 +379,38 @@ CGRenderer::CGRenderer(CGContextRef ctx, int width, int height) : m_ctx(ctx)
|
|||||||
|
|
||||||
CGRenderer::~CGRenderer() { CGContextRestoreGState(m_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)
|
void CGRenderer::transform(const Mat2D& m)
|
||||||
{
|
{
|
||||||
CGContextConcatCTM(m_ctx, convert(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)
|
void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
||||||
{
|
{
|
||||||
LITE_RTTI_CAST_OR_RETURN(cgpaint, CGRenderPaint*, paint);
|
LITE_RTTI_CAST_OR_RETURN(cgpaint, CGRenderPaint*, paint);
|
||||||
LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
|
LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
|
||||||
|
|
||||||
cgpaint->apply(m_ctx);
|
cgpaint->apply(m_ctx);
|
||||||
|
CGContextSetAlpha(m_ctx, m_opacityStack.back());
|
||||||
|
|
||||||
CGContextBeginPath(m_ctx);
|
CGContextBeginPath(m_ctx);
|
||||||
CGContextAddPath(m_ctx, cgpath->path());
|
CGContextAddPath(m_ctx, cgpath->path());
|
||||||
@@ -407,8 +424,9 @@ void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
|||||||
CGContextSaveGState(m_ctx);
|
CGContextSaveGState(m_ctx);
|
||||||
CGContextClip(m_ctx);
|
CGContextClip(m_ctx);
|
||||||
|
|
||||||
// so the gradient modulates with the color's alpha
|
// so the gradient modulates with the color's alpha and the modulated
|
||||||
CGContextSetAlpha(m_ctx, cgpaint->opacity());
|
// opacity
|
||||||
|
CGContextSetAlpha(m_ctx, cgpaint->opacity() * m_opacityStack.back());
|
||||||
|
|
||||||
sh->draw(m_ctx);
|
sh->draw(m_ctx);
|
||||||
CGContextRestoreGState(m_ctx);
|
CGContextRestoreGState(m_ctx);
|
||||||
@@ -438,9 +456,10 @@ void CGRenderer::drawImage(const RenderImage* image,
|
|||||||
LITE_RTTI_CAST_OR_RETURN(cgimg, const CGRenderImage*, image);
|
LITE_RTTI_CAST_OR_RETURN(cgimg, const CGRenderImage*, image);
|
||||||
|
|
||||||
auto bounds = CGRectMake(0, 0, image->width(), image->height());
|
auto bounds = CGRectMake(0, 0, image->width(), image->height());
|
||||||
|
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||||
|
|
||||||
CGContextSaveGState(m_ctx);
|
CGContextSaveGState(m_ctx);
|
||||||
CGContextSetAlpha(m_ctx, opacity);
|
CGContextSetAlpha(m_ctx, finalOpacity);
|
||||||
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
||||||
cgimg->applyLocalMatrix(m_ctx);
|
cgimg->applyLocalMatrix(m_ctx);
|
||||||
CGContextDrawImage(m_ctx, bounds, cgimg->m_image);
|
CGContextDrawImage(m_ctx, bounds, cgimg->m_image);
|
||||||
@@ -482,13 +501,15 @@ void CGRenderer::drawImageMesh(const RenderImage* image,
|
|||||||
auto pts = cgvertices->vecs();
|
auto pts = cgvertices->vecs();
|
||||||
auto uvs = cguvcoords->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
|
// We use the path to set the clip for each triangle. Since calling
|
||||||
// CGContextClip() resets the path, we only need to this once at
|
// CGContextClip() resets the path, we only need to this once at
|
||||||
// the beginning.
|
// the beginning.
|
||||||
CGContextBeginPath(m_ctx);
|
CGContextBeginPath(m_ctx);
|
||||||
|
|
||||||
CGContextSaveGState(m_ctx);
|
CGContextSaveGState(m_ctx);
|
||||||
CGContextSetAlpha(m_ctx, opacity);
|
CGContextSetAlpha(m_ctx, finalOpacity);
|
||||||
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
CGContextSetBlendMode(m_ctx, convert(blendMode));
|
||||||
CGContextSetShouldAntialias(m_ctx, false);
|
CGContextSetShouldAntialias(m_ctx, false);
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,12 @@ public:
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
float opacity) = 0;
|
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
|
// helpers
|
||||||
|
|
||||||
void translate(float x, float y);
|
void translate(float x, float y);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class RenderPaint;
|
|||||||
class ShapePaintMutator;
|
class ShapePaintMutator;
|
||||||
class Feather;
|
class Feather;
|
||||||
class ShapePaintContainer;
|
class ShapePaintContainer;
|
||||||
|
class TransformComponent;
|
||||||
class ShapePaint : public ShapePaintBase,
|
class ShapePaint : public ShapePaintBase,
|
||||||
public EffectsContainer,
|
public EffectsContainer,
|
||||||
public PathProvider
|
public PathProvider
|
||||||
@@ -83,6 +84,8 @@ public:
|
|||||||
void update(ComponentDirt value) override;
|
void update(ComponentDirt value) override;
|
||||||
virtual ShapePaintType paintType() const = 0;
|
virtual ShapePaintType paintType() const = 0;
|
||||||
|
|
||||||
|
TransformComponent* parentTransformComponent() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Feather* m_feather = nullptr;
|
Feather* m_feather = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public:
|
|||||||
BlendMode,
|
BlendMode,
|
||||||
float) override
|
float) override
|
||||||
{}
|
{}
|
||||||
|
void modulateOpacity(float) override {}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rive
|
} // namespace rive
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ public:
|
|||||||
rcp<const RiveRenderPath>,
|
rcp<const RiveRenderPath>,
|
||||||
FillRule,
|
FillRule,
|
||||||
const RiveRenderPaint*,
|
const RiveRenderPaint*,
|
||||||
|
float modulatedOpacity,
|
||||||
RawPath* scratchPath);
|
RawPath* scratchPath);
|
||||||
|
|
||||||
// Determines how coverage is calculated for antialiasing and feathers.
|
// Determines how coverage is calculated for antialiasing and feathers.
|
||||||
@@ -216,6 +217,7 @@ public:
|
|||||||
rcp<const RiveRenderPath>,
|
rcp<const RiveRenderPath>,
|
||||||
FillRule,
|
FillRule,
|
||||||
const RiveRenderPaint*,
|
const RiveRenderPaint*,
|
||||||
|
float modulatedOpacity,
|
||||||
CoverageType,
|
CoverageType,
|
||||||
const RenderContext::FrameDescriptor&);
|
const RenderContext::FrameDescriptor&);
|
||||||
|
|
||||||
@@ -356,7 +358,7 @@ protected:
|
|||||||
|
|
||||||
const RiveRenderPath* const m_pathRef;
|
const RiveRenderPath* const m_pathRef;
|
||||||
const FillRule m_pathFillRule; // Fill rule can mutate on RenderPath.
|
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 gpu::PaintType m_paintType;
|
||||||
const CoverageType m_coverageType;
|
const CoverageType m_coverageType;
|
||||||
float m_strokeRadius = 0;
|
float m_strokeRadius = 0;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public:
|
|||||||
uint32_t indexCount,
|
uint32_t indexCount,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
float opacity) override;
|
float opacity) override;
|
||||||
|
void modulateOpacity(float opacity) override;
|
||||||
|
|
||||||
// Determines if a path is an axis-aligned rectangle that can be represented
|
// Determines if a path is an axis-aligned rectangle that can be represented
|
||||||
// by rive::AABB.
|
// by rive::AABB.
|
||||||
@@ -62,6 +63,10 @@ public:
|
|||||||
{
|
{
|
||||||
return m_stack.back().clipRectMatrix;
|
return m_stack.back().clipRectMatrix;
|
||||||
}
|
}
|
||||||
|
float currentModulatedOpacity() const
|
||||||
|
{
|
||||||
|
return m_stack.back().modulatedOpacity;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -93,6 +98,7 @@ private:
|
|||||||
Mat2D clipRectMatrix;
|
Mat2D clipRectMatrix;
|
||||||
const gpu::ClipRectInverseMatrix* clipRectInverseMatrix = nullptr;
|
const gpu::ClipRectInverseMatrix* clipRectInverseMatrix = nullptr;
|
||||||
bool clipIsEmpty = false;
|
bool clipIsEmpty = false;
|
||||||
|
float modulatedOpacity = 1.0f;
|
||||||
};
|
};
|
||||||
std::vector<RenderState> m_stack{1};
|
std::vector<RenderState> m_stack{1};
|
||||||
|
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ DrawUniquePtr PathDraw::Make(RenderContext* context,
|
|||||||
rcp<const RiveRenderPath> path,
|
rcp<const RiveRenderPath> path,
|
||||||
FillRule fillRule,
|
FillRule fillRule,
|
||||||
const RiveRenderPaint* paint,
|
const RiveRenderPaint* paint,
|
||||||
|
float modulatedOpacity,
|
||||||
RawPath* scratchPath)
|
RawPath* scratchPath)
|
||||||
{
|
{
|
||||||
RIVE_PROF_SCOPE();
|
RIVE_PROF_SCOPE();
|
||||||
@@ -512,6 +513,7 @@ DrawUniquePtr PathDraw::Make(RenderContext* context,
|
|||||||
std::move(path),
|
std::move(path),
|
||||||
fillRule,
|
fillRule,
|
||||||
paint,
|
paint,
|
||||||
|
modulatedOpacity,
|
||||||
coverageType,
|
coverageType,
|
||||||
context->frameDescriptor());
|
context->frameDescriptor());
|
||||||
if (doTriangulation)
|
if (doTriangulation)
|
||||||
@@ -536,6 +538,7 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
|||||||
rcp<const RiveRenderPath> path,
|
rcp<const RiveRenderPath> path,
|
||||||
FillRule initialFillRule,
|
FillRule initialFillRule,
|
||||||
const RiveRenderPaint* paint,
|
const RiveRenderPaint* paint,
|
||||||
|
float modulatedOpacity,
|
||||||
CoverageType coverageType,
|
CoverageType coverageType,
|
||||||
const RenderContext::FrameDescriptor& frameDesc) :
|
const RenderContext::FrameDescriptor& frameDesc) :
|
||||||
Draw(pixelBounds,
|
Draw(pixelBounds,
|
||||||
@@ -547,7 +550,7 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
|||||||
m_pathRef(path.release()),
|
m_pathRef(path.release()),
|
||||||
m_pathFillRule(frameDesc.clockwiseFillOverride ? FillRule::clockwise
|
m_pathFillRule(frameDesc.clockwiseFillOverride ? FillRule::clockwise
|
||||||
: initialFillRule),
|
: initialFillRule),
|
||||||
m_gradientRef(safe_ref(paint->getGradient())),
|
m_gradientRef(paint->getGradientWithOpacity(modulatedOpacity).release()),
|
||||||
m_paintType(paint->getType()),
|
m_paintType(paint->getType()),
|
||||||
m_coverageType(coverageType)
|
m_coverageType(coverageType)
|
||||||
{
|
{
|
||||||
@@ -685,6 +688,27 @@ PathDraw::PathDraw(IAABB pixelBounds,
|
|||||||
|
|
||||||
m_simplePaintValue = paint->getSimpleValue();
|
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)
|
if (m_coverageType == CoverageType::atlas)
|
||||||
{
|
{
|
||||||
// Reserve two triangles for our on-screen rectangle that reads coverage
|
// 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
|
// 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.
|
// fail after setting up an atlas draw than a gradient draw.
|
||||||
if (m_gradientRef != nullptr &&
|
if (m_gradientRef != nullptr)
|
||||||
!flush->allocateGradient(m_gradientRef,
|
|
||||||
&m_simplePaintValue.colorRampLocation))
|
|
||||||
{
|
{
|
||||||
return false;
|
if (!flush->allocateGradient(m_gradientRef,
|
||||||
|
&m_simplePaintValue.colorRampLocation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate a coverage buffer range or atlas region if needed.
|
// Allocate a coverage buffer range or atlas region if needed.
|
||||||
|
|||||||
@@ -188,4 +188,39 @@ bool Gradient::isOpaque() const
|
|||||||
return m_isOpaque == gpu::TriState::yes;
|
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
|
} // namespace rive::gpu
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
namespace rive::gpu
|
namespace rive::gpu
|
||||||
{
|
{
|
||||||
|
|
||||||
// Copies an array of colors or stops for a gradient.
|
// Copies an array of colors or stops for a gradient.
|
||||||
// Stores the data locally if there are 4 values or fewer.
|
// Stores the data locally if there are 4 values or fewer.
|
||||||
// Spills onto the heap if there are >4 values.
|
// Spills onto the heap if there are >4 values.
|
||||||
@@ -25,6 +26,13 @@ public:
|
|||||||
memcpy(m_data, data, count * sizeof(T));
|
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)
|
GradDataArray(GradDataArray&& other)
|
||||||
{
|
{
|
||||||
if (other.m_data == other.m_localData.data())
|
if (other.m_data == other.m_localData.data())
|
||||||
@@ -83,6 +91,11 @@ public:
|
|||||||
size_t count() const { return m_count; }
|
size_t count() const { return m_count; }
|
||||||
bool isOpaque() const;
|
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:
|
private:
|
||||||
Gradient(PaintType paintType,
|
Gradient(PaintType paintType,
|
||||||
GradDataArray<ColorInt>&& colors, // [count]
|
GradDataArray<ColorInt>&& colors, // [count]
|
||||||
@@ -107,6 +120,11 @@ private:
|
|||||||
size_t m_count;
|
size_t m_count;
|
||||||
std::array<float, 3> m_coeffs;
|
std::array<float, 3> m_coeffs;
|
||||||
mutable gpu::TriState m_isOpaque = gpu::TriState::unknown;
|
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
|
} // namespace rive::gpu
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ void RiveRenderPaint::shader(rcp<RenderShader> shader)
|
|||||||
m_imageTexture.reset();
|
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)
|
void RiveRenderPaint::image(rcp<gpu::Texture> imageTexture, float opacity)
|
||||||
{
|
{
|
||||||
m_paintType = gpu::PaintType::image;
|
m_paintType = gpu::PaintType::image;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public:
|
|||||||
bool getIsStroked() const { return m_stroked; }
|
bool getIsStroked() const { return m_stroked; }
|
||||||
ColorInt getColor() const { return m_simpleValue.color; }
|
ColorInt getColor() const { return m_simpleValue.color; }
|
||||||
const gpu::Gradient* getGradient() const { return m_gradient.get(); }
|
const gpu::Gradient* getGradient() const { return m_gradient.get(); }
|
||||||
|
rcp<gpu::Gradient> getGradientWithOpacity(float opacity) const;
|
||||||
gpu::Texture* getImageTexture() const { return m_imageTexture.get(); }
|
gpu::Texture* getImageTexture() const { return m_imageTexture.get(); }
|
||||||
ImageSampler getImageSampler() const { return m_imageSampler; }
|
ImageSampler getImageSampler() const { return m_imageSampler; }
|
||||||
float getImageOpacity() const { return m_simpleValue.imageOpacity; }
|
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;
|
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)
|
void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
||||||
{
|
{
|
||||||
RIVE_PROF_SCOPE()
|
RIVE_PROF_SCOPE()
|
||||||
@@ -161,6 +167,7 @@ void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
|||||||
matrixMaxScale),
|
matrixMaxScale),
|
||||||
path->getFillRule(),
|
path->getFillRule(),
|
||||||
paint,
|
paint,
|
||||||
|
m_stack.back().modulatedOpacity,
|
||||||
&m_scratchPath));
|
&m_scratchPath));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,6 +178,7 @@ void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
|
|||||||
ref_rcp(path),
|
ref_rcp(path),
|
||||||
path->getFillRule(),
|
path->getFillRule(),
|
||||||
paint,
|
paint,
|
||||||
|
m_stack.back().modulatedOpacity,
|
||||||
&m_scratchPath));
|
&m_scratchPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +351,10 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
|||||||
return;
|
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].
|
// Scale the view matrix so we can draw this image as the rect [0, 0, 1, 1].
|
||||||
save();
|
save();
|
||||||
scale(image->width(), image->height());
|
scale(image->width(), image->height());
|
||||||
@@ -362,7 +374,7 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
|||||||
blendMode,
|
blendMode,
|
||||||
std::move(imageTexture),
|
std::move(imageTexture),
|
||||||
imageSampler,
|
imageSampler,
|
||||||
opacity)));
|
finalOpacity)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -378,7 +390,7 @@ void RiveRenderer::drawImage(const RenderImage* renderImage,
|
|||||||
}
|
}
|
||||||
|
|
||||||
RiveRenderPaint paint;
|
RiveRenderPaint paint;
|
||||||
paint.image(std::move(imageTexture), opacity);
|
paint.image(std::move(imageTexture), finalOpacity);
|
||||||
paint.blendMode(blendMode);
|
paint.blendMode(blendMode);
|
||||||
paint.imageSampler(imageSampler);
|
paint.imageSampler(imageSampler);
|
||||||
drawPath(m_unitRectPath.get(), &paint);
|
drawPath(m_unitRectPath.get(), &paint);
|
||||||
@@ -418,6 +430,10 @@ void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply modulated opacity (clamp to prevent negative values)
|
||||||
|
float finalOpacity =
|
||||||
|
std::max(0.0f, opacity * m_stack.back().modulatedOpacity);
|
||||||
|
|
||||||
clipAndPushDraw(gpu::DrawUniquePtr(
|
clipAndPushDraw(gpu::DrawUniquePtr(
|
||||||
m_context->make<gpu::ImageMeshDraw>(gpu::Draw::FULLSCREEN_PIXEL_BOUNDS,
|
m_context->make<gpu::ImageMeshDraw>(gpu::Draw::FULLSCREEN_PIXEL_BOUNDS,
|
||||||
m_stack.back().matrix,
|
m_stack.back().matrix,
|
||||||
@@ -428,7 +444,7 @@ void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
|
|||||||
std::move(uvCoords_f32),
|
std::move(uvCoords_f32),
|
||||||
std::move(indices_u16),
|
std::move(indices_u16),
|
||||||
indexCount,
|
indexCount,
|
||||||
opacity)));
|
finalOpacity)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RiveRenderer::clipAndPushDraw(gpu::DrawUniquePtr draw)
|
void RiveRenderer::clipAndPushDraw(gpu::DrawUniquePtr draw)
|
||||||
@@ -573,12 +589,14 @@ RiveRenderer::ApplyClipResult RiveRenderer::applyClip(gpu::Draw* draw)
|
|||||||
RiveRenderPaint clipUpdatePaint;
|
RiveRenderPaint clipUpdatePaint;
|
||||||
clipUpdatePaint.clipUpdate(/*clip THIS clipDraw against:*/ lastClipID);
|
clipUpdatePaint.clipUpdate(/*clip THIS clipDraw against:*/ lastClipID);
|
||||||
|
|
||||||
gpu::DrawUniquePtr clipDraw = gpu::PathDraw::Make(m_context,
|
gpu::DrawUniquePtr clipDraw =
|
||||||
clip.matrix,
|
gpu::PathDraw::Make(m_context,
|
||||||
clip.path,
|
clip.matrix,
|
||||||
clip.fillRule,
|
clip.path,
|
||||||
&clipUpdatePaint,
|
clip.fillRule,
|
||||||
&m_scratchPath);
|
&clipUpdatePaint,
|
||||||
|
1.0f, // no opacity modulation for clips
|
||||||
|
&m_scratchPath);
|
||||||
|
|
||||||
if (clipDraw == nullptr)
|
if (clipDraw == nullptr)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#define _RIVE_SKIA_RENDERER_HPP_
|
#define _RIVE_SKIA_RENDERER_HPP_
|
||||||
|
|
||||||
#include "rive/renderer.hpp"
|
#include "rive/renderer.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class SkCanvas;
|
class SkCanvas;
|
||||||
|
|
||||||
@@ -15,12 +16,14 @@ class SkiaRenderer : public Renderer
|
|||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
SkCanvas* m_Canvas;
|
SkCanvas* m_Canvas;
|
||||||
|
std::vector<float> m_opacityStack{1.0f};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SkiaRenderer(SkCanvas* canvas) : m_Canvas(canvas) {}
|
SkiaRenderer(SkCanvas* canvas) : m_Canvas(canvas) {}
|
||||||
void save() override;
|
void save() override;
|
||||||
void restore() override;
|
void restore() override;
|
||||||
void transform(const Mat2D& transform) override;
|
void transform(const Mat2D& transform) override;
|
||||||
|
void modulateOpacity(float opacity) override;
|
||||||
void clipPath(RenderPath* path) override;
|
void clipPath(RenderPath* path) override;
|
||||||
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
void drawPath(RenderPath* path, RenderPaint* paint) override;
|
||||||
void drawImage(const RenderImage*,
|
void drawImage(const RenderImage*,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "include/core/SkVertices.h"
|
#include "include/core/SkVertices.h"
|
||||||
#include "include/effects/SkGradientShader.h"
|
#include "include/effects/SkGradientShader.h"
|
||||||
#include "include/effects/SkImageFilters.h"
|
#include "include/effects/SkImageFilters.h"
|
||||||
|
#include "include/core/SkColorFilter.h"
|
||||||
|
|
||||||
#include "rive/math/vec2d.hpp"
|
#include "rive/math/vec2d.hpp"
|
||||||
#include "rive/shapes/paint/color.hpp"
|
#include "rive/shapes/paint/color.hpp"
|
||||||
@@ -218,19 +219,70 @@ void SkiaRenderPaint::shader(rcp<RenderShader> rsh)
|
|||||||
m_Paint.setShader(sksh ? sksh->shader : nullptr);
|
m_Paint.setShader(sksh ? sksh->shader : nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SkiaRenderer::save() { m_Canvas->save(); }
|
void SkiaRenderer::save()
|
||||||
void SkiaRenderer::restore() { m_Canvas->restore(); }
|
{
|
||||||
|
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)
|
void SkiaRenderer::transform(const Mat2D& transform)
|
||||||
{
|
{
|
||||||
m_Canvas->concat(ToSkia::convert(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)
|
void SkiaRenderer::drawPath(RenderPath* path, RenderPaint* paint)
|
||||||
{
|
{
|
||||||
LITE_RTTI_CAST_OR_RETURN(skiaRenderPath, SkiaRenderPath*, path);
|
LITE_RTTI_CAST_OR_RETURN(skiaRenderPath, SkiaRenderPath*, path);
|
||||||
LITE_RTTI_CAST_OR_RETURN(skiaRenderPaint, SkiaRenderPaint*, paint);
|
LITE_RTTI_CAST_OR_RETURN(skiaRenderPaint, SkiaRenderPaint*, paint);
|
||||||
|
|
||||||
SkiaRenderPaint::OverrideStrokeParamsForFeather ospff(skiaRenderPaint);
|
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)
|
void SkiaRenderer::clipPath(RenderPath* path)
|
||||||
@@ -245,8 +297,9 @@ void SkiaRenderer::drawImage(const RenderImage* image,
|
|||||||
float opacity)
|
float opacity)
|
||||||
{
|
{
|
||||||
LITE_RTTI_CAST_OR_RETURN(skiaImage, const SkiaRenderImage*, image);
|
LITE_RTTI_CAST_OR_RETURN(skiaImage, const SkiaRenderImage*, image);
|
||||||
|
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||||
SkPaint paint;
|
SkPaint paint;
|
||||||
paint.setAlphaf(opacity);
|
paint.setAlphaf(finalOpacity);
|
||||||
paint.setBlendMode(ToSkia::convert(blendMode));
|
paint.setBlendMode(ToSkia::convert(blendMode));
|
||||||
m_Canvas->drawImage(skiaImage->skImage(), 0.0f, 0.0f, gSampling, &paint);
|
m_Canvas->drawImage(skiaImage->skImage(), 0.0f, 0.0f, gSampling, &paint);
|
||||||
}
|
}
|
||||||
@@ -301,8 +354,9 @@ void SkiaRenderer::drawImageMesh(const RenderImage* image,
|
|||||||
gSampling,
|
gSampling,
|
||||||
&scaleM);
|
&scaleM);
|
||||||
|
|
||||||
|
float finalOpacity = std::max(0.0f, opacity * m_opacityStack.back());
|
||||||
SkPaint paint;
|
SkPaint paint;
|
||||||
paint.setAlphaf(opacity);
|
paint.setAlphaf(finalOpacity);
|
||||||
paint.setBlendMode(ToSkia::convert(blendMode));
|
paint.setBlendMode(ToSkia::convert(blendMode));
|
||||||
paint.setShader(shader);
|
paint.setShader(shader);
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,9 @@ void ScriptedPathEffect::updateEffect(PathProvider* pathProvider,
|
|||||||
// Stack: [self, "update", self]
|
// Stack: [self, "update", self]
|
||||||
lua_newrive<ScriptedPathData>(state, source->rawPath());
|
lua_newrive<ScriptedPathData>(state, source->rawPath());
|
||||||
// Stack: [self, "update", self, pathData]
|
// Stack: [self, "update", self, pathData]
|
||||||
lua_newrive<ScriptedNode>(
|
lua_newrive<ScriptedNode>(state,
|
||||||
state,
|
nullptr,
|
||||||
nullptr,
|
shapePaint->parentTransformComponent());
|
||||||
shapePaint->parent()->as<TransformComponent>());
|
|
||||||
auto scriptedNode = lua_torive<ScriptedNode>(state, -1);
|
auto scriptedNode = lua_torive<ScriptedNode>(state, -1);
|
||||||
scriptedNode->shapePaint(shapePaint);
|
scriptedNode->shapePaint(shapePaint);
|
||||||
// Stack: [self, "update", self, pathData, node]
|
// Stack: [self, "update", self, pathData, node]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "rive/shapes/shape_paint_container.hpp"
|
#include "rive/shapes/shape_paint_container.hpp"
|
||||||
#include "rive/shapes/paint/feather.hpp"
|
#include "rive/shapes/paint/feather.hpp"
|
||||||
#include "rive/artboard.hpp"
|
#include "rive/artboard.hpp"
|
||||||
|
#include "rive/transform_component.hpp"
|
||||||
#include "rive/factory.hpp"
|
#include "rive/factory.hpp"
|
||||||
#include "rive/shapes/paint/fill.hpp"
|
#include "rive/shapes/paint/fill.hpp"
|
||||||
#include "rive/profiler/profiler_macros.h"
|
#include "rive/profiler/profiler_macros.h"
|
||||||
@@ -186,4 +187,18 @@ void ShapePaint::addStrokeEffect(StrokeEffect* effect)
|
|||||||
{
|
{
|
||||||
effect->addPathProvider(this);
|
effect->addPathProvider(this);
|
||||||
EffectsContainer::addStrokeEffect(effect);
|
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/tess/sub_path.hpp"
|
||||||
#include "rive/math/mat2d.hpp"
|
#include "rive/math/mat2d.hpp"
|
||||||
#include "rive/math/mat4.hpp"
|
#include "rive/math/mat4.hpp"
|
||||||
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ struct RenderState
|
|||||||
{
|
{
|
||||||
Mat2D transform;
|
Mat2D transform;
|
||||||
std::vector<SubPath> clipPaths;
|
std::vector<SubPath> clipPaths;
|
||||||
|
float modulatedOpacity = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
class TessRenderer : public Renderer
|
class TessRenderer : public Renderer
|
||||||
@@ -42,7 +44,9 @@ public:
|
|||||||
void save() override;
|
void save() override;
|
||||||
void restore() override;
|
void restore() override;
|
||||||
void transform(const Mat2D& transform) override;
|
void transform(const Mat2D& transform) override;
|
||||||
|
void modulateOpacity(float opacity) override;
|
||||||
const Mat2D& transform() { return m_Stack.back().transform; }
|
const Mat2D& transform() { return m_Stack.back().transform; }
|
||||||
|
float modulatedOpacity() const { return m_Stack.back().modulatedOpacity; }
|
||||||
void clipPath(RenderPath* path) override;
|
void clipPath(RenderPath* path) override;
|
||||||
void drawImage(const RenderImage*,
|
void drawImage(const RenderImage*,
|
||||||
ImageSampler,
|
ImageSampler,
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ void TessRenderer::transform(const Mat2D& transform)
|
|||||||
stackMat = stackMat * 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)
|
void TessRenderer::clipPath(RenderPath* path)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public:
|
|||||||
void save() override {}
|
void save() override {}
|
||||||
void restore() override {}
|
void restore() override {}
|
||||||
void transform(const Mat2D& matrix) override {}
|
void transform(const Mat2D& matrix) override {}
|
||||||
|
void modulateOpacity(float) override {}
|
||||||
void drawPath(RenderPath* path, RenderPaint* paint) override
|
void drawPath(RenderPath* path, RenderPaint* paint) override
|
||||||
{
|
{
|
||||||
auto renderPath = static_cast<RiveRenderPath*>(path);
|
auto renderPath = static_cast<RiveRenderPath*>(path);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public:
|
|||||||
? FillRule::clockwise
|
? FillRule::clockwise
|
||||||
: FillRule::nonZero,
|
: FillRule::nonZero,
|
||||||
paint,
|
paint,
|
||||||
|
1.0f, // modulatedOpacity
|
||||||
SelectCoverageType(paint,
|
SelectCoverageType(paint,
|
||||||
1,
|
1,
|
||||||
context->platformFeatures(),
|
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,
|
frame = 28,
|
||||||
frameSize = 29,
|
frameSize = 29,
|
||||||
|
modulateOpacity = 30,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,6 +121,8 @@ static const char* opToName(SerializeOp op)
|
|||||||
return "frame";
|
return "frame";
|
||||||
case SerializeOp::frameSize:
|
case SerializeOp::frameSize:
|
||||||
return "frameSize";
|
return "frameSize";
|
||||||
|
case SerializeOp::modulateOpacity:
|
||||||
|
return "modulateOpacity";
|
||||||
}
|
}
|
||||||
return "???";
|
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
|
void drawPath(RenderPath* path, RenderPaint* paint) override
|
||||||
{
|
{
|
||||||
m_writer->writeVarUint((uint32_t)SerializeOp::drawPath);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case SerializeOp::modulateOpacity:
|
||||||
|
if (!floatMatches(opA,
|
||||||
|
"modulateopacity_value",
|
||||||
|
readerA,
|
||||||
|
readerB))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!readerB.reachedEnd())
|
if (!readerB.reachedEnd())
|
||||||
|
|||||||
Reference in New Issue
Block a user