| #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; | |
| } |