Skip to content

Commit 9bba250

Browse files
authored
Refactor transform animations (#4296)
* feat: add matrix module * fix(animations): parse transform property correctly * fix(css-animations): compute transformation value with matrix * refactor: add typings for keyframes array in style scope * fix(animations): transform regex and method invocation * fix(matrix): rewrite decomposition function * refactor: transform animations parse * test: add tests for css animation transform * refactor: move transformConverter to style-properties * lint: remove unnecessary comma * lint: remove unnecessary word in d.ts * fix(style-properties): correctly use transformConverter * fix(matrix): flat multiply affine 2d matrices cc @PanayotCankov
1 parent c228b97 commit 9bba250

15 files changed

Lines changed: 637 additions & 430 deletions

‎tests/app/ui/animation/css-animation-tests.ts‎

Lines changed: 180 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import * as color from "tns-core-modules/color";
99

1010
import {SelectorCore} from "tns-core-modules/ui/styling/css-selector";
1111

12-
//import * as styling from "ui/styling";
12+
const DELTA = 1;
13+
const SCALE_DELTA = 0.001;
1314

1415
function createAnimationFromCSS(css: string, name: string): keyframeAnimation.KeyframeAnimationInfo {
1516
let scope = new styleScope.StyleScope();
@@ -113,72 +114,179 @@ export function test_ReadKeyframe() {
113114
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "backgroundColor", "Keyframe declarations are not correct");
114115
}
115116

117+
export function test_ReadTransformAllSet() {
118+
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(10) scaleX(5) translate(100, 200); } }";
119+
const animation = createAnimationFromCSS(css, "test");
120+
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
121+
122+
TKUnit.assertAreClose(rotate, 10, DELTA);
123+
124+
TKUnit.assertAreClose(scale.x, 5, SCALE_DELTA);
125+
TKUnit.assertAreClose(scale.y, 1, SCALE_DELTA);
126+
127+
TKUnit.assertAreClose(translate.x, 100, DELTA);
128+
TKUnit.assertAreClose(translate.y, 200, DELTA);
129+
}
130+
131+
export function test_ReadTransformNone() {
132+
const css = ".test { animation-name: test; } @keyframes test { to { transform: none; } }";
133+
const animation = createAnimationFromCSS(css, "test");
134+
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
135+
136+
TKUnit.assertEqual(rotate, 0);
137+
138+
TKUnit.assertEqual(scale.x, 1);
139+
TKUnit.assertEqual(scale.y, 1);
140+
141+
TKUnit.assert(translate.x === 0);
142+
TKUnit.assert(translate.y === 0);
143+
}
144+
116145
export function test_ReadScale() {
117-
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scaleX(5),scaleY(10); } }", "test");
118-
let scale = animation.keyframes[0].declarations[0].value;
119-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
120-
TKUnit.assert(scale.x === 5 && scale.y === 10);
121-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(-5, 12.3pt); } }", "test");
122-
scale = animation.keyframes[0].declarations[0].value;
123-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
124-
TKUnit.assert(scale.x === -5 && scale.y === 12.3);
125-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scaleY(10); } }", "test");
126-
scale = animation.keyframes[0].declarations[0].value;
127-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
128-
TKUnit.assert(scale.x === 1 && scale.y === 10);
129-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale3d(10, 20, 30); } }", "test");
130-
scale = animation.keyframes[0].declarations[0].value;
131-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
132-
TKUnit.assert(scale.x === 10 && scale.y === 20);
146+
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(-5, 12.3pt); } }", "test");
147+
const { scale, rotate } = getTransforms(animation.keyframes[0].declarations);
148+
149+
TKUnit.assertEqual(scale.property, "scale");
150+
TKUnit.assertAreClose(scale.value.x, -5, DELTA);
151+
TKUnit.assertAreClose(scale.value.y, 12.3, DELTA);
152+
}
153+
154+
export function test_ReadScaleSingle() {
155+
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(2); } }", "test");
156+
const { scale } = getTransforms(animation.keyframes[0].declarations);
157+
158+
TKUnit.assertEqual(scale.property, "scale");
159+
TKUnit.assertAreClose(scale.value.x, 2, DELTA);
160+
TKUnit.assertAreClose(scale.value.y, 2, DELTA);
161+
}
162+
163+
export function test_ReadScaleXY() {
164+
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleX(5) scaleY(10); } }";
165+
const animation = createAnimationFromCSS(css, "test");
166+
const { scale } = getTransforms(animation.keyframes[0].declarations);
167+
168+
TKUnit.assertEqual(scale.property, "scale");
169+
TKUnit.assertAreClose(scale.value.x, 5, SCALE_DELTA);
170+
TKUnit.assertAreClose(scale.value.y, 10, SCALE_DELTA);
171+
}
172+
173+
export function test_ReadScaleX() {
174+
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleX(12.5); } }";
175+
const animation = createAnimationFromCSS(css, "test");
176+
const { scale } = getTransforms(animation.keyframes[0].declarations);
177+
178+
TKUnit.assertEqual(scale.property, "scale");
179+
TKUnit.assertAreClose(scale.value.x, 12.5, SCALE_DELTA);
180+
// y defaults to 1
181+
TKUnit.assertAreClose(scale.value.y, 1, SCALE_DELTA);
182+
}
183+
184+
export function test_ReadScaleY() {
185+
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleY(10); } }";
186+
const animation = createAnimationFromCSS(css, "test");
187+
const { scale } = getTransforms(animation.keyframes[0].declarations);
188+
189+
TKUnit.assertEqual(scale.property, "scale");
190+
TKUnit.assertAreClose(scale.value.y, 10, SCALE_DELTA);
191+
// x defaults to 1
192+
TKUnit.assertAreClose(scale.value.x, 1, SCALE_DELTA);
193+
}
194+
195+
export function test_ReadScale3d() {
196+
const css = ".test { animation-name: test; } @keyframes test { to { transform: scale3d(10, 20, 30); } }";
197+
const animation = createAnimationFromCSS(css, "test");
198+
const { scale } = getTransforms(animation.keyframes[0].declarations);
199+
200+
TKUnit.assertEqual(scale.property, "scale");
201+
TKUnit.assertAreClose(scale.value.x, 10, SCALE_DELTA);
202+
TKUnit.assertAreClose(scale.value.y, 20, SCALE_DELTA);
133203
}
134204

135205
export function test_ReadTranslate() {
136-
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translateX(5),translateY(10); } }", "test");
137-
let translate = animation.keyframes[0].declarations[0].value;
138-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
139-
TKUnit.assert(translate.x === 5 && translate.y === 10);
140-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(-5, 12.3pt); } }", "test");
141-
translate = animation.keyframes[0].declarations[0].value;
142-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
143-
TKUnit.assert(translate.x === -5 && translate.y === 12.3);
144-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translateX(10); } }", "test");
145-
translate = animation.keyframes[0].declarations[0].value;
146-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
147-
TKUnit.assert(translate.x === 10 && translate.y === 0);
148-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate3d(10, 20, 30); } }", "test");
149-
translate = animation.keyframes[0].declarations[0].value;
150-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
151-
TKUnit.assert(translate.x === 10 && translate.y === 20);
206+
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(100, 20); } }", "test");
207+
const { translate } = getTransforms(animation.keyframes[0].declarations);
208+
209+
TKUnit.assertEqual(translate.property, "translate");
210+
TKUnit.assertAreClose(translate.value.x, 100, DELTA);
211+
TKUnit.assertAreClose(translate.value.y, 20, DELTA);
212+
}
213+
214+
export function test_ReadTranslateSingle() {
215+
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(30); } }", "test");
216+
const { translate, rotate } = getTransforms(animation.keyframes[0].declarations);
217+
218+
TKUnit.assertEqual(translate.property, "translate");
219+
TKUnit.assertAreClose(translate.value.x, 30, DELTA);
220+
TKUnit.assertAreClose(translate.value.y, 30, DELTA);
221+
}
222+
223+
export function test_ReadTranslateXY() {
224+
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateX(5) translateY(10); } }";
225+
const animation = createAnimationFromCSS(css, "test");
226+
const { translate } = getTransforms(animation.keyframes[0].declarations);
227+
228+
TKUnit.assertEqual(translate.property, "translate");
229+
TKUnit.assertAreClose(translate.value.x, 5, DELTA);
230+
TKUnit.assertAreClose(translate.value.y, 10, DELTA);
231+
}
232+
233+
export function test_ReadTranslateX() {
234+
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateX(12.5); } }";
235+
const animation = createAnimationFromCSS(css, "test");
236+
const { translate } = getTransforms(animation.keyframes[0].declarations);
237+
238+
TKUnit.assertEqual(translate.property, "translate");
239+
TKUnit.assertAreClose(translate.value.x, 12.5, DELTA);
240+
// y defaults to 0
241+
TKUnit.assertAreClose(translate.value.y, 0, DELTA);
242+
}
243+
244+
export function test_ReadTranslateY() {
245+
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateY(10); } }";
246+
const animation = createAnimationFromCSS(css, "test");
247+
const { translate } = getTransforms(animation.keyframes[0].declarations);
248+
249+
TKUnit.assertEqual(translate.property, "translate");
250+
TKUnit.assertAreClose(translate.value.y, 10, DELTA);
251+
// x defaults to 0
252+
TKUnit.assertAreClose(translate.value.x, 0, DELTA);
253+
}
254+
255+
export function test_ReadTranslate3d() {
256+
const css = ".test { animation-name: test; } @keyframes test { to { transform: translate3d(10, 20, 30); } }";
257+
const animation = createAnimationFromCSS(css, "test");
258+
const { translate } = getTransforms(animation.keyframes[0].declarations);
259+
260+
TKUnit.assertEqual(translate.property, "translate");
261+
TKUnit.assertAreClose(translate.value.x, 10, DELTA);
262+
TKUnit.assertAreClose(translate.value.y, 20, DELTA);
152263
}
153264

154265
export function test_ReadRotate() {
155-
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(5); } }", "test");
156-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
157-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 5);
158-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(45deg); } }", "test");
159-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
160-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 45);
161-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(0.7853981634rad); } }", "test");
162-
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
163-
TKUnit.assertTrue(animation.keyframes[0].declarations[0].value - 45 < 0.1);
164-
}
165-
166-
export function test_ReadTransform() {
167-
let css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(10),scaleX(5),translate(2,4); } }";
168-
let animation = createAnimationFromCSS(css, "test");
169-
let rotate = animation.keyframes[0].declarations[0].value;
170-
let scale = animation.keyframes[0].declarations[1].value;
171-
let translate = animation.keyframes[0].declarations[2].value;
172-
TKUnit.assertEqual(rotate, 10);
173-
TKUnit.assert(scale.x === 5 && scale.y === 1);
174-
TKUnit.assert(translate.x === 2 && translate.y === 4);
175-
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: none; } }", "test");
176-
rotate = animation.keyframes[0].declarations[0].value;
177-
scale = animation.keyframes[0].declarations[1].value;
178-
translate = animation.keyframes[0].declarations[2].value;
179-
TKUnit.assertEqual(rotate, 0);
180-
TKUnit.assert(scale.x === 1 && scale.y === 1);
181-
TKUnit.assert(translate.x === 0 && translate.y === 0);
266+
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(5); } }";
267+
const animation = createAnimationFromCSS(css, "test");
268+
const { rotate } = getTransforms(animation.keyframes[0].declarations);
269+
270+
TKUnit.assertEqual(rotate.property, "rotate");
271+
TKUnit.assertAreClose(rotate.value, 5, DELTA);
272+
}
273+
274+
export function test_ReadRotateDeg() {
275+
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(45deg); } }";
276+
const animation = createAnimationFromCSS(css, "test");
277+
const { rotate } = getTransforms(animation.keyframes[0].declarations);
278+
279+
TKUnit.assertEqual(rotate.property, "rotate");
280+
TKUnit.assertAreClose(rotate.value, 45, DELTA);
281+
}
282+
283+
export function test_ReadRotateRad() {
284+
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(0.7853981634rad); } }";
285+
const animation = createAnimationFromCSS(css, "test");
286+
const { rotate } = getTransforms(animation.keyframes[0].declarations);
287+
288+
TKUnit.assertEqual(rotate.property, "rotate");
289+
TKUnit.assertAreClose(rotate.value, 45, DELTA);
182290
}
183291

184292
export function test_ReadAnimationWithUnsortedKeyframes() {
@@ -311,3 +419,15 @@ export function test_AnimationCurveInKeyframes() {
311419
TKUnit.assertEqual(realAnimation.animations[1].curve, enums.AnimationCurve.linear);
312420
TKUnit.assertEqual(realAnimation.animations[2].curve, enums.AnimationCurve.easeIn);
313421
}
422+
423+
function getTransformsValues(declarations) {
424+
return Object.assign({},
425+
...(<any>Object).entries(getTransforms(declarations))
426+
.map(([k, v]) => ({[k]: v.value}))
427+
);
428+
}
429+
430+
function getTransforms(declarations) {
431+
const [ translate, rotate, scale ] = [...declarations];
432+
return { translate, rotate, scale };
433+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Contains utility methods for transforming css matrixes.
3+
* All methods in this module are experimental and
4+
* may be changed in a non-major version.
5+
* @module "matrix"
6+
*/ /** */
7+
8+
import { TransformFunctionsInfo } from "../ui/animation/animation";
9+
10+
/**
11+
* Returns the affine matrix representation of the transformation.
12+
* @param transformation Property and value of the transformation.
13+
*/
14+
export declare const getTransformMatrix: ({property, value}) => number[];
15+
16+
/**
17+
* Returns the css matrix representation of
18+
* an affine transformation matrix
19+
* @param m The flat matrix array to be transformed
20+
*/
21+
export declare const matrixArrayToCssMatrix: (m: number[]) => number[];
22+
23+
/**
24+
* Multiplies two two-dimensional affine matrices
25+
* https://jsperf.com/array-vs-object-affine-matrices/
26+
* @param m1 Left-side matrix array
27+
* @param m2 Right-side matrix array
28+
*/
29+
export declare function multiplyAffine2d(m1: number[], m2: number[]): number[];
30+
31+
/**
32+
* QR decomposition using the Gram–Schmidt process.
33+
* Decomposes a css matrix to simple transforms - translate, rotate and scale.
34+
* @param matrix The css matrix array to decompose.
35+
*/
36+
export function decompose2DTransformMatrix(matrix: number[])
37+
: TransformFunctionsInfo;

‎tns-core-modules/matrix/matrix.ts‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { TransformFunctionsInfo } from "../ui/animation/animation";
2+
3+
import { radiansToDegrees, degreesToRadians } from "../utils/number-utils";
4+
5+
export const getTransformMatrix = ({property, value}) =>
6+
TRANSFORM_MATRIXES[property](value);
7+
8+
const TRANSFORM_MATRIXES = {
9+
"scale": ({x, y}) => [
10+
x, 0, 0,
11+
0, y, 0,
12+
0, 0, 1,
13+
],
14+
"translate": ({x, y}) => [
15+
1, 0, x,
16+
0, 1, y,
17+
0, 0, 1,
18+
],
19+
"rotate": angleInDeg => {
20+
const angleInRad = degreesToRadians(angleInDeg);
21+
22+
return [
23+
Math.cos(angleInRad), -Math.sin(angleInRad), 0,
24+
Math.sin(angleInRad), Math.cos(angleInRad), 0,
25+
0, 0, 1,
26+
]
27+
},
28+
};
29+
30+
export const matrixArrayToCssMatrix = (m: number[]) => [
31+
m[0], m[3], m[1],
32+
m[4], m[2], m[5],
33+
];
34+
35+
export function multiplyAffine2d(m1: number[], m2: number[]): number[] {
36+
return [
37+
m1[0] * m2[0] + m1[1] * m2[3],
38+
m1[0] * m2[1] + m1[1] * m2[4],
39+
m1[0] * m2[2] + m1[1] * m2[5] + m1[2],
40+
m1[3] * m2[0] + m1[4] * m2[3],
41+
m1[3] * m2[1] + m1[4] * m2[4],
42+
m1[3] * m2[2] + m1[4] * m2[5] + m1[5]
43+
]
44+
}
45+
46+
export function decompose2DTransformMatrix(matrix: number[])
47+
: TransformFunctionsInfo {
48+
49+
verifyTransformMatrix(matrix);
50+
51+
const [A, B, C, D, E, F] = [...matrix];
52+
const determinant = A * D - B * C;
53+
const translate = { x: E || 0, y: F || 0 };
54+
55+
// rewrite with obj desctructuring using the identity matrix
56+
let rotate = 0;
57+
let scale = { x: 1, y: 1 };
58+
if (A || B) {
59+
const R = Math.sqrt(A*A + B*B);
60+
rotate = B > 0 ? Math.acos(A / R) : -Math.acos(A / R);
61+
scale = { x: R, y: determinant / R };
62+
} else if (C || D) {
63+
const R = Math.sqrt(C*C + D*D);
64+
rotate = Math.PI / 2 - (D > 0 ? Math.acos(-C / R) : -Math.acos(C / R));
65+
scale = { x: determinant / R, y: R };
66+
}
67+
68+
rotate = radiansToDegrees(rotate);
69+
70+
return { translate, rotate, scale };
71+
}
72+
73+
function verifyTransformMatrix(matrix: number[]) {
74+
if (matrix.length < 6) {
75+
throw new Error("Transform matrix should be 2x3.");
76+
}
77+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name" : "matrix",
3+
"main" : "matrix",
4+
"types" : "matrix.d.ts",
5+
"nativescript": {}
6+
}

0 commit comments

Comments
 (0)