Skip to content

Commit 358ddba

Browse files
philterphilter
andcommitted
feat: Image Fit & Alignment when parented by Layout (#12057) ca2ce7d096
Adds support for fit type and alignment for Image, allowing images to resize while maintaining aspect ratio. This is ONLY supported when an image has a layout as its parent where the layout parent acts as a "container". Validated that the updates work if an n-slice or mesh is applied to an image using these fit/alignment types. Co-authored-by: Philip Chung <philterdesign@gmail.com>
1 parent 98b098a commit 358ddba

16 files changed

Lines changed: 380 additions & 10 deletions

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
19c486654acdbb64d5451496d2cd0bfd95961567
1+
ca2ce7d0967d22340e49d135e400b6310de0c7c2

dev/defs/shapes/image.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"int": 206,
1616
"string": "assetid"
1717
},
18-
"description": "Image drawable for an image asset"
18+
"description": "Image drawable for an image asset",
19+
"bindable": true
1920
},
2021
"originX": {
2122
"type": "double",
@@ -36,6 +37,30 @@
3637
"string": "originy"
3738
},
3839
"description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
40+
},
41+
"fit": {
42+
"type": "uint",
43+
"key": {
44+
"int": 974,
45+
"string": "fit"
46+
},
47+
"description": "Fit type for the Image, determines how the image resizes within its layout container."
48+
},
49+
"alignmentX": {
50+
"type": "double",
51+
"key": {
52+
"int": 975,
53+
"string": "alignmentx"
54+
},
55+
"description": "Alignment value on X, determines how the image aligns horizontally in its layout container."
56+
},
57+
"alignmentY": {
58+
"type": "double",
59+
"key": {
60+
"int": 976,
61+
"string": "alignmenty"
62+
},
63+
"description": "Alignment value on Y, determines how the image aligns vertically in its layout container."
3964
}
4065
}
4166
}

include/rive/generated/core_registry.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,9 @@ class CoreRegistry
14021402
case ImageBase::assetIdPropertyKey:
14031403
object->as<ImageBase>()->assetId(value);
14041404
break;
1405+
case ImageBase::fitPropertyKey:
1406+
object->as<ImageBase>()->fit(value);
1407+
break;
14051408
case FocusDataBase::edgeBehaviorValuePropertyKey:
14061409
object->as<FocusDataBase>()->edgeBehaviorValue(value);
14071410
break;
@@ -2320,6 +2323,12 @@ class CoreRegistry
23202323
case ImageBase::originYPropertyKey:
23212324
object->as<ImageBase>()->originY(value);
23222325
break;
2326+
case ImageBase::alignmentXPropertyKey:
2327+
object->as<ImageBase>()->alignmentX(value);
2328+
break;
2329+
case ImageBase::alignmentYPropertyKey:
2330+
object->as<ImageBase>()->alignmentY(value);
2331+
break;
23232332
case CubicDetachedVertexBase::inRotationPropertyKey:
23242333
object->as<CubicDetachedVertexBase>()->inRotation(value);
23252334
break;
@@ -2950,6 +2959,8 @@ class CoreRegistry
29502959
return object->as<PolygonBase>()->points();
29512960
case ImageBase::assetIdPropertyKey:
29522961
return object->as<ImageBase>()->assetId();
2962+
case ImageBase::fitPropertyKey:
2963+
return object->as<ImageBase>()->fit();
29532964
case FocusDataBase::edgeBehaviorValuePropertyKey:
29542965
return object->as<FocusDataBase>()->edgeBehaviorValue();
29552966
case DrawRulesBase::drawTargetIdPropertyKey:
@@ -3598,6 +3609,10 @@ class CoreRegistry
35983609
return object->as<ImageBase>()->originX();
35993610
case ImageBase::originYPropertyKey:
36003611
return object->as<ImageBase>()->originY();
3612+
case ImageBase::alignmentXPropertyKey:
3613+
return object->as<ImageBase>()->alignmentX();
3614+
case ImageBase::alignmentYPropertyKey:
3615+
return object->as<ImageBase>()->alignmentY();
36013616
case CubicDetachedVertexBase::inRotationPropertyKey:
36023617
return object->as<CubicDetachedVertexBase>()->inRotation();
36033618
case CubicDetachedVertexBase::inDistancePropertyKey:
@@ -3918,6 +3933,7 @@ class CoreRegistry
39183933
case ClippingShapeBase::fillRulePropertyKey:
39193934
case PolygonBase::pointsPropertyKey:
39203935
case ImageBase::assetIdPropertyKey:
3936+
case ImageBase::fitPropertyKey:
39213937
case FocusDataBase::edgeBehaviorValuePropertyKey:
39223938
case DrawRulesBase::drawTargetIdPropertyKey:
39233939
case LayoutComponentBase::styleIdPropertyKey:
@@ -4216,6 +4232,8 @@ class CoreRegistry
42164232
case StarBase::innerRadiusPropertyKey:
42174233
case ImageBase::originXPropertyKey:
42184234
case ImageBase::originYPropertyKey:
4235+
case ImageBase::alignmentXPropertyKey:
4236+
case ImageBase::alignmentYPropertyKey:
42194237
case CubicDetachedVertexBase::inRotationPropertyKey:
42204238
case CubicDetachedVertexBase::inDistancePropertyKey:
42214239
case CubicDetachedVertexBase::outRotationPropertyKey:
@@ -4641,6 +4659,8 @@ class CoreRegistry
46414659
return object->is<PolygonBase>();
46424660
case ImageBase::assetIdPropertyKey:
46434661
return object->is<ImageBase>();
4662+
case ImageBase::fitPropertyKey:
4663+
return object->is<ImageBase>();
46444664
case FocusDataBase::edgeBehaviorValuePropertyKey:
46454665
return object->is<FocusDataBase>();
46464666
case DrawRulesBase::drawTargetIdPropertyKey:
@@ -5227,6 +5247,10 @@ class CoreRegistry
52275247
return object->is<ImageBase>();
52285248
case ImageBase::originYPropertyKey:
52295249
return object->is<ImageBase>();
5250+
case ImageBase::alignmentXPropertyKey:
5251+
return object->is<ImageBase>();
5252+
case ImageBase::alignmentYPropertyKey:
5253+
return object->is<ImageBase>();
52305254
case CubicDetachedVertexBase::inRotationPropertyKey:
52315255
return object->is<CubicDetachedVertexBase>();
52325256
case CubicDetachedVertexBase::inDistancePropertyKey:

include/rive/generated/shapes/image_base.hpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ class ImageBase : public Drawable
3737
static const uint16_t assetIdPropertyKey = 206;
3838
static const uint16_t originXPropertyKey = 380;
3939
static const uint16_t originYPropertyKey = 381;
40+
static const uint16_t fitPropertyKey = 974;
41+
static const uint16_t alignmentXPropertyKey = 975;
42+
static const uint16_t alignmentYPropertyKey = 976;
4043

4144
protected:
4245
uint32_t m_AssetId = -1;
4346
float m_OriginX = 0.5f;
4447
float m_OriginY = 0.5f;
48+
uint32_t m_Fit = 0;
49+
float m_AlignmentX = 0.0f;
50+
float m_AlignmentY = 0.0f;
4551

4652
public:
4753
inline uint32_t assetId() const { return m_AssetId; }
@@ -77,12 +83,48 @@ class ImageBase : public Drawable
7783
originYChanged();
7884
}
7985

86+
inline uint32_t fit() const { return m_Fit; }
87+
void fit(uint32_t value)
88+
{
89+
if (m_Fit == value)
90+
{
91+
return;
92+
}
93+
m_Fit = value;
94+
fitChanged();
95+
}
96+
97+
inline float alignmentX() const { return m_AlignmentX; }
98+
void alignmentX(float value)
99+
{
100+
if (m_AlignmentX == value)
101+
{
102+
return;
103+
}
104+
m_AlignmentX = value;
105+
alignmentXChanged();
106+
}
107+
108+
inline float alignmentY() const { return m_AlignmentY; }
109+
void alignmentY(float value)
110+
{
111+
if (m_AlignmentY == value)
112+
{
113+
return;
114+
}
115+
m_AlignmentY = value;
116+
alignmentYChanged();
117+
}
118+
80119
Core* clone() const override;
81120
void copy(const ImageBase& object)
82121
{
83122
m_AssetId = object.m_AssetId;
84123
m_OriginX = object.m_OriginX;
85124
m_OriginY = object.m_OriginY;
125+
m_Fit = object.m_Fit;
126+
m_AlignmentX = object.m_AlignmentX;
127+
m_AlignmentY = object.m_AlignmentY;
86128
Drawable::copy(object);
87129
}
88130

@@ -99,6 +141,15 @@ class ImageBase : public Drawable
99141
case originYPropertyKey:
100142
m_OriginY = CoreDoubleType::deserialize(reader);
101143
return true;
144+
case fitPropertyKey:
145+
m_Fit = CoreUintType::deserialize(reader);
146+
return true;
147+
case alignmentXPropertyKey:
148+
m_AlignmentX = CoreDoubleType::deserialize(reader);
149+
return true;
150+
case alignmentYPropertyKey:
151+
m_AlignmentY = CoreDoubleType::deserialize(reader);
152+
return true;
102153
}
103154
return Drawable::deserialize(propertyKey, reader);
104155
}
@@ -107,6 +158,9 @@ class ImageBase : public Drawable
107158
virtual void assetIdChanged() {}
108159
virtual void originXChanged() {}
109160
virtual void originYChanged() {}
161+
virtual void fitChanged() {}
162+
virtual void alignmentXChanged() {}
163+
virtual void alignmentYChanged() {}
110164
};
111165
} // namespace rive
112166

include/rive/shapes/image.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class Image : public ImageBase, public FileAssetReferencer
2020
// and use the image width/height to compute the proper scale
2121
float m_layoutWidth = NAN;
2222
float m_layoutHeight = NAN;
23+
float m_layoutOffsetX = 0.0f;
24+
float m_layoutOffsetY = 0.0f;
2325
void updateImageScale();
2426

2527
public:
@@ -44,6 +46,7 @@ class Image : public ImageBase, public FileAssetReferencer
4446
float height() const;
4547
void assetUpdated() override;
4648
AABB localBounds() const override;
49+
void updateTransform() override;
4750

4851
#ifdef TESTING
4952
Mesh* mesh() const;

src/shapes/image.cpp

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "rive/importers/backboard_importer.hpp"
55
#include "rive/assets/file_asset.hpp"
66
#include "rive/assets/image_asset.hpp"
7+
#include "rive/layout.hpp"
78
#include "rive/layout/n_slicer.hpp"
89
#include "rive/shapes/mesh_drawable.hpp"
910
#include "rive/artboard.hpp"
@@ -135,7 +136,15 @@ Core* Image::clone() const
135136
return twin;
136137
}
137138

138-
void Image::setMesh(MeshDrawable* mesh) { m_Mesh = mesh; }
139+
void Image::setMesh(MeshDrawable* mesh)
140+
{
141+
if (m_Mesh == mesh)
142+
{
143+
return;
144+
}
145+
m_Mesh = mesh;
146+
updateImageScale();
147+
}
139148

140149
float Image::width() const
141150
{
@@ -218,27 +227,119 @@ void Image::controlSize(Vec2D size,
218227
}
219228
}
220229

230+
void Image::updateTransform()
231+
{
232+
Super::updateTransform();
233+
m_Transform[4] += m_layoutOffsetX;
234+
m_Transform[5] += m_layoutOffsetY;
235+
}
236+
221237
void Image::updateImageScale()
222238
{
223-
// User-created meshes are not affected by scale
224-
if ((m_Mesh != nullptr && m_Mesh->type() == MeshType::vertex) ||
225-
imageAsset() == nullptr)
239+
if (imageAsset() == nullptr)
226240
{
241+
if (m_layoutOffsetX != 0.0f || m_layoutOffsetY != 0.0f)
242+
{
243+
m_layoutOffsetX = 0.0f;
244+
m_layoutOffsetY = 0.0f;
245+
markTransformDirty();
246+
}
227247
return;
228248
}
249+
250+
float newOffsetX = 0.0f;
251+
float newOffsetY = 0.0f;
229252
auto renderImage = imageAsset()->renderImage();
230253
if (renderImage != nullptr && !std::isnan(m_layoutWidth) &&
231254
!std::isnan(m_layoutHeight))
232255
{
233-
float newScaleX = m_layoutWidth / (float)renderImage->width();
234-
float newScaleY = m_layoutHeight / (float)renderImage->height();
256+
float imgW = (float)renderImage->width();
257+
float imgH = (float)renderImage->height();
258+
float newScaleX, newScaleY;
259+
auto imageFit = static_cast<Fit>(fit());
260+
switch (imageFit)
261+
{
262+
case Fit::fill:
263+
newScaleX = m_layoutWidth / imgW;
264+
newScaleY = m_layoutHeight / imgH;
265+
break;
266+
case Fit::contain:
267+
{
268+
float s =
269+
std::fmin(m_layoutWidth / imgW, m_layoutHeight / imgH);
270+
newScaleX = newScaleY = s;
271+
break;
272+
}
273+
case Fit::cover:
274+
{
275+
float s =
276+
std::fmax(m_layoutWidth / imgW, m_layoutHeight / imgH);
277+
newScaleX = newScaleY = s;
278+
break;
279+
}
280+
case Fit::fitWidth:
281+
newScaleX = newScaleY = m_layoutWidth / imgW;
282+
break;
283+
case Fit::fitHeight:
284+
newScaleX = newScaleY = m_layoutHeight / imgH;
285+
break;
286+
case Fit::none:
287+
newScaleX = newScaleY = 1.0f;
288+
break;
289+
case Fit::scaleDown:
290+
{
291+
float s =
292+
std::fmin(m_layoutWidth / imgW, m_layoutHeight / imgH);
293+
s = s < 1.0f ? s : 1.0f;
294+
newScaleX = newScaleY = s;
295+
break;
296+
}
297+
case Fit::layout:
298+
default:
299+
newScaleX = m_layoutWidth / imgW;
300+
newScaleY = m_layoutHeight / imgH;
301+
break;
302+
}
303+
304+
// Compatibility: legacy files assume fill does not apply fit/alignment
305+
// translation offsets, only scale.
306+
if (imageFit != Fit::fill)
307+
{
308+
float boundsW = imgW;
309+
float boundsH = imgH;
310+
float boundsLeft = -imgW * originX();
311+
float boundsTop = -imgH * originY();
312+
if (m_Mesh != nullptr && m_Mesh->type() == MeshType::vertex)
313+
{
314+
// Keep fit behavior stable while editing vertex meshes.
315+
boundsLeft = -imgW * 0.5f;
316+
boundsTop = -imgH * 0.5f;
317+
}
318+
Alignment alignment(alignmentX(), alignmentY());
319+
float xAlign = (alignment.x() + 1.0f) * 0.5f;
320+
float yAlign = (alignment.y() + 1.0f) * 0.5f;
321+
float scaledLeft = boundsLeft * newScaleX;
322+
float scaledTop = boundsTop * newScaleY;
323+
float widthRemainder = m_layoutWidth - (boundsW * newScaleX);
324+
float heightRemainder = m_layoutHeight - (boundsH * newScaleY);
325+
newOffsetX = -scaledLeft + widthRemainder * xAlign;
326+
newOffsetY = -scaledTop + heightRemainder * yAlign;
327+
}
328+
235329
if (newScaleX != scaleX() || newScaleY != scaleY())
236330
{
237331
scaleX(newScaleX);
238332
scaleY(newScaleY);
239-
addDirt(ComponentDirt::WorldTransform, false);
240333
}
241334
}
335+
if (newOffsetX != m_layoutOffsetX || newOffsetY != m_layoutOffsetY)
336+
{
337+
m_layoutOffsetX = newOffsetX;
338+
m_layoutOffsetY = newOffsetY;
339+
// Offset is applied in updateTransform(), so changing it must mark the
340+
// local transform dirty (not just world transform).
341+
markTransformDirty();
342+
}
242343
}
243344

244345
AABB Image::localBounds() const
201 KB
Binary file not shown.
202 KB
Binary file not shown.

0 commit comments

Comments
 (0)