| #include "rive/math/hit_test.hpp" | |
| #include "rive/shapes/image.hpp" | |
| #include "rive/node.hpp" | |
| #include "rive/layout/layout_node_style.hpp" | |
| #include "rive/layout/layout_participant.hpp" | |
| #include "rive/backboard.hpp" | |
| #include "rive/importers/backboard_importer.hpp" | |
| #include "rive/assets/file_asset.hpp" | |
| #include "rive/assets/image_asset.hpp" | |
| #include "rive/layout.hpp" | |
| #include "rive/layout/n_slicer.hpp" | |
| #include "rive/shapes/mesh_drawable.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/clip_result.hpp" | |
| #include <cassert> | |
| using namespace rive; | |
| void Image::draw(Renderer* renderer) | |
| { | |
| rive::ImageAsset* asset = this->imageAsset(); | |
| rive::RenderImage* renderImage = asset->renderImage(); | |
| if (renderImage == nullptr) | |
| { | |
| return; | |
| } | |
| if (m_needsSaveOperation) | |
| { | |
| renderer->save(); | |
| } | |
| float width = (float)renderImage->width(); | |
| float height = (float)renderImage->height(); | |
| if (m_Mesh != nullptr) | |
| { | |
| m_Mesh->draw(renderer, | |
| renderImage, | |
| imageSampler(), | |
| blendMode(), | |
| renderOpacity()); | |
| } | |
| else | |
| { | |
| renderer->transform(worldTransform()); | |
| renderer->translate(-width * originX(), -height * originY()); | |
| renderer->drawImage(renderImage, | |
| imageSampler(), | |
| blendMode(), | |
| renderOpacity()); | |
| } | |
| if (m_needsSaveOperation) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| bool Image::willDraw() | |
| { | |
| return Super::willDraw() && renderOpacity() != 0.0f && | |
| this->imageAsset() != nullptr; | |
| } | |
| Core* Image::hitTest(HitInfo* hinfo, const Mat2D& xform) | |
| { | |
| // TODO: handle clip? | |
| auto renderImage = imageAsset()->renderImage(); | |
| float width = (float)renderImage->width(); | |
| float height = (float)renderImage->height(); | |
| if (m_Mesh) | |
| { | |
| printf("Missing mesh\n"); | |
| // TODO: hittest mesh | |
| } | |
| else | |
| { | |
| auto mx = xform * worldTransform() * | |
| Mat2D::fromTranslate(-width * originX(), -height * originY()); | |
| HitTester tester(hinfo->area); | |
| tester.addRect(AABB(0, 0, (float)width, (float)height), mx); | |
| if (tester.test()) | |
| { | |
| return this; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| StatusCode Image::import(ImportStack& importStack) | |
| { | |
| auto result = registerReferencer(importStack); | |
| if (result != StatusCode::Ok) | |
| { | |
| return result; | |
| } | |
| // Files exported before 7.2 overwrite scaleX/scaleY with the layout fit, so | |
| // any stored scale on a layout image was ignored. Keep that legacy behavior | |
| // for those files; newer files compose the fit as a separate scale so the | |
| // user scale stays editable/animatable. | |
| int major = importStack.majorVersion(); | |
| int minor = importStack.minorVersion(); | |
| m_layoutScaleSeparate = major > 7 || (major == 7 && minor >= 2); | |
| return Super::import(importStack); | |
| } | |
| // Question: thoughts on this? it looks a bit odd to me, | |
| // maybe there's a trick i'm missing here .. (could also implement | |
| // getAssetId...) | |
| uint32_t Image::assetId() { return ImageBase::assetId(); } | |
| void Image::setAsset(rcp<FileAsset> asset) | |
| { | |
| if (asset != nullptr && asset->is<ImageAsset>()) | |
| { | |
| FileAssetReferencer::setAsset(asset); | |
| // If we have a mesh and we're in the source artboard, let's initialize | |
| // the mesh buffers. | |
| if (m_Mesh != nullptr && !artboard()->isInstance()) | |
| { | |
| m_Mesh->onAssetLoaded(imageAsset()->renderImage()); | |
| } | |
| updateImageScale(); | |
| } | |
| } | |
| void Image::assetUpdated() | |
| { | |
| updateImageScale(); | |
| markWorldTransformDirty(); | |
| #ifdef WITH_RIVE_EDITOR | |
| // Editor / coop path: the FileAsset's `renderImage` typically | |
| // arrives AFTER `Image::setAsset` has already run (bytes are | |
| // fetched asynchronously from the CDN and pushed in via a pump | |
| // command). `Mesh::onAssetLoaded` allocated its GPU buffers on | |
| // the first bind with `renderImage == nullptr` and wrote UVs | |
| // using an identity `uvTransform`; now that the RenderImage is | |
| // actually present (with a real `uvTransform`), re-run the | |
| // UV/buffer initialization so sampling lines up with the | |
| // decoded texture. | |
| // | |
| // Runtime `.riv` loads don't need this — the RenderImage is | |
| // present the moment `setAsset` binds, so the single call at | |
| // image.cpp:117 is enough. | |
| if (m_Mesh != nullptr && artboard() != nullptr && !artboard()->isInstance()) | |
| { | |
| auto* ia = imageAsset(); | |
| m_Mesh->onAssetLoaded(ia != nullptr ? ia->renderImage() : nullptr); | |
| } | |
| #endif | |
| } | |
| Core* Image::clone() const | |
| { | |
| Image* twin = ImageBase::clone()->as<Image>(); | |
| twin->m_layoutScaleSeparate = m_layoutScaleSeparate; | |
| if (m_fileAsset != nullptr) | |
| { | |
| twin->setAsset(m_fileAsset); | |
| } | |
| return twin; | |
| } | |
| void Image::setMesh(MeshDrawable* mesh) | |
| { | |
| if (m_Mesh == mesh) | |
| { | |
| return; | |
| } | |
| m_Mesh = mesh; | |
| updateImageScale(); | |
| } | |
| float Image::width() const | |
| { | |
| rive::ImageAsset* asset = this->imageAsset(); | |
| if (asset == nullptr) | |
| { | |
| return 0.0f; | |
| } | |
| rive::RenderImage* renderImage = asset->renderImage(); | |
| if (renderImage == nullptr) | |
| { | |
| return asset->width(); | |
| } | |
| return (float)renderImage->width(); | |
| } | |
| float Image::height() const | |
| { | |
| rive::ImageAsset* asset = this->imageAsset(); | |
| if (asset == nullptr) | |
| { | |
| return 0.0f; | |
| } | |
| rive::RenderImage* renderImage = asset->renderImage(); | |
| if (renderImage == nullptr) | |
| { | |
| return asset->height(); | |
| } | |
| return (float)renderImage->height(); | |
| } | |
| Vec2D Image::measureLayout(float width, | |
| LayoutMeasureMode widthMode, | |
| float height, | |
| LayoutMeasureMode heightMode) | |
| { | |
| // Hug to the intrinsic image size. Only an `exactly` constraint overrides | |
| // the natural size — `atMost` is the available space and must NOT grow a | |
| // hugging image to fill it. | |
| float measuredWidth = | |
| widthMode == LayoutMeasureMode::exactly ? width : Image::width(); | |
| float measuredHeight = | |
| heightMode == LayoutMeasureMode::exactly ? height : Image::height(); | |
| return Vec2D(measuredWidth, measuredHeight); | |
| } | |
| void Image::controlSize(Vec2D size, | |
| LayoutScaleType widthScaleType, | |
| LayoutScaleType heightScaleType, | |
| LayoutDirection direction) | |
| { | |
| // We store layout width/height because the image asset may not be available | |
| // yet (referenced images) and we have defer controlling its size | |
| if (m_layoutWidth != size.x || m_layoutHeight != size.y) | |
| { | |
| m_layoutWidth = size.x; | |
| m_layoutHeight = size.y; | |
| updateImageScale(); | |
| } | |
| } | |
| Vec2D Image::layoutBaseTranslation(LayoutParticipant* participant) const | |
| { | |
| assert(participant != nullptr); | |
| return Vec2D(participant->resolvedLeft(), participant->resolvedTop()); | |
| } | |
| void Image::composeWorldTransform() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| auto* participant = layoutParticipant(); | |
| if (participant != nullptr && m_ParentTransformComponent != nullptr) | |
| { | |
| // Origin 0: the slot base is just the slot top-left (the image composes | |
| // its origin + fit separately in updateTransform). | |
| Mat2D base = Mat2D::fromTranslation(layoutBaseTranslation(participant)); | |
| m_WorldTransform = | |
| m_ParentTransformComponent->worldTransform() * base * m_Transform; | |
| return; | |
| } | |
| #endif | |
| Super::composeWorldTransform(); | |
| } | |
| void Image::updateConstraints() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| auto* participant = layoutParticipant(); | |
| if (participant != nullptr) | |
| { | |
| participant->applyLayoutConstraints(); | |
| } | |
| #endif | |
| Super::updateConstraints(); | |
| } | |
| LayoutParticipant* Image::layoutParticipant() const | |
| { | |
| for (auto* child : children()) | |
| { | |
| if (child->is<LayoutParticipant>()) | |
| { | |
| return child->as<LayoutParticipant>(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool Image::isParticipatingInLayout() const | |
| { | |
| return layoutParticipant() != nullptr; | |
| } | |
| void Image::updateTransform() | |
| { | |
| Super::updateTransform(); | |
| // Compose the layout fit scale on top of the user transform (innermost), so | |
| // the user's scaleX/scaleY (built by Super) remain free to be animated: | |
| // M = T(offset) * UserLocal * S(fitScale) | |
| m_Transform.scaleByValues(m_layoutScaleX, m_layoutScaleY); | |
| m_Transform[4] += m_layoutOffsetX; | |
| m_Transform[5] += m_layoutOffsetY; | |
| } | |
| void Image::updateImageScale() | |
| { | |
| if (imageAsset() == nullptr) | |
| { | |
| if (m_layoutOffsetX != 0.0f || m_layoutOffsetY != 0.0f) | |
| { | |
| m_layoutOffsetX = 0.0f; | |
| m_layoutOffsetY = 0.0f; | |
| markTransformDirty(); | |
| } | |
| return; | |
| } | |
| float newOffsetX = 0.0f; | |
| float newOffsetY = 0.0f; | |
| auto renderImage = imageAsset()->renderImage(); | |
| if (renderImage != nullptr && !std::isnan(m_layoutWidth) && | |
| !std::isnan(m_layoutHeight)) | |
| { | |
| float imgW = (float)renderImage->width(); | |
| float imgH = (float)renderImage->height(); | |
| float newScaleX, newScaleY; | |
| auto imageFit = static_cast<ImageFit>(fit()); | |
| switch (imageFit) | |
| { | |
| case ImageFit::contain: | |
| { | |
| float s = | |
| std::fmin(m_layoutWidth / imgW, m_layoutHeight / imgH); | |
| newScaleX = newScaleY = s; | |
| break; | |
| } | |
| case ImageFit::cover: | |
| { | |
| float s = | |
| std::fmax(m_layoutWidth / imgW, m_layoutHeight / imgH); | |
| newScaleX = newScaleY = s; | |
| break; | |
| } | |
| case ImageFit::fitWidth: | |
| newScaleX = newScaleY = m_layoutWidth / imgW; | |
| break; | |
| case ImageFit::fitHeight: | |
| newScaleX = newScaleY = m_layoutHeight / imgH; | |
| break; | |
| case ImageFit::none: | |
| newScaleX = newScaleY = 1.0f; | |
| break; | |
| case ImageFit::scaleDown: | |
| { | |
| float s = | |
| std::fmin(m_layoutWidth / imgW, m_layoutHeight / imgH); | |
| s = s < 1.0f ? s : 1.0f; | |
| newScaleX = newScaleY = s; | |
| break; | |
| } | |
| case ImageFit::fill: | |
| case ImageFit::resize: | |
| default: | |
| newScaleX = m_layoutWidth / imgW; | |
| newScaleY = m_layoutHeight / imgH; | |
| break; | |
| } | |
| // Compatibility: for a legacy (controlSized) parent, resize does not | |
| // apply fit/alignment translation offsets (only scale). A participant | |
| // composes the origin-based offset for resize too, so the image fills | |
| // its slot positionally as well as in size. | |
| if (imageFit != ImageFit::resize || isParticipatingInLayout()) | |
| { | |
| float boundsW = imgW; | |
| float boundsH = imgH; | |
| float boundsLeft = -imgW * originX(); | |
| float boundsTop = -imgH * originY(); | |
| if (m_Mesh != nullptr && m_Mesh->type() == MeshType::vertex) | |
| { | |
| // Keep fit behavior stable while editing vertex meshes. | |
| boundsLeft = -imgW * 0.5f; | |
| boundsTop = -imgH * 0.5f; | |
| } | |
| Alignment alignment(alignmentX(), alignmentY()); | |
| float xAlign = (alignment.x() + 1.0f) * 0.5f; | |
| float yAlign = (alignment.y() + 1.0f) * 0.5f; | |
| float scaledLeft = boundsLeft * newScaleX; | |
| float scaledTop = boundsTop * newScaleY; | |
| float widthRemainder = m_layoutWidth - (boundsW * newScaleX); | |
| float heightRemainder = m_layoutHeight - (boundsH * newScaleY); | |
| newOffsetX = -scaledLeft + widthRemainder * xAlign; | |
| newOffsetY = -scaledTop + heightRemainder * yAlign; | |
| } | |
| if (m_layoutScaleSeparate) | |
| { | |
| if (newScaleX != m_layoutScaleX || newScaleY != m_layoutScaleY) | |
| { | |
| m_layoutScaleX = newScaleX; | |
| m_layoutScaleY = newScaleY; | |
| // Fit scale is composed in updateTransform(), so changing it | |
| // must mark the local transform dirty (not just the world | |
| // transform). | |
| markTransformDirty(); | |
| } | |
| } | |
| else | |
| { | |
| // Legacy (pre-7.2): the fit overwrites the user scale. | |
| if (newScaleX != scaleX() || newScaleY != scaleY()) | |
| { | |
| scaleX(newScaleX); | |
| scaleY(newScaleY); | |
| } | |
| } | |
| } | |
| if (newOffsetX != m_layoutOffsetX || newOffsetY != m_layoutOffsetY) | |
| { | |
| m_layoutOffsetX = newOffsetX; | |
| m_layoutOffsetY = newOffsetY; | |
| // Offset is applied in updateTransform(), so changing it must mark the | |
| // local transform dirty (not just world transform). | |
| markTransformDirty(); | |
| } | |
| } | |
| AABB Image::localBounds() const | |
| { | |
| if (imageAsset() == nullptr) | |
| { | |
| return AABB(); | |
| } | |
| return AABB::fromLTWH(-width() * originX(), | |
| -height() * originY(), | |
| width(), | |
| height()); | |
| } | |
| ImageAsset* Image::imageAsset() const { return (ImageAsset*)m_fileAsset.get(); } | |
| ImageSampler Image::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()); | |
| } | |
| // Node values are offset by one, zero means inherit from the asset. | |
| if (samplerFilter() != 0) | |
| { | |
| sampler.filter = filterValue(samplerFilter() - 1); | |
| } | |
| if (samplerWrapX() != 0) | |
| { | |
| sampler.wrapX = wrapValue(samplerWrapX() - 1); | |
| } | |
| if (samplerWrapY() != 0) | |
| { | |
| sampler.wrapY = wrapValue(samplerWrapY() - 1); | |
| } | |
| return sampler; | |
| } | |
| #ifdef TESTING | |
| #include "rive/shapes/mesh.hpp" | |
| Mesh* Image::mesh() const { return static_cast<Mesh*>(m_Mesh); }; | |
| #endif |