pharo-graphics / Bloc

Low-level UI infrastructure & framework for Pharo
MIT License
80 stars 39 forks source link

Rotate Animation isn't centered #521

Open rvillemeur opened 4 weeks ago

rvillemeur commented 4 weeks ago

Hi all,

Do you know how can I make my rotate animation centered on my element ? When rotating a square, it seems the corner translates while the full element rotates around it creating some kind of offset we can see on this snippet .

elt := BlElement new background: (Color red alpha: 0.5); position: 100 asPoint; size: 100 asPoint.

frame := BlElement new background: Color yellow; position: 100 asPoint; size: 100 asPoint.

container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: {frame. elt}.

anim := (BlTransformAnimation rotate: 90) duration: 1 second.

elt addEventHandlerOn: BlClickEvent do: [ elt addAnimation: anim copy ].

container openInSpace

Resulting at some point at this state :

image

I saw I can use rotateBy: around: on the transformation builder of my BlTransformAnimation but I can't make it work as it seems to need a 3D Vector

Any idea ?

Thanks, Enzo

rvillemeur commented 4 weeks ago

Hi Enzo

Thanks for this great question, I had some fun figuring out what was going on. Short answers, your element is already centered on your element.

Let me show you what's going on by playing a little bit with your example. I have changed the geometry to rectangle, to better figure out what was going on.

I also expanded the BlTransformAnimation with transformDo: to better control the parameter. If you comment out the origin: you'll find back your original behaviour. If you specify the origin:, you'll notice that the element is rotating around this point. By default, this point is the center of your element.

Then come the animation phase where you can see your element rotating. This is managed by the easing: method. By default, it's BlEasing linear. For the fun, change this to BlEasing bounceOut or other BlEasing subclass, and you'll notice a slight difference in the way your rotation is displayed.

You're tricked by how easing compute your element bounds position during the animation. It's centered, but the position of its bounds as returned by the easing method make it looks like its not rotating around it. I don't have any straightforward solution for this.

elt := BlElement new background: (Color red alpha: 0.5); position: 100 asPoint; geometry:BlRectangleGeometry new; size: 50@100.

frame := BlElement new background: Color yellow; position: 100 asPoint; geometry:BlRectangleGeometry new; size: 50@100.

container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: {frame. elt}.

anim := BlTransformAnimation new
      transformDo: [ :aBuilder |
      aBuilder origin:( BlAffineTransformationPositionOrigin position: 10@10);
rotateBy: 90];
easing:(BlEasing linear);
      duration: 1 seconds.

elt addEventHandlerOn: BlClickEvent do: [ elt addAnimation: anim copy ].

container openInSpace

Last, rotateBy: around: would specify the axis around which your element rotate. By default, it's the Z-Azis, perpendicular to your screen. X and Y axis doesn't seems to be implemented, as I got error when I tried them. If you need rotation around those axis, you probably need another way, or implement the missing methods.

rvillemeur commented 4 weeks ago

Hi Enzo

As a (simplified) test, I have created a custom animation for element rotation.

BlAnimation << #BlRotateAnimation slots: { #angle }; tag: 'Animation'; package: 'BookletGraphics'

BlRotateAnimation >> angle: anAngle angle := anAngle

BlRotateAnimation >> applyValue: anAngle self target transformDo: [ :t | t rotateBy: anAngle ]

BlRotateAnimation >> valueForStep: aNumber ^ (angle * aNumber)

You then update your example with:

|elt frame container anim|
elt := BlElement new background: (Color red alpha: 0.5); position: 100 asPoint; size: 100 asPoint.
frame := BlElement new background: Color yellow; position: 100 asPoint; size: 100 asPoint.
container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: {frame. elt}.

anim := BlRotateAnimation new angle: 90; duration: 1 second.

elt addEventHandlerOn: BlClickEvent do: [ elt addAnimation: anim copy ].

container openInSpace

I think the result is what you were expected. Code is in Booklet-Graphics-Code repository.

rvillemeur commented 4 weeks ago

Hi, We can see that the animation does not produce the same effect that if we do the transformation in steps programmatically (outside bloc animations) by hand. Basically what we have seen is the following:

1) Rotations of an element in the origin requires two transformations: (1) transposing the origin to 0@0 (local transformation in Bloc terms) and (2) the rotation itself. Both operations are encoded in a single Affine matrix 2) To perform the animation an initial and final matrix are calculated, and then the matrix are interpolatted from the initial to the final.

3) This means that the interpolation not only affect the rotation angle but the transpose, making the result not as expected.

4) As a solution, we need to interpolate the transformation objects and generate a new matrix in each step of the animation.

rvillemeur commented 4 weeks ago

I think BlTransformAnimation is overcomplicating things by applying multiple transformation at once during animation phase, with limited benefit (or I don't understand it completely). It only allows rotation, scaling or translation of element, but only one at a time. However the position is computed in valueForStep: using BlElementAbsoluteTransformation which try to interpolate value for all 3 transformations, even if you can only specify one when creating your animation.

However, BlTransformationBuilder allows much more, by combining scaling, rotating and translating element, as well as specifying the origin of the transformation. One simply need to add transformation to the element, specify its origin, and then we could do transformDo: [ :t | t origin: origin; rotateBy: anAngle ] to achieve similar result, without the graphical effect produced by BlElementAbsoluteTransformation

When I use rotateBy:, it's using an affine transformation:

rotateBy: aDegreesAngle

    affineCompositeTransformation addTransformation:
        (BlRotationTransformation new
             angle: aDegreesAngle;
             axis: BlVector zAxis;
             origin: origin;
             yourself)

We could redefine BlTransformAnimation to allow all transformation and only compute those which are necessary

rvillemeur commented 2 weeks ago

I took a deeper look at how transformation are done in Bloc and understand why we have such difference.

Let's take the initial step-up to show how it works:

| elt frame container anim |
elt := BlElement new    background: (Color red alpha: 0.5);    position: 100 asPoint;    size: 100 asPoint.
frame := BlElement new background: Color yellow; position: 100 asPoint; size: 100 asPoint.
container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: { frame. elt }.

Transformation are affine transformation. For more detail, you can search on the internet, there are countless references to it. To simplify it, I'll say we apply a transformation matrix (BlMatrix2D) to all point of our figure path, each point represented as BlVector.

You have 3 type of tranformation available in Bloc:

When you're doing a transformation using transformDo:, you'll, by default, use BlElementLocalTransformation. The origin will be set to BlAffineTransformationCenterOrigin.

For an element rotated to 45 degree, it'll look like:

elt transformation: (BlElementLocalTransformation newWith: (
(BlRotationTransformation new angle: 45) 
origin: (BlAffineTransformationCenterOrigin defaultInstance ) )).

image

However, BlTransformAnimation will use BlElementAbsoluteTransformation to manage the transformation matrix. This transformation works by updating its trasformation matrix, from IdentityMatrix, to target matrix, through interpolation computed at each animation step (fromMatrix interpolate: aNumber to: toMatrix)

This transformation doesn't use an origin point. Instead, It'll apply a translation, so that your shape still look like it's in the element bound. This translation is added to your transformation definition (boundingRectangle:)

So, for the sample 90 degree rotation used as an example, the tranformation will look like below in mid-way (45 degrees rotation, and translation of 50px on 100px)

elt transformation: (BlElementAbsoluteTransformation 
matrix: (BlMatrix2D new 
x: 50 ;y: 0; 
shx: 45 degreesToRadians sin * -1 ; shy: 45 degreesToRadians sin ; 
sx: 45 degreesToRadians cos; sy: 45 degreesToRadians cos )   ).

image

You can notice that for a similar rotation, the position of orange square is not the same.

Now, why do we need to use a BlElementAbsoluteTransformation for BlTransformAnimation. My assumption is that this animation must managed all 3 basic transformation: rotation, translation and scale. Each tranformation will happen gradually during animation, with new position computed at each step.

You can specify a default generic value of 0 for rotation and translation, so if you don't specifiy it, it won't affect your element (At each step, 0*StepValue = 0). However, for scaling, if you don't specify it, you need to keep it's value at 1. Matrix interpolation is one possibility to manage this.

If you want to have an exact rotation animation, it's better to define your own animation to manage only rotation, like I did in a previous comment.