blob: 5e933e78ffdea078b9b902469acfdd3089a314b0 [file]
#include "rive/shapes/paint/paint_image.hpp"
#include "rive/shapes/paint/artboard_paint_source.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/artboard.hpp"
#include "rive/assets/file_asset.hpp"
#include "rive/assets/image_asset.hpp"
#include "rive/factory.hpp"
#include "rive/importers/backboard_importer.hpp"
#include "rive/importers/import_stack.hpp"
#include "rive/renderer.hpp"
#include "rive/core_context.hpp"
using namespace rive;
PaintImage::PaintImage() = default;
PaintImage::~PaintImage() = default;
StatusCode PaintImage::import(ImportStack& importStack)
{
// The image is optional, so registration is best-effort: unlike the Image
// node we must not fail the import when there's no backboard importer. The
// backboard importer safely skips out-of-range indices during resolve.
registerReferencer(importStack);
StatusCode code = Super::import(importStack);
if (code != StatusCode::Ok)
{
return code;
}
// Same for the artboard, and registered last, once this object is certain
// to survive -- resolve also runs the nested artboard cycle cut over it,
// and skips an id that names no artboard (an image source's).
if (auto backboardImporter = importStack.latest<BackboardImporter>(
Backboard::typeKey))
{
backboardImporter->addArtboardReferencer(this);
}
return StatusCode::Ok;
}
bool PaintImage::validate(CoreContext* context)
{
if (!Super::validate(context))
{
return false;
}
auto coreObject = context->resolve(parentId());
// we know it's not nullptr from Super::validate
assert(coreObject != nullptr);
return coreObject->is<ShapePaint>();
}
StatusCode PaintImage::onAddedClean(CoreContext* context)
{
StatusCode code = Super::onAddedClean(context);
if (code != StatusCode::Ok)
{
return code;
}
// Only instances play the source: a file level artboard is never drawn,
// and instancing here (rather than in clone) picks up the factory the
// host instance was given, which instance() only sets after cloning.
Artboard* host = artboard();
if (m_referencedArtboard != nullptr && host != nullptr && host->isInstance())
{
m_source = ArtboardPaintSource::make(
m_referencedArtboard,
host->factory(),
this);
if (m_source != nullptr)
{
addDirt(ComponentDirt::Components);
// Normally the context arrives later, when the host is bound; an
// instance made inside an already bound artboard picks it up now.
if (auto dataContext = host->dataContext())
{
m_source->dataContext(dataContext);
}
}
}
return StatusCode::Ok;
}
ArtboardInstance* PaintImage::artboardInstance(int index)
{
return index == 0 && m_source != nullptr ? m_source->artboard() : nullptr;
}
void PaintImage::internalDataContext(rcp<DataContext> dataContext)
{
if (m_source != nullptr)
{
m_source->dataContext(dataContext);
}
}
void PaintImage::bindViewModelInstance(
rcp<ViewModelInstance>,
rcp<DataContext> parent)
{
// Only reached for a bind path, which a paint does not have; keep the
// passthrough either way.
internalDataContext(parent);
}
void PaintImage::relinkDataContext(rcp<ViewModelInstance> viewModelInstance)
{
if (m_source != nullptr)
{
m_source->relinkDataContext(viewModelInstance);
}
}
void PaintImage::clearDataContext()
{
if (m_source != nullptr)
{
m_source->clearDataContext();
}
}
void PaintImage::unbind()
{
if (m_source != nullptr)
{
m_source->unbind();
}
}
void PaintImage::updateDataBinds()
{
if (m_source != nullptr && !isCollapsed())
{
m_source->updateDataBinds();
}
}
void PaintImage::buildDependencies()
{
Super::buildDependencies();
// Puts this in the update order, so the Components dirt an advancing
// source raises reaches update() and gets cleared -- otherwise the dirt
// sticks, and every later change is swallowed as "already marked". On the
// artboard rather than the parent paint: a Fill only joins the graph when
// it has path effects. Unconditional, so an instance's replayed order
// always matches its source's.
if (Artboard* owner = artboard())
{
owner->addDependent(this);
}
}
void PaintImage::update(ComponentDirt value)
{
Super::update(value);
if (m_source != nullptr && hasDirt(value, ComponentDirt::Components))
{
// Whether this updated is discarded, as NestedArtboard does: by the end
// of the pass all the dirt is gone, and only another advance of the
// scene can bring it back.
m_source->update();
}
}
bool PaintImage::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
if (m_source == nullptr || isCollapsed())
{
return false;
}
bool keepGoing = m_source->advance(elapsedSeconds, flags);
if (m_source->needsUpdate())
{
// Marks the host artboard changed too, so a host gated on didChange()
// draws the frame that re-rasterizes the source.
addDirt(ComponentDirt::Components);
}
return keepGoing;
}
Core* PaintImage::clone() const
{
PaintImage* twin = PaintImageBase::clone()->as<PaintImage>();
// The generated clone copies the imageAssetId property but not the resolved
// asset, so carry it over (mirrors Image::clone). Without this a nested
// artboard instance loses its paint image.
if (m_fileAsset != nullptr)
{
twin->setAsset(m_fileAsset);
}
// The file level source, never an instance of it: the twin instances its
// own once its artboard is initialized (onAddedClean).
twin->referencedArtboard(m_referencedArtboard);
return twin;
}
uint32_t PaintImage::assetId() { return imageAssetId(); }
void PaintImage::invalidatePaint()
{
if (parent() != nullptr && parent()->is<ShapePaint>())
{
parent()->as<ShapePaint>()->invalidateRendering();
}
}
void PaintImage::setAsset(rcp<FileAsset> asset)
{
if (asset != nullptr && asset->is<ImageAsset>())
{
FileAssetReferencer::setAsset(asset);
invalidatePaint();
}
}
// The texture usually lands after setAsset -- decoding is async, and in the
// editor the bytes arrive from the CDN later still. ImageAsset publishes it by
// calling assetUpdated() on its referencers, so re-invalidate here; otherwise a
// paint that recorded "no image" on its first draw keeps that cleared state
// until some unrelated redraw happens to dirty it.
void PaintImage::assetUpdated() { invalidatePaint(); }
ImageAsset* PaintImage::imageAsset() const
{
return static_cast<ImageAsset*>(m_fileAsset.get());
}
ImageSampler PaintImage::imageSampler() const
{
// Clamp file values so the key stays inside the backends' sampler tables.
auto filterValue = [](uint32_t value) {
return value <= (uint32_t)ImageFilter::nearest
? static_cast<ImageFilter>(value)
: ImageFilter::bilinear;
};
auto wrapValue = [](uint32_t value) {
return value <= (uint32_t)ImageWrap::mirror ? static_cast<ImageWrap>(value)
: ImageWrap::clamp;
};
ImageSampler sampler = ImageSampler::LinearClamp();
if (ImageAsset* asset = imageAsset())
{
sampler.filter = filterValue(asset->samplerFilter());
sampler.wrapX = wrapValue(asset->samplerWrapX());
sampler.wrapY = wrapValue(asset->samplerWrapY());
}
// Paint values are offset by one, zero means inherit from the asset.
if (imageSamplerFilter() != 0)
{
sampler.filter = filterValue(imageSamplerFilter() - 1);
}
if (imageSamplerWrapX() != 0)
{
sampler.wrapX = wrapValue(imageSamplerWrapX() - 1);
}
if (imageSamplerWrapY() != 0)
{
sampler.wrapY = wrapValue(imageSamplerWrapY() - 1);
}
return sampler;
}
Mat2D PaintImage::textureTransform(
const AABB& bounds,
float originalWidth,
float originalHeight) const
{
// Maps the image's normalized texture space (0..1) into the path's
// coordinate space. The base fits the image across the bounds once; the
// user transform (scale/offset/rotation about the bounds center) modifies
// it. At defaults it reproduces the plain fit; with scale < 1 and
// wrap=repeat/mirror it tiles. Base tile size: shape bounds (fill) or the
// source's own size (original). 0 = fill, 1 = original.
bool original = imageSizeMode() == 1;
float baseW = original ? originalWidth : bounds.width();
float baseH = original ? originalHeight : bounds.height();
float cx = bounds.left() + bounds.width() * 0.5f;
float cy = bounds.top() + bounds.height() * 0.5f;
return Mat2D::fromTranslate(cx, cy) * Mat2D::fromRotation(imageRotation()) *
Mat2D::fromTranslate(imageOffsetX() * baseW, imageOffsetY() * baseH) *
Mat2D::fromScale(baseW * imageScaleX(), baseH * imageScaleY()) *
Mat2D::fromTranslate(-0.5f, -0.5f);
}
bool PaintImage::applyTo(
Renderer* renderer,
RenderPaint* paint,
const AABB& bounds,
ModulatedDrawTarget* target)
{
target->renderer = renderer;
if (m_referencedArtboard != nullptr)
{
// An artboard source wins over the asset whenever it resolves, even
// while it has no instance (a file level artboard is never drawn).
if (m_source == nullptr)
{
return false;
}
Factory* factory = artboard() != nullptr ? artboard()->factory() : nullptr;
// "Original" is the artboard's own size in its units, not the raster's
// pixels: those follow the zoom.
const AABB box = m_source->bounds();
return m_source->applyTo(
renderer,
factory != nullptr ? factory->canvasContentHost() : nullptr,
paint,
imageSampler(),
textureTransform(bounds, box.width(), box.height()),
target);
}
ImageAsset* asset = imageAsset();
RenderImage* renderImage = asset != nullptr ? asset->renderImage() : nullptr;
if (renderImage == nullptr)
{
// The asset was cleared (or replaced by one that hasn't decoded yet).
// The caller clears the stale texture off the (persistent) paint.
return false;
}
paint->modulatedImage(
renderImage,
imageSampler(),
textureTransform(
bounds,
(float)renderImage->width(),
(float)renderImage->height()));
return true;
}