| #include "rive/shapes/paint/paint_image.hpp" |
| #include "rive/shapes/paint/shape_paint.hpp" |
| #include "rive/assets/file_asset.hpp" |
| #include "rive/assets/image_asset.hpp" |
| #include "rive/renderer.hpp" |
| #include "rive/core_context.hpp" |
| |
| using namespace rive; |
| |
| 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); |
| return Super::import(importStack); |
| } |
| |
| 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>(); |
| } |
| |
| 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); |
| } |
| 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; |
| } |
| |
| bool PaintImage::applyTo(RenderPaint* paint, const AABB& bounds) |
| { |
| 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; |
| } |
| // 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 |
| // image's native pixel size (original). 0 = fill, 1 = original. |
| bool original = imageSizeMode() == 1; |
| float baseW = original ? (float)renderImage->width() : bounds.width(); |
| float baseH = original ? (float)renderImage->height() : bounds.height(); |
| float cx = bounds.left() + bounds.width() * 0.5f; |
| float cy = bounds.top() + bounds.height() * 0.5f; |
| Mat2D imageTransform = |
| 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); |
| paint->modulatedImage(renderImage, imageSampler(), imageTransform); |
| return true; |
| } |