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:
Stanimira Vlaeva
2017-06-09 18:20:07 +03:00
committed by GitHub
parent c228b97263
commit 9bba250424
15 changed files with 639 additions and 432 deletions

View File

@@ -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
View 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 GramSchmidt 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;

View 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.");
}
}

View File

@@ -0,0 +1,6 @@
{
"name" : "matrix",
"main" : "matrix",
"types" : "matrix.d.ts",
"nativescript": {}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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] }));
}

View File

@@ -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>;

View File

@@ -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.

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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>;