mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
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
This commit is contained in:
@@ -9,7 +9,8 @@ import * as color from "tns-core-modules/color";
|
||||
|
||||
import {SelectorCore} from "tns-core-modules/ui/styling/css-selector";
|
||||
|
||||
//import * as styling from "ui/styling";
|
||||
const DELTA = 1;
|
||||
const SCALE_DELTA = 0.001;
|
||||
|
||||
function createAnimationFromCSS(css: string, name: string): keyframeAnimation.KeyframeAnimationInfo {
|
||||
let scope = new styleScope.StyleScope();
|
||||
@@ -113,72 +114,179 @@ export function test_ReadKeyframe() {
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "backgroundColor", "Keyframe declarations are not correct");
|
||||
}
|
||||
|
||||
export function test_ReadTransformAllSet() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(10) scaleX(5) translate(100, 200); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertAreClose(rotate, 10, DELTA);
|
||||
|
||||
TKUnit.assertAreClose(scale.x, 5, SCALE_DELTA);
|
||||
TKUnit.assertAreClose(scale.y, 1, SCALE_DELTA);
|
||||
|
||||
TKUnit.assertAreClose(translate.x, 100, DELTA);
|
||||
TKUnit.assertAreClose(translate.y, 200, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTransformNone() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: none; } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate, 0);
|
||||
|
||||
TKUnit.assertEqual(scale.x, 1);
|
||||
TKUnit.assertEqual(scale.y, 1);
|
||||
|
||||
TKUnit.assert(translate.x === 0);
|
||||
TKUnit.assert(translate.y === 0);
|
||||
}
|
||||
|
||||
export function test_ReadScale() {
|
||||
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scaleX(5),scaleY(10); } }", "test");
|
||||
let scale = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
|
||||
TKUnit.assert(scale.x === 5 && scale.y === 10);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(-5, 12.3pt); } }", "test");
|
||||
scale = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
|
||||
TKUnit.assert(scale.x === -5 && scale.y === 12.3);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scaleY(10); } }", "test");
|
||||
scale = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
|
||||
TKUnit.assert(scale.x === 1 && scale.y === 10);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale3d(10, 20, 30); } }", "test");
|
||||
scale = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "scale");
|
||||
TKUnit.assert(scale.x === 10 && scale.y === 20);
|
||||
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(-5, 12.3pt); } }", "test");
|
||||
const { scale, rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.x, -5, DELTA);
|
||||
TKUnit.assertAreClose(scale.value.y, 12.3, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadScaleSingle() {
|
||||
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: scale(2); } }", "test");
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.x, 2, DELTA);
|
||||
TKUnit.assertAreClose(scale.value.y, 2, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadScaleXY() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleX(5) scaleY(10); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.x, 5, SCALE_DELTA);
|
||||
TKUnit.assertAreClose(scale.value.y, 10, SCALE_DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadScaleX() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleX(12.5); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.x, 12.5, SCALE_DELTA);
|
||||
// y defaults to 1
|
||||
TKUnit.assertAreClose(scale.value.y, 1, SCALE_DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadScaleY() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: scaleY(10); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.y, 10, SCALE_DELTA);
|
||||
// x defaults to 1
|
||||
TKUnit.assertAreClose(scale.value.x, 1, SCALE_DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadScale3d() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: scale3d(10, 20, 30); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { scale } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(scale.property, "scale");
|
||||
TKUnit.assertAreClose(scale.value.x, 10, SCALE_DELTA);
|
||||
TKUnit.assertAreClose(scale.value.y, 20, SCALE_DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslate() {
|
||||
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translateX(5),translateY(10); } }", "test");
|
||||
let translate = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
|
||||
TKUnit.assert(translate.x === 5 && translate.y === 10);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(-5, 12.3pt); } }", "test");
|
||||
translate = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
|
||||
TKUnit.assert(translate.x === -5 && translate.y === 12.3);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translateX(10); } }", "test");
|
||||
translate = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
|
||||
TKUnit.assert(translate.x === 10 && translate.y === 0);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate3d(10, 20, 30); } }", "test");
|
||||
translate = animation.keyframes[0].declarations[0].value;
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "translate");
|
||||
TKUnit.assert(translate.x === 10 && translate.y === 20);
|
||||
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(100, 20); } }", "test");
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.x, 100, DELTA);
|
||||
TKUnit.assertAreClose(translate.value.y, 20, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslateSingle() {
|
||||
const animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: translate(30); } }", "test");
|
||||
const { translate, rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.x, 30, DELTA);
|
||||
TKUnit.assertAreClose(translate.value.y, 30, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslateXY() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateX(5) translateY(10); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.x, 5, DELTA);
|
||||
TKUnit.assertAreClose(translate.value.y, 10, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslateX() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateX(12.5); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.x, 12.5, DELTA);
|
||||
// y defaults to 0
|
||||
TKUnit.assertAreClose(translate.value.y, 0, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslateY() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: translateY(10); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.y, 10, DELTA);
|
||||
// x defaults to 0
|
||||
TKUnit.assertAreClose(translate.value.x, 0, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTranslate3d() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: translate3d(10, 20, 30); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { translate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(translate.property, "translate");
|
||||
TKUnit.assertAreClose(translate.value.x, 10, DELTA);
|
||||
TKUnit.assertAreClose(translate.value.y, 20, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadRotate() {
|
||||
let animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(5); } }", "test");
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 5);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(45deg); } }", "test");
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].value, 45);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: rotate(0.7853981634rad); } }", "test");
|
||||
TKUnit.assertEqual(animation.keyframes[0].declarations[0].property, "rotate");
|
||||
TKUnit.assertTrue(animation.keyframes[0].declarations[0].value - 45 < 0.1);
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(5); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, "rotate");
|
||||
TKUnit.assertAreClose(rotate.value, 5, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadTransform() {
|
||||
let css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(10),scaleX(5),translate(2,4); } }";
|
||||
let animation = createAnimationFromCSS(css, "test");
|
||||
let rotate = animation.keyframes[0].declarations[0].value;
|
||||
let scale = animation.keyframes[0].declarations[1].value;
|
||||
let translate = animation.keyframes[0].declarations[2].value;
|
||||
TKUnit.assertEqual(rotate, 10);
|
||||
TKUnit.assert(scale.x === 5 && scale.y === 1);
|
||||
TKUnit.assert(translate.x === 2 && translate.y === 4);
|
||||
animation = createAnimationFromCSS(".test { animation-name: test; } @keyframes test { to { transform: none; } }", "test");
|
||||
rotate = animation.keyframes[0].declarations[0].value;
|
||||
scale = animation.keyframes[0].declarations[1].value;
|
||||
translate = animation.keyframes[0].declarations[2].value;
|
||||
TKUnit.assertEqual(rotate, 0);
|
||||
TKUnit.assert(scale.x === 1 && scale.y === 1);
|
||||
TKUnit.assert(translate.x === 0 && translate.y === 0);
|
||||
export function test_ReadRotateDeg() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(45deg); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, "rotate");
|
||||
TKUnit.assertAreClose(rotate.value, 45, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadRotateRad() {
|
||||
const css = ".test { animation-name: test; } @keyframes test { to { transform: rotate(0.7853981634rad); } }";
|
||||
const animation = createAnimationFromCSS(css, "test");
|
||||
const { rotate } = getTransforms(animation.keyframes[0].declarations);
|
||||
|
||||
TKUnit.assertEqual(rotate.property, "rotate");
|
||||
TKUnit.assertAreClose(rotate.value, 45, DELTA);
|
||||
}
|
||||
|
||||
export function test_ReadAnimationWithUnsortedKeyframes() {
|
||||
@@ -311,3 +419,15 @@ export function test_AnimationCurveInKeyframes() {
|
||||
TKUnit.assertEqual(realAnimation.animations[1].curve, enums.AnimationCurve.linear);
|
||||
TKUnit.assertEqual(realAnimation.animations[2].curve, enums.AnimationCurve.easeIn);
|
||||
}
|
||||
|
||||
function getTransformsValues(declarations) {
|
||||
return Object.assign({},
|
||||
...(<any>Object).entries(getTransforms(declarations))
|
||||
.map(([k, v]) => ({[k]: v.value}))
|
||||
);
|
||||
}
|
||||
|
||||
function getTransforms(declarations) {
|
||||
const [ translate, rotate, scale ] = [...declarations];
|
||||
return { translate, rotate, scale };
|
||||
}
|
||||
|
||||
37
tns-core-modules/matrix/matrix.d.ts
vendored
Normal file
37
tns-core-modules/matrix/matrix.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Contains utility methods for transforming css matrixes.
|
||||
* All methods in this module are experimental and
|
||||
* may be changed in a non-major version.
|
||||
* @module "matrix"
|
||||
*/ /** */
|
||||
|
||||
import { TransformFunctionsInfo } from "../ui/animation/animation";
|
||||
|
||||
/**
|
||||
* Returns the affine matrix representation of the transformation.
|
||||
* @param transformation Property and value of the transformation.
|
||||
*/
|
||||
export declare const getTransformMatrix: ({property, value}) => number[];
|
||||
|
||||
/**
|
||||
* Returns the css matrix representation of
|
||||
* an affine transformation matrix
|
||||
* @param m The flat matrix array to be transformed
|
||||
*/
|
||||
export declare const matrixArrayToCssMatrix: (m: number[]) => number[];
|
||||
|
||||
/**
|
||||
* Multiplies two two-dimensional affine matrices
|
||||
* https://jsperf.com/array-vs-object-affine-matrices/
|
||||
* @param m1 Left-side matrix array
|
||||
* @param m2 Right-side matrix array
|
||||
*/
|
||||
export declare function multiplyAffine2d(m1: number[], m2: number[]): number[];
|
||||
|
||||
/**
|
||||
* QR decomposition using the Gram–Schmidt process.
|
||||
* Decomposes a css matrix to simple transforms - translate, rotate and scale.
|
||||
* @param matrix The css matrix array to decompose.
|
||||
*/
|
||||
export function decompose2DTransformMatrix(matrix: number[])
|
||||
: TransformFunctionsInfo;
|
||||
77
tns-core-modules/matrix/matrix.ts
Normal file
77
tns-core-modules/matrix/matrix.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { TransformFunctionsInfo } from "../ui/animation/animation";
|
||||
|
||||
import { radiansToDegrees, degreesToRadians } from "../utils/number-utils";
|
||||
|
||||
export const getTransformMatrix = ({property, value}) =>
|
||||
TRANSFORM_MATRIXES[property](value);
|
||||
|
||||
const TRANSFORM_MATRIXES = {
|
||||
"scale": ({x, y}) => [
|
||||
x, 0, 0,
|
||||
0, y, 0,
|
||||
0, 0, 1,
|
||||
],
|
||||
"translate": ({x, y}) => [
|
||||
1, 0, x,
|
||||
0, 1, y,
|
||||
0, 0, 1,
|
||||
],
|
||||
"rotate": angleInDeg => {
|
||||
const angleInRad = degreesToRadians(angleInDeg);
|
||||
|
||||
return [
|
||||
Math.cos(angleInRad), -Math.sin(angleInRad), 0,
|
||||
Math.sin(angleInRad), Math.cos(angleInRad), 0,
|
||||
0, 0, 1,
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
export const matrixArrayToCssMatrix = (m: number[]) => [
|
||||
m[0], m[3], m[1],
|
||||
m[4], m[2], m[5],
|
||||
];
|
||||
|
||||
export function multiplyAffine2d(m1: number[], m2: number[]): number[] {
|
||||
return [
|
||||
m1[0] * m2[0] + m1[1] * m2[3],
|
||||
m1[0] * m2[1] + m1[1] * m2[4],
|
||||
m1[0] * m2[2] + m1[1] * m2[5] + m1[2],
|
||||
m1[3] * m2[0] + m1[4] * m2[3],
|
||||
m1[3] * m2[1] + m1[4] * m2[4],
|
||||
m1[3] * m2[2] + m1[4] * m2[5] + m1[5]
|
||||
]
|
||||
}
|
||||
|
||||
export function decompose2DTransformMatrix(matrix: number[])
|
||||
: TransformFunctionsInfo {
|
||||
|
||||
verifyTransformMatrix(matrix);
|
||||
|
||||
const [A, B, C, D, E, F] = [...matrix];
|
||||
const determinant = A * D - B * C;
|
||||
const translate = { x: E || 0, y: F || 0 };
|
||||
|
||||
// rewrite with obj desctructuring using the identity matrix
|
||||
let rotate = 0;
|
||||
let scale = { x: 1, y: 1 };
|
||||
if (A || B) {
|
||||
const R = Math.sqrt(A*A + B*B);
|
||||
rotate = B > 0 ? Math.acos(A / R) : -Math.acos(A / R);
|
||||
scale = { x: R, y: determinant / R };
|
||||
} else if (C || D) {
|
||||
const R = Math.sqrt(C*C + D*D);
|
||||
rotate = Math.PI / 2 - (D > 0 ? Math.acos(-C / R) : -Math.acos(C / R));
|
||||
scale = { x: determinant / R, y: R };
|
||||
}
|
||||
|
||||
rotate = radiansToDegrees(rotate);
|
||||
|
||||
return { translate, rotate, scale };
|
||||
}
|
||||
|
||||
function verifyTransformMatrix(matrix: number[]) {
|
||||
if (matrix.length < 6) {
|
||||
throw new Error("Transform matrix should be 2x3.");
|
||||
}
|
||||
}
|
||||
6
tns-core-modules/matrix/package.json
Normal file
6
tns-core-modules/matrix/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "matrix",
|
||||
"main" : "matrix",
|
||||
"types" : "matrix.d.ts",
|
||||
"nativescript": {}
|
||||
}
|
||||
29
tns-core-modules/ui/animation/animation.d.ts
vendored
29
tns-core-modules/ui/animation/animation.d.ts
vendored
@@ -75,6 +75,26 @@ export class CubicBezierAnimationCurve {
|
||||
constructor(x1: number, y1: number, x2: number, y2: number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a key-value pair for css transformation
|
||||
*/
|
||||
export type Transformation = {
|
||||
property: TransformationType;
|
||||
value: TransformationValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines possible css transformations
|
||||
*/
|
||||
export type TransformationType = "rotate" |
|
||||
"translate" | "translateX" | "translateY" |
|
||||
"scale" | "scaleX" | "scaleY";
|
||||
|
||||
/**
|
||||
* Defines possible css transformation values
|
||||
*/
|
||||
export type TransformationValue = Pair | number;
|
||||
|
||||
/**
|
||||
* Defines a pair of values (horizontal and vertical) for translate and scale animations.
|
||||
*/
|
||||
@@ -83,6 +103,15 @@ export interface Pair {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines full information for css transformation
|
||||
*/
|
||||
export type TransformFunctionsInfo = {
|
||||
translate: Pair,
|
||||
rotate: number,
|
||||
scale: Pair,
|
||||
}
|
||||
|
||||
export interface Cancelable {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
|
||||
import { View } from "../core/view";
|
||||
|
||||
export declare const ANIMATION_PROPERTIES;
|
||||
|
||||
export interface Keyframes {
|
||||
name: string;
|
||||
keyframes: Array<UnparsedKeyframe>;
|
||||
}
|
||||
|
||||
export interface UnparsedKeyframe {
|
||||
values: Array<any>;
|
||||
declarations: Array<KeyframeDeclaration>;
|
||||
}
|
||||
|
||||
export interface KeyframeDeclaration {
|
||||
property: string;
|
||||
value: any;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Definitions.
|
||||
import {
|
||||
Keyframes as KeyframesDefinition,
|
||||
UnparsedKeyframe as UnparsedKeyframeDefinition,
|
||||
KeyframeDeclaration as KeyframeDeclarationDefinition,
|
||||
KeyframeInfo as KeyframeInfoDefinition,
|
||||
KeyframeAnimationInfo as KeyframeAnimationInfoDefinition,
|
||||
KeyframeAnimation as KeyframeAnimationDefinition
|
||||
KeyframeAnimation as KeyframeAnimationDefinition,
|
||||
} from "./keyframe-animation";
|
||||
|
||||
import { View, Color } from "../core/view";
|
||||
@@ -18,6 +20,16 @@ import {
|
||||
rotateProperty, opacityProperty
|
||||
} from "../styling/style-properties";
|
||||
|
||||
export class Keyframes implements KeyframesDefinition {
|
||||
name: string;
|
||||
keyframes: Array<UnparsedKeyframe>;
|
||||
}
|
||||
|
||||
export class UnparsedKeyframe implements UnparsedKeyframeDefinition {
|
||||
values: Array<any>;
|
||||
declarations: Array<KeyframeDeclaration>;
|
||||
}
|
||||
|
||||
export class KeyframeDeclaration implements KeyframeDeclarationDefinition {
|
||||
public property: string;
|
||||
public value: any;
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { Color } from "../../color";
|
||||
import { CubicBezierAnimationCurve } from "../animation";
|
||||
import { AnimationCurve } from "../enums";
|
||||
|
||||
export function colorConverter(value: string): Color {
|
||||
return new Color(value);
|
||||
}
|
||||
|
||||
export function floatConverter(value: string): number {
|
||||
// TODO: parse different unit types
|
||||
const result: number = parseFloat(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function fontSizeConverter(value: string): number {
|
||||
return floatConverter(value);
|
||||
}
|
||||
|
||||
export const numberConverter = parseFloat;
|
||||
|
||||
export function opacityConverter(value: string): number {
|
||||
let result = parseFloat(value);
|
||||
result = Math.max(0.0, result);
|
||||
result = Math.min(1.0, result);
|
||||
return result;
|
||||
}
|
||||
const STYLE_CURVE_MAP = Object.freeze({
|
||||
"ease": AnimationCurve.ease,
|
||||
"linear": AnimationCurve.linear,
|
||||
"ease-in": AnimationCurve.easeIn,
|
||||
"ease-out": AnimationCurve.easeOut,
|
||||
"ease-in-out": AnimationCurve.easeInOut,
|
||||
"spring": AnimationCurve.spring,
|
||||
});
|
||||
|
||||
export function timeConverter(value: string): number {
|
||||
let result = parseFloat(value);
|
||||
@@ -33,86 +18,36 @@ export function timeConverter(value: string): number {
|
||||
return Math.max(0.0, result);
|
||||
}
|
||||
|
||||
export function bezieArgumentConverter(value: string): number {
|
||||
export function animationTimingFunctionConverter(value: string): any {
|
||||
return value ?
|
||||
STYLE_CURVE_MAP[value] || parseCubicBezierCurve(value) :
|
||||
AnimationCurve.ease;
|
||||
}
|
||||
|
||||
function parseCubicBezierCurve(value: string) {
|
||||
const coordsString = /\((.*?)\)/.exec(value);
|
||||
const coords = coordsString && coordsString[1]
|
||||
.split(",")
|
||||
.map(stringToBezieCoords);
|
||||
|
||||
if (value.startsWith("cubic-bezier") &&
|
||||
coordsString &&
|
||||
coords.length === 4) {
|
||||
|
||||
const [x1, x2, y1, y2] = [...coords];
|
||||
return AnimationCurve.cubicBezier(x1, x2, y1, y2);
|
||||
} else {
|
||||
throw new Error(`Invalid value for animation: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stringToBezieCoords(value: string): number {
|
||||
let result = parseFloat(value);
|
||||
result = Math.max(0.0, result);
|
||||
result = Math.min(1.0, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function animationTimingFunctionConverter(value: string): Object {
|
||||
let result: Object = "ease";
|
||||
switch (value) {
|
||||
case "ease":
|
||||
result = "ease";
|
||||
break;
|
||||
case "linear":
|
||||
result = "linear";
|
||||
break;
|
||||
case "ease-in":
|
||||
result = "easeIn";
|
||||
break;
|
||||
case "ease-out":
|
||||
result = "easeOut";
|
||||
break;
|
||||
case "ease-in-out":
|
||||
result = "easeInOut";
|
||||
break;
|
||||
case "spring":
|
||||
result = "spring";
|
||||
break;
|
||||
default:
|
||||
if (value.indexOf("cubic-bezier(") === 0) {
|
||||
let bezierArr = value.substring(13).split(/[,]+/);
|
||||
if (bezierArr.length !== 4) {
|
||||
throw new Error("Invalid value for animation: " + value);
|
||||
}
|
||||
|
||||
result = new CubicBezierAnimationCurve(bezieArgumentConverter(bezierArr[0]),
|
||||
bezieArgumentConverter(bezierArr[1]),
|
||||
bezieArgumentConverter(bezierArr[2]),
|
||||
bezieArgumentConverter(bezierArr[3]));
|
||||
}
|
||||
else {
|
||||
throw new Error("Invalid value for animation: " + value);
|
||||
}
|
||||
break;
|
||||
if (result < 0) {
|
||||
return 0;
|
||||
} else if (result > 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function transformConverter(value: any): Object {
|
||||
if (value === "none") {
|
||||
let operations = {};
|
||||
operations[value] = value;
|
||||
return operations;
|
||||
}
|
||||
else if (typeof value === "string") {
|
||||
let operations = {};
|
||||
let operator = "";
|
||||
let pos = 0;
|
||||
while (pos < value.length) {
|
||||
if (value[pos] === " " || value[pos] === ",") {
|
||||
pos++;
|
||||
}
|
||||
else if (value[pos] === "(") {
|
||||
let start = pos + 1;
|
||||
while (pos < value.length && value[pos] !== ")") {
|
||||
pos++;
|
||||
}
|
||||
let operand = value.substring(start, pos);
|
||||
operations[operator] = operand.trim();
|
||||
operator = "";
|
||||
pos++;
|
||||
}
|
||||
else {
|
||||
operator += value[pos++];
|
||||
}
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,62 @@
|
||||
import { Pair } from "../animation";
|
||||
import { Color } from "../../color";
|
||||
import { KeyframeAnimationInfo, KeyframeInfo, KeyframeDeclaration } from "../animation/keyframe-animation";
|
||||
import { timeConverter, numberConverter, transformConverter, animationTimingFunctionConverter } from "../styling/converters";
|
||||
import { CssAnimationProperty } from "../core/properties";
|
||||
|
||||
interface TransformInfo {
|
||||
scale: Pair;
|
||||
translate: Pair;
|
||||
}
|
||||
import {
|
||||
KeyframeAnimationInfo,
|
||||
KeyframeDeclaration,
|
||||
KeyframeInfo,
|
||||
UnparsedKeyframe,
|
||||
} from "../animation/keyframe-animation";
|
||||
import { timeConverter, animationTimingFunctionConverter } from "../styling/converters";
|
||||
|
||||
let animationProperties = {
|
||||
"animation-name": (info, declaration) => info.name = declaration.value,
|
||||
"animation-duration": (info, declaration) => info.duration = timeConverter(declaration.value),
|
||||
"animation-delay": (info, declaration) => info.delay = timeConverter(declaration.value),
|
||||
"animation-timing-function": (info, declaration) => info.curve = animationTimingFunctionConverter(declaration.value),
|
||||
"animation-iteration-count": (info, declaration) => declaration.value === "infinite" ? info.iterations = Number.MAX_VALUE : info.iterations = numberConverter(declaration.value),
|
||||
"animation-direction": (info, declaration) => info.isReverse = declaration.value === "reverse",
|
||||
"animation-fill-mode": (info, declaration) => info.isForwards = declaration.value === "forwards"
|
||||
};
|
||||
import { transformConverter } from "../styling/style-properties";
|
||||
|
||||
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
|
||||
"animation-name": (info, value) => info.name = value,
|
||||
"animation-duration": (info, value) => info.duration = timeConverter(value),
|
||||
"animation-delay": (info, value) => info.delay = timeConverter(value),
|
||||
"animation-timing-function": (info, value) => info.curve = animationTimingFunctionConverter(value),
|
||||
"animation-iteration-count": (info, value) => info.iterations = value === "infinite" ? Number.MAX_VALUE : parseFloat(value),
|
||||
"animation-direction": (info, value) => info.isReverse = value === "reverse",
|
||||
"animation-fill-mode": (info, value) => info.isForwards = value === "forwards"
|
||||
});
|
||||
|
||||
export class CssAnimationParser {
|
||||
public static keyframeAnimationsFromCSSDeclarations(declarations: { property: string, value: string }[]): Array<KeyframeAnimationInfo> {
|
||||
let animations: Array<KeyframeAnimationInfo> = new Array<KeyframeAnimationInfo>();
|
||||
let animationInfo: KeyframeAnimationInfo = undefined;
|
||||
if (declarations === null || declarations === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
for (let declaration of declarations) {
|
||||
if (declaration.property === "animation") {
|
||||
keyframeAnimationsFromCSSProperty(declaration.value, animations);
|
||||
}
|
||||
else {
|
||||
let propertyHandler = animationProperties[declaration.property];
|
||||
|
||||
let animations = new Array<KeyframeAnimationInfo>();
|
||||
let animationInfo: KeyframeAnimationInfo = undefined;
|
||||
|
||||
declarations.forEach(({ property, value }) => {
|
||||
if (property === "animation") {
|
||||
keyframeAnimationsFromCSSProperty(value, animations);
|
||||
} else {
|
||||
const propertyHandler = ANIMATION_PROPERTY_HANDLERS[property];
|
||||
if (propertyHandler) {
|
||||
if (animationInfo === undefined) {
|
||||
animationInfo = new KeyframeAnimationInfo();
|
||||
animations.push(animationInfo);
|
||||
}
|
||||
propertyHandler(animationInfo, declaration);
|
||||
}
|
||||
propertyHandler(animationInfo, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return animations.length === 0 ? undefined : animations;
|
||||
}
|
||||
|
||||
public static keyframesArrayFromCSS(cssKeyframes: Object): Array<KeyframeInfo> {
|
||||
public static keyframesArrayFromCSS(keyframes: Array<UnparsedKeyframe>): Array<KeyframeInfo> {
|
||||
let parsedKeyframes = new Array<KeyframeInfo>();
|
||||
for (let keyframe of (<any>cssKeyframes).keyframes) {
|
||||
let declarations = parseKeyframeDeclarations(keyframe);
|
||||
for (let keyframe of keyframes) {
|
||||
let declarations = parseKeyframeDeclarations(keyframe.declarations);
|
||||
for (let time of keyframe.values) {
|
||||
if (time === "from") {
|
||||
time = 0;
|
||||
}
|
||||
else if (time === "to") {
|
||||
} else if (time === "to") {
|
||||
time = 1;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
time = parseFloat(time) / 100;
|
||||
if (time < 0) {
|
||||
time = 0;
|
||||
@@ -69,7 +71,7 @@ export class CssAnimationParser {
|
||||
current.duration = time;
|
||||
parsedKeyframes[time] = current;
|
||||
}
|
||||
for (let declaration of <any>keyframe.declarations) {
|
||||
for (let declaration of keyframe.declarations) {
|
||||
if (declaration.property === "animation-timing-function") {
|
||||
current.curve = animationTimingFunctionConverter(declaration.value);
|
||||
}
|
||||
@@ -122,132 +124,22 @@ function keyframeAnimationsFromCSSProperty(value: any, animations: Array<Keyfram
|
||||
}
|
||||
}
|
||||
|
||||
function getTransformationValues(value: any): Array<{ propertyName: string, value: number }> {
|
||||
let newTransform = transformConverter(value);
|
||||
let array = new Array<{ propertyName: string, value: number }>();
|
||||
let values = undefined;
|
||||
for (let transform in newTransform) {
|
||||
switch (transform) {
|
||||
case "scaleX":
|
||||
array.push({ propertyName: "scaleX", value: parseFloat(newTransform[transform]) });
|
||||
break;
|
||||
case "scaleY":
|
||||
array.push({ propertyName: "scaleY", value: parseFloat(newTransform[transform]) });
|
||||
break;
|
||||
case "scale":
|
||||
case "scale3d":
|
||||
values = newTransform[transform].split(",");
|
||||
if (values.length === 2 || values.length === 3) {
|
||||
array.push({ propertyName: "scaleX", value: parseFloat(values[0]) });
|
||||
array.push({ propertyName: "scaleY", value: parseFloat(values[1]) });
|
||||
}
|
||||
break;
|
||||
case "translateX":
|
||||
array.push({ propertyName: "translateX", value: parseFloat(newTransform[transform]) });
|
||||
break;
|
||||
case "translateY":
|
||||
array.push({ propertyName: "translateY", value: parseFloat(newTransform[transform]) });
|
||||
break;
|
||||
case "translate":
|
||||
case "translate3d":
|
||||
values = newTransform[transform].split(",");
|
||||
if (values.length === 2 || values.length === 3) {
|
||||
array.push({ propertyName: "translateX", value: parseFloat(values[0]) });
|
||||
array.push({ propertyName: "translateY", value: parseFloat(values[1]) });
|
||||
}
|
||||
break;
|
||||
case "rotate":
|
||||
let text = newTransform[transform];
|
||||
let val = parseFloat(text);
|
||||
if (text.slice(-3) === "rad") {
|
||||
val = val * (180.0 / Math.PI);
|
||||
}
|
||||
array.push({ propertyName: "rotate", value: val });
|
||||
break;
|
||||
case "none":
|
||||
array.push({ propertyName: "scaleX", value: 1 });
|
||||
array.push({ propertyName: "scaleY", value: 1 });
|
||||
array.push({ propertyName: "translateX", value: 0 });
|
||||
array.push({ propertyName: "translateY", value: 0 });
|
||||
array.push({ propertyName: "rotate", value: 0 });
|
||||
break;
|
||||
}
|
||||
function parseKeyframeDeclarations(unparsedKeyframeDeclarations: Array<KeyframeDeclaration>)
|
||||
: Array<KeyframeDeclaration> {
|
||||
|
||||
const declarations = unparsedKeyframeDeclarations
|
||||
.reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => {
|
||||
const property = CssAnimationProperty._getByCssName(unparsedProperty);
|
||||
|
||||
if (typeof unparsedProperty === "string" && property && property._valueConverter) {
|
||||
declarations[property.name] = property._valueConverter(<string>unparsedValue);
|
||||
} else if (typeof unparsedValue === "string" && unparsedProperty === "transform") {
|
||||
const transformations = transformConverter(unparsedValue);
|
||||
Object.assign(declarations, transformations);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
return declarations;
|
||||
}, {});
|
||||
|
||||
function parseKeyframeDeclarations(keyframe: Object): Array<KeyframeDeclaration> {
|
||||
let declarations = {};
|
||||
let transforms = { scale: undefined, translate: undefined };
|
||||
for (let declaration of (<any>keyframe).declarations) {
|
||||
let propertyName = declaration.property;
|
||||
let value = declaration.value;
|
||||
if (propertyName === "opacity") {
|
||||
declarations[propertyName] = parseFloat(value);
|
||||
}
|
||||
else if (propertyName === "transform") {
|
||||
let values = getTransformationValues(value);
|
||||
if (values) {
|
||||
for (let pair of values) {
|
||||
if (!preprocessAnimationValues(pair.propertyName, pair.value, transforms)) {
|
||||
declarations[pair.propertyName] = pair.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
delete declarations[propertyName];
|
||||
}
|
||||
else if (propertyName === "backgroundColor" || propertyName === "background-color") {
|
||||
declarations["backgroundColor"] = new Color(value);
|
||||
}
|
||||
else {
|
||||
declarations[propertyName] = value;
|
||||
}
|
||||
}
|
||||
if (transforms.scale !== undefined) {
|
||||
declarations["scale"] = transforms.scale;
|
||||
}
|
||||
if (transforms.translate !== undefined) {
|
||||
declarations["translate"] = transforms.translate;
|
||||
}
|
||||
let array = new Array<KeyframeDeclaration>();
|
||||
for (let declaration in declarations) {
|
||||
let keyframeDeclaration = <KeyframeDeclaration>{};
|
||||
keyframeDeclaration.property = declaration;
|
||||
keyframeDeclaration.value = declarations[declaration];
|
||||
array.push(keyframeDeclaration);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function preprocessAnimationValues(propertyName: string, value: number, transforms: TransformInfo) {
|
||||
if (propertyName === "scaleX") {
|
||||
if (transforms.scale === undefined) {
|
||||
transforms.scale = { x: 1, y: 1 };
|
||||
}
|
||||
transforms.scale.x = value;
|
||||
return true;
|
||||
}
|
||||
if (propertyName === "scaleY") {
|
||||
if (transforms.scale === undefined) {
|
||||
transforms.scale = { x: 1, y: 1 };
|
||||
}
|
||||
transforms.scale.y = value;
|
||||
return true;
|
||||
}
|
||||
if (propertyName === "translateX") {
|
||||
if (transforms.translate === undefined) {
|
||||
transforms.translate = { x: 0, y: 0 };
|
||||
}
|
||||
transforms.translate.x = value;
|
||||
return true;
|
||||
}
|
||||
if (propertyName === "translateY") {
|
||||
if (transforms.translate === undefined) {
|
||||
transforms.translate = { x: 0, y: 0 };
|
||||
}
|
||||
transforms.translate.y = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Object.keys(declarations).map(property => ({ property, value: declarations[property] }));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @module "ui/styling/style-properties"
|
||||
*/ /** */
|
||||
|
||||
import { TransformFunctionsInfo } from "../animation/animation";
|
||||
import { Color } from "../../color";
|
||||
import { Style, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from "../core/properties";
|
||||
import { Font, FontStyle, FontWeight } from "./font";
|
||||
@@ -48,6 +49,8 @@ export const scaleYProperty: CssAnimationProperty<Style, number>;
|
||||
export const translateXProperty: CssAnimationProperty<Style, dip>;
|
||||
export const translateYProperty: CssAnimationProperty<Style, dip>;
|
||||
|
||||
export function transformConverter(text: string): TransformFunctionsInfo;
|
||||
|
||||
export const clipPathProperty: CssProperty<Style, string>;
|
||||
export const colorProperty: InheritedCssProperty<Style, Color>;
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
// Types
|
||||
import {
|
||||
Transformation,
|
||||
TransformationValue,
|
||||
TransformFunctionsInfo,
|
||||
} from "../animation/animation";
|
||||
|
||||
import { dip, px, percent } from "../core/view";
|
||||
|
||||
import { Color } from "../../color";
|
||||
@@ -11,6 +17,16 @@ import { Style } from "./style";
|
||||
|
||||
import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty, makeValidator, makeParser } from "../core/properties";
|
||||
|
||||
import { hasDuplicates } from "../../utils/utils";
|
||||
import { radiansToDegrees } from "../../utils/number-utils";
|
||||
|
||||
import {
|
||||
decompose2DTransformMatrix,
|
||||
getTransformMatrix,
|
||||
matrixArrayToCssMatrix,
|
||||
multiplyAffine2d,
|
||||
} from "../../matrix";
|
||||
|
||||
export type LengthDipUnit = { readonly unit: "dip", readonly value: dip };
|
||||
export type LengthPxUnit = { readonly unit: "px", readonly value: px };
|
||||
export type LengthPercentUnit = { readonly unit: "%", readonly value: percent };
|
||||
@@ -418,97 +434,114 @@ const transformProperty = new ShorthandProperty<Style, string>({
|
||||
});
|
||||
transformProperty.register(Style);
|
||||
|
||||
function transformConverter(value: string): Object {
|
||||
if (value.indexOf("none") !== -1) {
|
||||
let operations = {};
|
||||
operations[value] = value;
|
||||
return operations;
|
||||
}
|
||||
const IDENTITY_TRANSFORMATION = {
|
||||
translate: { x: 0, y: 0 },
|
||||
rotate: 0,
|
||||
scale: { x: 1, y: 1 },
|
||||
};
|
||||
|
||||
let operations = {};
|
||||
let operator = "";
|
||||
let pos = 0;
|
||||
while (pos < value.length) {
|
||||
if (value[pos] === " " || value[pos] === ",") {
|
||||
pos++;
|
||||
}
|
||||
else if (value[pos] === "(") {
|
||||
let start = pos + 1;
|
||||
while (pos < value.length && value[pos] !== ")") {
|
||||
pos++;
|
||||
}
|
||||
let operand = value.substring(start, pos);
|
||||
operations[operator] = operand.trim();
|
||||
operator = "";
|
||||
pos++;
|
||||
}
|
||||
else {
|
||||
operator += value[pos++];
|
||||
}
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
|
||||
const TRANSFORMATIONS = Object.freeze([
|
||||
"rotate",
|
||||
"translate",
|
||||
"translate3d",
|
||||
"translateX",
|
||||
"translateY",
|
||||
"scale",
|
||||
"scale3d",
|
||||
"scaleX",
|
||||
"scaleY",
|
||||
]);
|
||||
|
||||
const STYLE_TRANSFORMATION_MAP = Object.freeze({
|
||||
"scale": value => ({ property: "scale", value }),
|
||||
"scale3d": value => ({ property: "scale", value }),
|
||||
"scaleX": ({x}) => ({ property: "scale", value: { x, y: IDENTITY_TRANSFORMATION.scale.y } }),
|
||||
"scaleY": ({y}) => ({ property: "scale", value: { y, x: IDENTITY_TRANSFORMATION.scale.x } }),
|
||||
|
||||
"translate": value => ({ property: "translate", value }),
|
||||
"translate3d": value => ({ property: "translate", value }),
|
||||
"translateX": ({x}) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }),
|
||||
"translateY": ({y}) => ({ property: "translate", value: { y, x: IDENTITY_TRANSFORMATION.translate.x } }),
|
||||
|
||||
"rotate": value => ({ property: "rotate", value }),
|
||||
});
|
||||
|
||||
function convertToTransform(value: string): [CssProperty<any, any>, any][] {
|
||||
let newTransform = value === unsetValue ? { "none": "none" } : transformConverter(value);
|
||||
let array = [];
|
||||
let values: Array<string>;
|
||||
for (let transform in newTransform) {
|
||||
switch (transform) {
|
||||
case "scaleX":
|
||||
array.push([scaleXProperty, newTransform[transform]]);
|
||||
break;
|
||||
case "scaleY":
|
||||
array.push([scaleYProperty, newTransform[transform]]);
|
||||
break;
|
||||
case "scale":
|
||||
case "scale3d":
|
||||
values = newTransform[transform].split(",");
|
||||
if (values.length >= 2) {
|
||||
array.push([scaleXProperty, values[0]]);
|
||||
array.push([scaleYProperty, values[1]]);
|
||||
if (value === unsetValue) {
|
||||
value = "none";
|
||||
}
|
||||
else if (values.length === 1) {
|
||||
array.push([scaleXProperty, values[0]]);
|
||||
array.push([scaleYProperty, values[0]]);
|
||||
|
||||
const { translate, rotate, scale } = transformConverter(value);
|
||||
return [
|
||||
[translateXProperty, translate.x],
|
||||
[translateYProperty, translate.y],
|
||||
|
||||
[scaleXProperty, scale.x],
|
||||
[scaleYProperty, scale.y],
|
||||
|
||||
[rotateProperty, rotate],
|
||||
];
|
||||
}
|
||||
break;
|
||||
case "translateX":
|
||||
array.push([translateXProperty, newTransform[transform]]);
|
||||
break;
|
||||
case "translateY":
|
||||
array.push([translateYProperty, newTransform[transform]]);
|
||||
break;
|
||||
case "translate":
|
||||
case "translate3d":
|
||||
values = newTransform[transform].split(",");
|
||||
if (values.length >= 2) {
|
||||
array.push([translateXProperty, values[0]]);
|
||||
array.push([translateYProperty, values[1]]);
|
||||
|
||||
export function transformConverter(text: string): TransformFunctionsInfo {
|
||||
const transformations = parseTransformString(text);
|
||||
|
||||
if (text === "none" || text === "" || !transformations.length) {
|
||||
return IDENTITY_TRANSFORMATION;
|
||||
}
|
||||
else if (values.length === 1) {
|
||||
array.push([translateXProperty, values[0]]);
|
||||
array.push([translateYProperty, values[0]]);
|
||||
|
||||
const usedTransforms = transformations.map(t => t.property);
|
||||
if (!hasDuplicates(usedTransforms)) {
|
||||
const fullTransformations = Object.assign({}, IDENTITY_TRANSFORMATION);
|
||||
transformations.forEach(transform => {
|
||||
fullTransformations[transform.property] = transform.value;
|
||||
});
|
||||
|
||||
return fullTransformations;
|
||||
}
|
||||
break;
|
||||
case "rotate":
|
||||
let text = newTransform[transform];
|
||||
let val = parseFloat(text);
|
||||
if (text.slice(-3) === "rad") {
|
||||
val = val * (180.0 / Math.PI);
|
||||
|
||||
const affineMatrix = transformations
|
||||
.map(getTransformMatrix)
|
||||
.reduce(multiplyAffine2d)
|
||||
const cssMatrix = matrixArrayToCssMatrix(affineMatrix)
|
||||
|
||||
return decompose2DTransformMatrix(cssMatrix);
|
||||
}
|
||||
array.push([rotateProperty, val]);
|
||||
break;
|
||||
case "none":
|
||||
array.push([scaleXProperty, 1]);
|
||||
array.push([scaleYProperty, 1]);
|
||||
array.push([translateXProperty, 0]);
|
||||
array.push([translateYProperty, 0]);
|
||||
array.push([rotateProperty, 0]);
|
||||
break;
|
||||
|
||||
// using general regex and manually checking the matched
|
||||
// properties is faster than using more specific regex
|
||||
// https://jsperf.com/cssparse
|
||||
function parseTransformString(text: string): Transformation[] {
|
||||
let matches: Transformation[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) {
|
||||
const property = match[1];
|
||||
const value = convertTransformValue(property, match[2]);
|
||||
|
||||
if (TRANSFORMATIONS.indexOf(property) !== -1) {
|
||||
matches.push(normalizeTransformation({ property, value }));
|
||||
}
|
||||
}
|
||||
return array;
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function normalizeTransformation({ property, value }: Transformation) {
|
||||
return STYLE_TRANSFORMATION_MAP[property](value);
|
||||
}
|
||||
|
||||
function convertTransformValue(property: string, stringValue: string)
|
||||
: TransformationValue {
|
||||
|
||||
const [x, y = x] = stringValue.split(",").map(parseFloat);
|
||||
|
||||
if (property === "rotate") {
|
||||
return stringValue.slice(-3) === "rad" ? radiansToDegrees(x) : x;
|
||||
}
|
||||
|
||||
return y ? { x, y } : x;
|
||||
}
|
||||
|
||||
// Background properties.
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { Keyframes } from "../animation/keyframe-animation";
|
||||
import { ViewBase } from "../core/view-base";
|
||||
import { View } from "../core/view";
|
||||
import { resetCSSProperties } from "../core/properties";
|
||||
import { SyntaxTree, Keyframes, parse as parseCss, Node as CssNode } from "../../css";
|
||||
import { RuleSet, SelectorsMap, SelectorCore, SelectorsMatch, ChangeMap, fromAstNodes, Node } from "./css-selector";
|
||||
import { write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace";
|
||||
import {
|
||||
SyntaxTree,
|
||||
Keyframes as KeyframesDefinition,
|
||||
parse as parseCss,
|
||||
Node as CssNode,
|
||||
} from "../../css";
|
||||
import {
|
||||
RuleSet,
|
||||
SelectorsMap,
|
||||
SelectorCore,
|
||||
SelectorsMatch,
|
||||
ChangeMap,
|
||||
fromAstNodes,
|
||||
Node,
|
||||
} from "./css-selector";
|
||||
import {
|
||||
write as traceWrite,
|
||||
categories as traceCategories,
|
||||
messageType as traceMessageType,
|
||||
} from "../../trace";
|
||||
import { File, knownFolders, path } from "../../file-system";
|
||||
import * as application from "../../application";
|
||||
import { profile } from "../../profiling";
|
||||
@@ -167,7 +185,7 @@ export class StyleScope {
|
||||
private _localCssSelectorVersion: number = 0;
|
||||
private _localCssSelectorsAppliedVersion: number = 0;
|
||||
private _applicationCssSelectorsAppliedVersion: number = 0;
|
||||
private _keyframes = {};
|
||||
private _keyframes = new Map<string, Keyframes>();
|
||||
|
||||
get css(): string {
|
||||
return this._css;
|
||||
@@ -206,15 +224,18 @@ export class StyleScope {
|
||||
}
|
||||
|
||||
public getKeyframeAnimationWithName(animationName: string): kam.KeyframeAnimationInfo {
|
||||
let keyframes = this._keyframes[animationName];
|
||||
if (keyframes !== undefined) {
|
||||
ensureKeyframeAnimationModule();
|
||||
let animation = new keyframeAnimationModule.KeyframeAnimationInfo();
|
||||
ensureCssAnimationParserModule();
|
||||
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframes);
|
||||
return animation;
|
||||
const cssKeyframes = this._keyframes[animationName];
|
||||
if (!cssKeyframes) {
|
||||
return;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
ensureKeyframeAnimationModule();
|
||||
const animation = new keyframeAnimationModule.KeyframeAnimationInfo();
|
||||
ensureCssAnimationParserModule();
|
||||
animation.keyframes = cssAnimationParserModule
|
||||
.CssAnimationParser.keyframesArrayFromCSS(cssKeyframes.keyframes);
|
||||
|
||||
return animation;
|
||||
}
|
||||
|
||||
public ensureSelectors(): number {
|
||||
@@ -278,9 +299,10 @@ export class StyleScope {
|
||||
if (animations !== undefined && animations.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
for (let animation of animations) {
|
||||
let keyframe = this._keyframes[animation.name];
|
||||
if (keyframe !== undefined) {
|
||||
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframe);
|
||||
const cssKeyframe = this._keyframes[animation.name];
|
||||
if (cssKeyframe !== undefined) {
|
||||
animation.keyframes = cssAnimationParserModule
|
||||
.CssAnimationParser.keyframesArrayFromCSS(cssKeyframe.keyframes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +314,7 @@ export class StyleScope {
|
||||
}
|
||||
}
|
||||
|
||||
function createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[] {
|
||||
function createSelectorsFromCss(css: string, cssFileName: string, keyframes: Map<string, Keyframes>): RuleSet[] {
|
||||
try {
|
||||
const pageCssSyntaxTree = css ? parseCss(css, { source: cssFileName }) : null;
|
||||
let pageCssSelectors: RuleSet[] = [];
|
||||
@@ -306,7 +328,7 @@ function createSelectorsFromCss(css: string, cssFileName: string, keyframes: Obj
|
||||
}
|
||||
}
|
||||
|
||||
function createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[] {
|
||||
function createSelectorsFromImports(tree: SyntaxTree, keyframes: Map<string, Keyframes>): RuleSet[] {
|
||||
let selectors: RuleSet[] = [];
|
||||
|
||||
if (tree !== null && tree !== undefined) {
|
||||
@@ -336,14 +358,18 @@ function createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSe
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function createSelectorsFromSyntaxTree(ast: SyntaxTree, keyframes: Object): RuleSet[] {
|
||||
function createSelectorsFromSyntaxTree(ast: SyntaxTree, keyframes: Map<string, Keyframes>): RuleSet[] {
|
||||
const nodes = ast.stylesheet.rules;
|
||||
(<Keyframes[]>nodes.filter(isKeyframe)).forEach(node => keyframes[node.name] = node);
|
||||
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach(node => keyframes[node.name] = node);
|
||||
|
||||
const rulesets = fromAstNodes(nodes);
|
||||
if (rulesets && rulesets.length) {
|
||||
ensureCssAnimationParserModule();
|
||||
rulesets.forEach(rule => rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations));
|
||||
|
||||
rulesets.forEach(rule => {
|
||||
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser
|
||||
.keyframeAnimationsFromCSSDeclarations(rule.declarations);
|
||||
});
|
||||
}
|
||||
|
||||
return rulesets;
|
||||
@@ -371,7 +397,7 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx
|
||||
|
||||
export function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
let localStyle = `local { ${styleStr} }`;
|
||||
let inlineRuleSet = createSelectorsFromCss(localStyle, null, {});
|
||||
let inlineRuleSet = createSelectorsFromCss(localStyle, null, new Map());
|
||||
const style = view.style;
|
||||
|
||||
inlineRuleSet[0].declarations.forEach(d => {
|
||||
@@ -389,7 +415,7 @@ export function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function isKeyframe(node: CssNode): node is Keyframes {
|
||||
function isKeyframe(node: CssNode): node is KeyframesDefinition {
|
||||
return node.type === "keyframes";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var epsilon = 1E-05;
|
||||
const epsilon = 1E-05;
|
||||
|
||||
export function areClose(value1: number, value2: number): boolean {
|
||||
return (Math.abs(value1 - value2) < epsilon);
|
||||
@@ -27,3 +27,7 @@ export function greaterThanZero(value: Object): boolean {
|
||||
export function notNegative(value: Object): boolean {
|
||||
return (<number>value) >= 0;
|
||||
}
|
||||
|
||||
export const radiansToDegrees = (a: number) => a * (180 / Math.PI);
|
||||
|
||||
export const degreesToRadians = (a: number) => a * (Math.PI / 180);
|
||||
|
||||
@@ -153,3 +153,11 @@ export function merge(left, right, compareFunc) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function hasDuplicates(arr: Array<any>): boolean {
|
||||
return arr.length !== eliminateDuplicates(arr).length;
|
||||
}
|
||||
|
||||
export function eliminateDuplicates(arr: Array<any>): Array<any> {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
|
||||
13
tns-core-modules/utils/utils.d.ts
vendored
13
tns-core-modules/utils/utils.d.ts
vendored
@@ -270,3 +270,16 @@ export function convertString(value: any): any
|
||||
* @param compareFunc - function that will be used to compare two elements of the array
|
||||
*/
|
||||
export function mergeSort(arr: Array<any>, compareFunc: (a: any, b: any) => number): Array<any>
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks if array has any duplicate elements.
|
||||
* @param arr - The array to be checked.
|
||||
*/
|
||||
export function hasDuplicates(arr: Array<any>): boolean;
|
||||
|
||||
/**
|
||||
* Removes duplicate elements from array.
|
||||
* @param arr - The array.
|
||||
*/
|
||||
export function eliminateDuplicates(arr: Array<any>): Array<any>;
|
||||
Reference in New Issue
Block a user