blob: 36018740e85df964b47481ff3b74b6dd18c6f60d [file]
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/shape_paint_container.hpp"
#include "rive/shapes/paint/feather.hpp"
#include "rive/shapes/paint/paint_image.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"
using namespace rive;
StatusCode ShapePaint::onAddedClean(CoreContext* context)
{
// Every child has registered with us by now (onAddedDirty runs for all
// objects first).
m_hasPaintImage = firstChild<PaintImage>() != nullptr;
m_isFill = is<Fill>();
auto container = ShapePaintContainer::from(parent());
if (container == nullptr)
{
return StatusCode::MissingObject;
}
// If the paint mutator wasn't compatible with this runtime it's possible we
// get a ShapePaint with no mutator (not children).
if (m_PaintMutator != nullptr)
{
container->addPaint(this);
}
#ifdef WITH_RIVE_EDITOR
// Edit-time: Stroke / Fill::update derefs `m_RenderPaint` which
// is set by `initRenderPaint` only if a child mutator
// (SolidColor / LinearGradient) successfully initialized. Coop
// can deliver duplicate or conflicting mutators where all but
// the first return InvalidObject from `initPaintMutator`,
// leaving `m_RenderPaint` null. Signal the dispatcher to cull
// this ShapePaint so it doesn't enter `m_DependencyOrder` and
// crash inside `update()`.
if (m_RenderPaint == nullptr)
{
return StatusCode::InvalidObject;
}
#endif
return StatusCode::Ok;
}
void ShapePaint::renderOpacity(float value)
{
m_PaintMutator->renderOpacity(value);
// A transparent shape usually defers its path too, and rebuilding that
// path when the shape shows is what brings Path dirt back here. Shapes
// that never defer (skinned ones, or ones following a path) build while
// hidden, so nothing else would re-run the effects this paint skipped.
if (m_effectsDeferred && value != 0)
{
addDirt(ComponentDirt::Path);
}
}
void ShapePaint::update(ComponentDirt value)
{
Super::update(value);
auto shapeEffects = effects();
if (hasDirt(value, ComponentDirt::Path) && shapeEffects->size() > 0)
{
auto container = ShapePaintContainer::from(parent());
// Hidden paints are re-invalidated when shown (see renderOpacity), so
// measuring now is wasted, unless a clip still reads the result. One
// without a color or gradient never shows.
if ((m_PaintMutator == nullptr || renderOpacity() == 0) &&
(container->pathFlags() & PathFlags::clipping) == PathFlags::none)
{
m_effectsDeferred = true;
return;
}
m_effectsDeferred = false;
auto path = pickPath(container);
// Nothing to apply them to yet, like a text whose font has not loaded.
if (path == nullptr)
{
return;
}
for (auto& effect : *shapeEffects)
{
effect->updateEffect(this, path, this);
auto newPath = effect->effectPath(this);
if (newPath)
{
path = newPath;
}
}
}
}
RenderPaint* ShapePaint::initRenderPaint(ShapePaintMutator* mutator)
{
assert(m_RenderPaint == nullptr);
m_PaintMutator = mutator;
auto factory = mutator->component()->artboard()->factory();
m_RenderPaint = factory->makeRenderPaint();
return m_RenderPaint.get();
}
void ShapePaint::blendMode(BlendMode parentValue, uint8_t parentAdditiveAmount)
{
assert(m_RenderPaint != nullptr);
// 127 means inherit
const bool inherits = blendModeValue() == 127;
const BlendMode mode = inherits ? parentValue : (BlendMode)blendModeValue();
const uint8_t amount = inherits ? parentAdditiveAmount : additiveAmount();
m_RenderPaint->blendMode(mode);
// Only additive is parameterized; every other mode ignores additiveness
// anyway, but keep it at 0 so a stale value can never leak through.
m_RenderPaint->additiveness(additivenessFor(mode, amount));
}
void ShapePaint::additiveAmountChanged()
{
// additiveAmount animates and data binds, so it can change long after
// buildDependencies() did the initial sync. A paint set to inherit takes
// its amount from the parent drawable, which pushes it down itself.
if (m_RenderPaint == nullptr || inheritsBlendMode())
{
return;
}
m_RenderPaint->blendMode(blendMode());
m_RenderPaint->additiveness(additivenessFor(blendMode(), additiveAmount()));
}
void ShapePaint::feather(Feather* feather) { m_feather = feather; }
Feather* ShapePaint::feather() const { return m_feather; }
void ShapePaint::draw(
Renderer* renderer,
ShapePaintPath* shapePaintPath,
const Mat2D& transform,
bool usePathFillRule,
RenderPaint* overridePaint,
bool needsSaveOperation)
{
RIVE_PROF_SCOPE_L(2)
ShapePaintPath* pathToDraw = shapePaintPath;
bool saved = !needsSaveOperation;
if (m_feather != nullptr)
{
bool offsetInArtboard = m_feather->space() == TransformSpace::world;
if (offsetInArtboard && !m_feather->isInner())
{
if (m_feather->offsetX() != 0 || m_feather->offsetY() != 0)
{
if (!saved)
{
saved = true;
renderer->save();
}
renderer->translate(m_feather->offsetX(), m_feather->offsetY());
}
}
}
if (shapePaintPath->isLocal())
{
if (!saved)
{
saved = true;
renderer->save();
}
renderer->transform(transform);
}
auto pathEffect = lastEffectPath(this);
if (pathEffect)
{
pathToDraw = pathEffect;
}
if (m_feather != nullptr)
{
if (m_feather->isInner())
{
if (m_feather->innerPath() == nullptr)
{
// Bail out, but never leave the renderer's state stack
// unbalanced: we may already have saved above.
if (saved && needsSaveOperation)
{
renderer->restore();
}
return;
}
// When a path effect is active, the inner path and clip must be
// based on the effect-modified path, not the original shape path.
// Only rebuild when the effect path has actually changed.
if (pathEffect != nullptr && m_feather->effectPathDirty())
{
auto container = ShapePaintContainer::from(parent());
if (container != nullptr)
{
bool offsetInArtboard = m_feather->space() == TransformSpace::world;
m_feather->rebuildInnerPath(
pathEffect,
container->shapeWorldTransform(),
offsetInArtboard);
}
}
pathToDraw = m_feather->innerPath();
if (!saved)
{
saved = true;
renderer->save();
}
auto clipPath = pathEffect ? pathEffect : shapePaintPath;
auto renderPath = clipPath->renderPath(this);
if (renderPath != nullptr)
{
renderer->clipPath(renderPath);
}
}
// If we're offseting in world space, apply the offset last.
if (m_feather->space() != TransformSpace::world && !m_feather->isInner() &&
(m_feather->offsetX() != 0 || m_feather->offsetY() != 0))
{
if (!saved)
{
saved = true;
renderer->save();
}
renderer->translate(m_feather->offsetX(), m_feather->offsetY());
}
}
auto renderPath = pathToDraw->renderPath(this);
if (renderPath != nullptr)
{
// Ugh, can't we make fillRule part of the Paint?
#ifdef WITH_RIVE_EDITOR
const bool isFill = is<Fill>();
#else
const bool isFill = m_isFill;
#endif
if (!usePathFillRule && isFill)
{
renderPath->fillRule((FillRule)as<Fill>()->fillRule());
}
// Modulate this paint with the (optional) PaintImage child, fit to the
// shape's local bounds. Skipped when drawing with an external override
// paint. The child is discovered by type rather than cached.
ModulatedDrawTarget target;
target.renderer = renderer;
if (overridePaint == nullptr)
{
applyModulatedImage(renderer, shapePaintPath, &target);
}
if (target.renderer != renderer)
{
// An artboard source rasterized on an immediate host, which leaves
// [renderer] unable to sample the result; draw through the clean
// renderer it handed back, with this draw's transform and opacity.
target.renderer->save();
target.renderer->transform(target.transform);
target.renderer->modulateOpacity(target.opacity);
target.renderer->drawPath(renderPath, renderPaint());
target.renderer->restore();
}
else
{
renderer->drawPath(
renderPath,
overridePaint != nullptr ? overridePaint : renderPaint());
}
}
if (saved && needsSaveOperation)
{
renderer->restore();
}
}
void ShapePaint::applyModulatedImage(
Renderer* renderer,
const ShapePaintPath* path,
ModulatedDrawTarget* target)
{
#ifdef WITH_RIVE_EDITOR
PaintImage* image = firstChild<PaintImage>();
#else
PaintImage* image = m_hasPaintImage ? firstChild<PaintImage>() : nullptr;
#endif
if (image != nullptr &&
image
->applyTo(renderer, renderPaint(), path->rawPath()->bounds(), target))
{
m_hasModulatedImage = true;
return;
}
if (m_hasModulatedImage)
{
// No image to modulate with any more -- the child was removed, its
// asset cleared, or the replacement hasn't decoded yet. The RenderPaint
// persists across draws, so drop the stale texture explicitly.
renderPaint()->modulatedImage(
nullptr,
ImageSampler::LinearClamp(),
Mat2D());
m_hasModulatedImage = false;
}
}
void ShapePaint::invalidateEffects(StrokeEffect* invalidatingEffect)
{
EffectsContainer::invalidateEffects(invalidatingEffect);
if (m_feather != nullptr)
{
m_feather->markEffectPathDirty();
// The path we paint changed; an inner feather derives its geometry
// from that path so it has to rebuild.
if (m_feather->isInner())
{
m_feather->addDirt(ComponentDirt::Path);
}
}
invalidateRendering();
}
void ShapePaint::invalidateEffects() { invalidateEffects(nullptr); }
void ShapePaint::invalidateRendering() { addDirt(ComponentDirt::Path, true); }
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;
}