There are more concrete things we can do than the very general approach to diagonalizing your matrix, supposing you only do scaling, rotating, and translating : no shearing (a.k.a. skewing).
Reflection is (a composition of rotations, translations, and) scaling by -1 along one coordinate, so we can safely consider it. If your transformation includes shearing, the easiest way is to refer to MvG's answer.
However if you know the start and end shear values, you may also use A0
and A1
below as your start and end matrices by removing the shear (multiplying them by the matrix of opposite shear at both points) and then use the matrix defined below multiplied by an interpolated shear.
Pieces of the puzzle
With our assumptions, any transformation matrix A is given and can be decomposed as follows :
| a c tx | | i*x -i*y u |
A = | b d ty | = | j*y j*x v |
| 0 0 1 | | 0 0 1 |
Where x and y are the respective sine and cosine of the final rotation angle. So to get the translating and scaling values, we can do the following :
u = A.tx;
v = A.ty;
i = Math.sqrt(A.a * A.a + A.c * A.c);
j = Math.sqrt(A.b * A.b + A.d * A.d);
Now we need to know by how much to rotate. Suppose for each t
in 0..1
, you have a rotation R(t)
such that R(0) = matrix(a=x0, b=y0, c=y0, d=x0)
is the rotation in your initial matrix A0
, and similarly R(1) = matrix(a=x1, b=y1, c=y1, d=x1)
is the rotation in your final matrix A1
Then the total rotation matrix you will be applying from the beginning to the end of the movement is T = R(0)^-1 R(1)
and since R
is a rotation, its inverse is its transposed. Thus :
| x0 y0 0 | | x1 -y1 0 | | x0*x1 + y0*x1 -x0*y1 + y0*x1 0 |
T = | -y0 x0 0 | x | y1 x1 0 | = |-y0*x1 + x0*y1 y0*y1 + x0*x1 0 |
| 0 0 1 | | 0 0 1 | | 0 0 1 |
This is still a rotation matrix. Thus we can get the angle of rotation of T :
angle = atan2( x0 * y1 - y0 * x1, y0 * y1 + x0 * x1 )
which can be rewritten, by scaling both sides by j0*j1
(defined respectively from A0 and A1 as j is defined from A above. Scaling both components does not modify the returned value of atan2). :
angle = atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d)
Putting them together
Thus, supposing you have to matrices, A0
and A1
, you start with the rotation at A0 and do an easy linear interpolation on the scaling and translating coordinates. Then you apply the partial rotation that you want, as a fraction of angle
.
// compute i1 / i0 and j1 / j0
i1_0 = Math.sqrt( (A1.a * A1.a + A1.c * A1.c) / (A0.a * A0.a + A0.c * A0.c) );
j1_0 = Math.sqrt( (A1.b * A1.b + A1.d * A1.d) / (A0.b * A0.b + A0.d * A0.d) );
angle = Math.atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d);
for (i = 0; i < MAX; i++)
{
t = i/MAX;
// the values of i and j at t are scaled by i and j at 0
// in order to reuse easily A0's values, which include initial rotation
it = (1-t) + t * i1_0;
jt = (1-t) + t * j1_0;
At = new Matrix(
a = A0.a * it,
b = A0.b * jt,
c = A0.c * it,
d = A0.d * jt,
tx = (1-t) * A0.tx + t * A1.tx,
ty = (1-t) * A0.ty + t * A1.ty
);
At.rotate( t * angle );
// now apply matrix At instead of A0 or A1
obj.transform.matrix = At;
}
Obviously, if you just want the matrix that is halfway, use only one t with a value of 0.5. We could get more clever and get half the rotation of T using Carnot's linearization formulas for double angles, thus without ever calling trigonometric formulas, but I don't think it's really worth it.
This code is also easily generalizable to any other programming language since it only uses very common functions.
The easiest way to showcase this is to include a javascript test snippet, see below. I have used actionscript naming conventions for coherence with the rest of the post (i.e. the .a .tx and so on).
function rotate_mat(A, angle)
{
var x = Math.cos(angle);
var y = Math.sin(angle);
var B = {a:x*A.a+y*A.c, b:x*A.b+y*A.d, c:-y*A.a+x*A.c, d:-y*A.b+x*A.d, tx:A.tx, ty:A.ty};
return B;
}
function apply(id,A)
{
var mat="matrix("+A.a+","+A.b+","+A.c+","+A.d+","+A.tx+","+A.ty+")";
document.getElementById(id).style.transform=mat;
}
function val(str) { return document.getElementById(str).value; }
document.getElementById("go").onclick = function(options)
{
var A0 = {a:val("a0"), b:val("b0"), c:val("c0"), d:val("d0"), tx:val("tx0"), ty:val("ty0")};
var A1 = {a:val("a1"), b:val("b1"), c:val("c1"), d:val("d1"), tx:val("tx1"), ty:val("ty1")};
apply("guineapig", A0);
var i1_0 = Math.sqrt( (A1.a * A1.a + A1.c * A1.c) / (A0.a * A0.a + A0.c * A0.c) );
var j1_0 = Math.sqrt( (A1.b * A1.b + A1.d * A1.d) / (A0.b * A0.b + A0.d * A0.d) );
var angle = Math.atan2(A0.d*A1.b - A0.b*A1.d, A0.b*A1.b + A0.d*A1.d);
var timer;
var MAX = +val("MAX");
var i = 0;
function update()
{
if( ++i == MAX )
clearInterval(timer);
var t = i/MAX;
// the values of i and j at t are scaled by i and j at 0
// in order to reuse easily A0's values, which include initial rotation
var it = 1-t + t * i1_0;
var jt = 1-t + t * j1_0;
At = {
a : A0.a * it,
b : A0.b * jt,
c : A0.c * it,
d : A0.d * jt,
tx : (1-t) * A0.tx + t * A1.tx,
ty : (1-t) * A0.ty + t * A1.ty
};
//At.rotate( t * angle );
At = rotate_mat(At, t * angle);
// now apply matrix At instead of A0 or A1
//obj.transform.matrix = At;
apply("guineapig", At);
}
var step = +val("step");
setTimeout(function(){timer=setInterval(update,step);},10*step);
};
<p>Matrix A0 :
<label>a<input type="text" size="2" id ="a0" value="1" /></label>
<label>b<input type="text" size="2" id ="b0" value="0" /></label>
<label>c<input type="text" size="2" id ="c0" value="0" /></label>
<label>d<input type="text" size="2" id ="d0" value="1" /></label>
<label>tx<input type="text" size="2" id ="tx0" value="0" /></label>
<label>ty<input type="text" size="2" id ="ty0" value="0" /></label>
</p>
<p>Matrix A1 :
<label>a<input type="text" size="2" id ="a1" value="1.41421356237" /></label>
<label>b<input type="text" size="2" id ="b1" value="1.41421356237" /></label>
<label>c<input type="text" size="2" id ="c1" value="-1.41421356237" /></label>
<label>d<input type="text" size="2" id ="d1" value="1.41421356237" /></label>
<label>tx<input type="text" size="2" id ="tx1" value="40" /></label>
<label>ty<input type="text" size="2" id ="ty1" value="40" /></label>
</p>
<p><label>frames<input type="text" size="2" id="MAX" value="100" /></label><label>interval (ms)<input type="text" size="2" id="step" value="10" /></label><button id="go">Animate !</button></p>
<div id="c" style="height:200px;width:200px;border:thin blue solid">
<div id="guineapig" style="height:40px;width:40px;position:relative;top:80px;left:80px;background:green;" />
</div>
PS : You definitely need the homogeneous coordinates MvG mentions, otherwise you risk ending up with an interpolation movement that looks more like an arc of a spiral rather than a rotation and scaling along a linear movement.
Since Adobe's documentation is not very clear on that, and I know for a fact this happens with SVG transform matrices, here is how to compensate for that : apply the transformation to the x
and y
coordinates, and add the result to the translate values u
and v
, while setting x
and y
to 0. Thus, before you do anything :
correction = A0.transformPoint(new Point(x=obj.x, y=obj.y));
A0.tx += correction.x;
A0.ty += correction.y;
correction = A1.transformPoint(new Point(x=obj.x, y=obj.y));
A1.tx += correction.x;
A1.ty += correction.y;
obj.x = 0;
obj.y = 0;
Note that you can save these values x
and y
to undo this trick once your animation is done, if you want. But to be honest, once you have transformations, coordinates are superfluous.
Theta=(Alpha1 + Alpha2)/2
and Cos(Theta), +/-Sin(Theta) as matrix elements – MBow
is 1 in all your matrices. – Vesper