From 9bba25042457beecb5919d89223406375693f15d Mon Sep 17 00:00:00 2001 From: Stanimira Vlaeva Date: Fri, 9 Jun 2017 18:20:07 +0300 Subject: [PATCH] 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 --- tests/app/ui/animation/css-animation-tests.ts | 236 +++++++++++++----- tns-core-modules/matrix/matrix.d.ts | 37 +++ tns-core-modules/matrix/matrix.ts | 77 ++++++ tns-core-modules/matrix/package.json | 6 + tns-core-modules/ui/animation/animation.d.ts | 29 +++ .../ui/animation/keyframe-animation.d.ts | 12 + .../ui/animation/keyframe-animation.ts | 14 +- tns-core-modules/ui/styling/converters.ts | 139 +++-------- .../ui/styling/css-animation-parser.ts | 210 ++++------------ .../ui/styling/style-properties.d.ts | 5 +- .../ui/styling/style-properties.ts | 205 ++++++++------- tns-core-modules/ui/styling/style-scope.ts | 70 ++++-- tns-core-modules/utils/number-utils.ts | 8 +- tns-core-modules/utils/utils-common.ts | 10 +- tns-core-modules/utils/utils.d.ts | 13 + 15 files changed, 639 insertions(+), 432 deletions(-) create mode 100644 tns-core-modules/matrix/matrix.d.ts create mode 100644 tns-core-modules/matrix/matrix.ts create mode 100644 tns-core-modules/matrix/package.json diff --git a/tests/app/ui/animation/css-animation-tests.ts b/tests/app/ui/animation/css-animation-tests.ts index 934b0c32a..d43b23a57 100644 --- a/tests/app/ui/animation/css-animation-tests.ts +++ b/tests/app/ui/animation/css-animation-tests.ts @@ -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({}, + ...(Object).entries(getTransforms(declarations)) + .map(([k, v]) => ({[k]: v.value})) + ); +} + +function getTransforms(declarations) { + const [ translate, rotate, scale ] = [...declarations]; + return { translate, rotate, scale }; +} diff --git a/tns-core-modules/matrix/matrix.d.ts b/tns-core-modules/matrix/matrix.d.ts new file mode 100644 index 000000000..b9c43d3e4 --- /dev/null +++ b/tns-core-modules/matrix/matrix.d.ts @@ -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; diff --git a/tns-core-modules/matrix/matrix.ts b/tns-core-modules/matrix/matrix.ts new file mode 100644 index 000000000..29f580dfc --- /dev/null +++ b/tns-core-modules/matrix/matrix.ts @@ -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."); + } +} diff --git a/tns-core-modules/matrix/package.json b/tns-core-modules/matrix/package.json new file mode 100644 index 000000000..a8b4b3e2c --- /dev/null +++ b/tns-core-modules/matrix/package.json @@ -0,0 +1,6 @@ +{ + "name" : "matrix", + "main" : "matrix", + "types" : "matrix.d.ts", + "nativescript": {} +} diff --git a/tns-core-modules/ui/animation/animation.d.ts b/tns-core-modules/ui/animation/animation.d.ts index f27de6bb5..43edb439e 100644 --- a/tns-core-modules/ui/animation/animation.d.ts +++ b/tns-core-modules/ui/animation/animation.d.ts @@ -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; } diff --git a/tns-core-modules/ui/animation/keyframe-animation.d.ts b/tns-core-modules/ui/animation/keyframe-animation.d.ts index 8e3442cf7..200b0b836 100644 --- a/tns-core-modules/ui/animation/keyframe-animation.d.ts +++ b/tns-core-modules/ui/animation/keyframe-animation.d.ts @@ -4,6 +4,18 @@ import { View } from "../core/view"; +export declare const ANIMATION_PROPERTIES; + +export interface Keyframes { + name: string; + keyframes: Array; +} + +export interface UnparsedKeyframe { + values: Array; + declarations: Array; +} + export interface KeyframeDeclaration { property: string; value: any; diff --git a/tns-core-modules/ui/animation/keyframe-animation.ts b/tns-core-modules/ui/animation/keyframe-animation.ts index 79e0abaa2..e507be58a 100644 --- a/tns-core-modules/ui/animation/keyframe-animation.ts +++ b/tns-core-modules/ui/animation/keyframe-animation.ts @@ -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; +} + +export class UnparsedKeyframe implements UnparsedKeyframeDefinition { + values: Array; + declarations: Array; +} + export class KeyframeDeclaration implements KeyframeDeclarationDefinition { public property: string; public value: any; diff --git a/tns-core-modules/ui/styling/converters.ts b/tns-core-modules/ui/styling/converters.ts index e9461d1bf..d314deb66 100644 --- a/tns-core-modules/ui/styling/converters.ts +++ b/tns-core-modules/ui/styling/converters.ts @@ -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; - } -} diff --git a/tns-core-modules/ui/styling/css-animation-parser.ts b/tns-core-modules/ui/styling/css-animation-parser.ts index 665b06994..bc5b67ccb 100644 --- a/tns-core-modules/ui/styling/css-animation-parser.ts +++ b/tns-core-modules/ui/styling/css-animation-parser.ts @@ -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 { - let animations: Array = new Array(); - 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(); + 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 { + public static keyframesArrayFromCSS(keyframes: Array): Array { let parsedKeyframes = new Array(); - for (let keyframe of (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 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 { - 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) + : Array { - return array; -} + const declarations = unparsedKeyframeDeclarations + .reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => { + const property = CssAnimationProperty._getByCssName(unparsedProperty); -function parseKeyframeDeclarations(keyframe: Object): Array { - let declarations = {}; - let transforms = { scale: undefined, translate: undefined }; - for (let declaration of (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; - } - } + if (typeof unparsedProperty === "string" && property && property._valueConverter) { + declarations[property.name] = property._valueConverter(unparsedValue); + } else if (typeof unparsedValue === "string" && unparsedProperty === "transform") { + const transformations = transformConverter(unparsedValue); + Object.assign(declarations, transformations); } - 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(); - for (let declaration in declarations) { - let 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 declarations; + }, {}); + + return Object.keys(declarations).map(property => ({ property, value: declarations[property] })); } diff --git a/tns-core-modules/ui/styling/style-properties.d.ts b/tns-core-modules/ui/styling/style-properties.d.ts index 6ec5f2853..10c766bc9 100644 --- a/tns-core-modules/ui/styling/style-properties.d.ts +++ b/tns-core-modules/ui/styling/style-properties.d.ts @@ -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; export const translateXProperty: CssAnimationProperty; export const translateYProperty: CssAnimationProperty; +export function transformConverter(text: string): TransformFunctionsInfo; + export const clipPathProperty: CssProperty; export const colorProperty: InheritedCssProperty; @@ -109,4 +112,4 @@ export const fontInternalProperty: InheritedCssProperty; export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat"; export type Visibility = "visible" | "hidden" | "collapse"; export type HorizontalAlignment = "left" | "center" | "right" | "stretch"; -export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch"; \ No newline at end of file +export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch"; diff --git a/tns-core-modules/ui/styling/style-properties.ts b/tns-core-modules/ui/styling/style-properties.ts index 41bd6142c..1b233e404 100644 --- a/tns-core-modules/ui/styling/style-properties.ts +++ b/tns-core-modules/ui/styling/style-properties.ts @@ -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({ }); 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][] { - let newTransform = value === unsetValue ? { "none": "none" } : transformConverter(value); - let array = []; - let values: Array; - 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]]); - } - else if (values.length === 1) { - array.push([scaleXProperty, values[0]]); - array.push([scaleYProperty, values[0]]); - } - 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]]); - } - else if (values.length === 1) { - array.push([translateXProperty, values[0]]); - array.push([translateYProperty, values[0]]); - } - break; - case "rotate": - let text = newTransform[transform]; - let val = parseFloat(text); - if (text.slice(-3) === "rad") { - val = val * (180.0 / Math.PI); - } - 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; + if (value === unsetValue) { + value = "none"; + } + + const { translate, rotate, scale } = transformConverter(value); + return [ + [translateXProperty, translate.x], + [translateYProperty, translate.y], + + [scaleXProperty, scale.x], + [scaleYProperty, scale.y], + + [rotateProperty, rotate], + ]; +} + +export function transformConverter(text: string): TransformFunctionsInfo { + const transformations = parseTransformString(text); + + if (text === "none" || text === "" || !transformations.length) { + return IDENTITY_TRANSFORMATION; + } + + 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; + } + + const affineMatrix = transformations + .map(getTransformMatrix) + .reduce(multiplyAffine2d) + const cssMatrix = matrixArrayToCssMatrix(affineMatrix) + + return decompose2DTransformMatrix(cssMatrix); +} + +// 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. diff --git a/tns-core-modules/ui/styling/style-scope.ts b/tns-core-modules/ui/styling/style-scope.ts index b4737e792..b90c062f9 100644 --- a/tns-core-modules/ui/styling/style-scope.ts +++ b/tns-core-modules/ui/styling/style-scope.ts @@ -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(); 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): 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): 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): RuleSet[] { const nodes = ast.stylesheet.rules; - (nodes.filter(isKeyframe)).forEach(node => keyframes[node.name] = node); + (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"; } diff --git a/tns-core-modules/utils/number-utils.ts b/tns-core-modules/utils/number-utils.ts index 1fa72182c..7d443afbc 100644 --- a/tns-core-modules/utils/number-utils.ts +++ b/tns-core-modules/utils/number-utils.ts @@ -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); @@ -26,4 +26,8 @@ export function greaterThanZero(value: Object): boolean { export function notNegative(value: Object): boolean { return (value) >= 0; -} \ No newline at end of file +} + +export const radiansToDegrees = (a: number) => a * (180 / Math.PI); + +export const degreesToRadians = (a: number) => a * (Math.PI / 180); diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index 4a3ce9d81..a31cb51d0 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -152,4 +152,12 @@ export function merge(left, right, compareFunc) { } return result; -} \ No newline at end of file +} + +export function hasDuplicates(arr: Array): boolean { + return arr.length !== eliminateDuplicates(arr).length; +} + +export function eliminateDuplicates(arr: Array): Array { + return Array.from(new Set(arr)); +} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index a0afc84b8..06b075b25 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -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, compareFunc: (a: any, b: any) => number): Array + +/** + * + * Checks if array has any duplicate elements. + * @param arr - The array to be checked. + */ +export function hasDuplicates(arr: Array): boolean; + +/** + * Removes duplicate elements from array. + * @param arr - The array. + */ +export function eliminateDuplicates(arr: Array): Array; \ No newline at end of file