Using AffineTransform::scaled on a child component results in the component’s position changing

I have a child Component of my mainComponent that I want to apply a scaling to, increasing its overall size to 150%.

Just for testing purposes, I have it hooked up to a button so that turning on the button applies the transform, and turning it off removes it.

In the Constructor of the mainComponent:

    addAndMakeVisible(rifBufferDisplay);		// the child component to be scaled

    addAndMakeVisible(testAffine_but);                   // a TextButton
    testAffine_but.setClickingTogglesState(true);

    testAffine_but.onClick = [this] {
        if (testAffine_but.getToggleState())
            rifBufferDisplay.setTransform(AffineTransform().scaled(1.5f));
        else
            rifBufferDisplay.setTransform(AffineTransform());
    };

When I click the button, it indeed scales the child up to 150% size, but it then moves it down and to the right in position. The top/left corner does not stay in the same place.

In my mainComponent’s resized(), am I supposed to do something else depending on scaling? I am simply setting the bounds of the child component based on its original size.

What am I missing here? Thanks.

AffiineTransform scales from the centre. Not sure about your use case, or if that’s desirable (doesn’t sound like it!), but it likely means you’ll need to translate it back to where you need the component.

Also, there’s a good chance you’ll have to do that exclusively from resized() in your parent component, meaning you’ll need to call resized instead of setting the transform at an adhoc time. This will give you the flexibility to set up the bounds correctly, and then scale+translate as needed, all from one place.

How would the button then tell the component to update stuff if the transformation matrix is only changed in resized? Would calling repaint be the proper way?

The link is fairly straightforward: since you pass this into your lambda, you can start calling the parent component’s functions.

testAffine_but.onClick = [this] { resized(); };

The logic you had would be moved to resized, as I mentioned. And there shouldn’t be a need to call repaint (because resized should trigger that already).

That’s not what it seems to be doing. It seems to be related to the position in the parent component. I figured out the offset, but I’m not sure I understand it.

If a component is specified in the parent’s resized() to be at

x = 20, y = 40

Then with an AffineTransform::scale of 2.0f it “appears” to be at

x = 40, y = 80

(even though getBounds() shows it is still at 20,40). With a scale of 1.5f it appears to be at

x = 30, y = 60;

So you take 1.0f minus the amount of the scaling, and multiply the x and y positions by it, you can shift position (AffineTransform::translation) by the result and the component stays in the same location.

With this the component changes size, but the top/left point stays in the same place. (I did move this to the parent component’s resized()):

// testAffine_num is a Slider specifying a scaleAmt of 0.0f to 3.0f

    if (approximatelyEqual(testAffine_num.getValue(), 1.0))
        rifBufferDisplay.setTransform(AffineTransform());
    else
    {
        auto scaleAmt = (float) testAffine_num.getValue();    // i.e 1.5f
        auto bounds = rifBufferDisplay.getBounds().toFloat();
        rifBufferDisplay.setTransform(AffineTransform::scale(scaleAmt).
            followedBy(AffineTransform::translation(bounds.getX() * (1.0f - scaleAmt), bounds.getY() * (1.0f - scaleAmt))));
     }

The JUCE docs say:

scale() [2/3]

static AffineTransform AffineTransform::scale ( float factor ) staticnoexcept

Returns a new transform which is a re-scale about the origin.

Perhaps origin refers to 0,0 in the parentComponent?

1 Like

I guess I should just read the documentation: :flushed:

setTransform()

void Component::setTransform ( const AffineTransform & transform )

Sets a transform matrix to be applied to this component.

If you set a transform for a component, the component’s position will be warped by it, relative to the component’s parent’s top-left origin. This means that the values you pass into setBounds() will no longer reflect the actual area within the parent that the component covers, as the bounds will be transformed and the component will probably end up actually appearing somewhere else within its parent.

Because of this, I put Components that I want to scale inside (at 0, 0) another Component which retains its 1.0 scale and resizes itself according to the actual size of the component it contains. This way, I can apply whatever scale transform to the contained Component, and it just changes its size remaining at its apparent location within the main UI

1 Like