From c543aea4c962cf0bc6e1135f8d6e7bbd9ddd3cce Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 1 Feb 2025 16:43:18 +0200 Subject: [PATCH 1/6] ref(core): Style properties module organization --- packages/core/core-types/index.ts | 5 +- .../core/ui/animation/animation-interfaces.ts | 13 +- .../core/ui/styling/css-animation-parser.ts | 2 +- packages/core/ui/styling/css-transform.ts | 133 ++++ .../core/ui/styling/style-properties.d.ts | 18 +- packages/core/ui/styling/style-properties.ts | 647 +++++++----------- 6 files changed, 399 insertions(+), 419 deletions(-) create mode 100644 packages/core/ui/styling/css-transform.ts diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index 52172b49ab..b244998e19 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -29,8 +29,9 @@ export namespace CoreTypes { export type LengthPxUnit = { readonly unit: 'px'; readonly value: px }; export type LengthPercentUnit = { readonly unit: '%'; readonly value: percent }; - export type LengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit; - export type PercentLengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit; + export type FixedLengthType = dip | LengthDipUnit | LengthPxUnit; + export type LengthType = 'auto' | FixedLengthType; + export type PercentLengthType = 'auto' | FixedLengthType | LengthPercentUnit; export const zeroLength: LengthType = { value: 0, diff --git a/packages/core/ui/animation/animation-interfaces.ts b/packages/core/ui/animation/animation-interfaces.ts index d86e923c4a..4c2efce1c2 100644 --- a/packages/core/ui/animation/animation-interfaces.ts +++ b/packages/core/ui/animation/animation-interfaces.ts @@ -10,11 +10,17 @@ export type Transformation = { export type TransformationType = 'rotate' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY'; -export type TransformationValue = Pair | number; +export type TransformationValue = Point3D | Pair | number; + +export interface Point3D { + x: number; + y: number; + z: number; +} export type TransformFunctionsInfo = { translate: Pair; - rotate: number; + rotate: Point3D; scale: Pair; }; @@ -26,6 +32,7 @@ export interface AnimationPromise extends Promise, Cancelable { export interface Pair { x: number; y: number; + z: number; } export interface Cancelable { @@ -55,7 +62,7 @@ export interface AnimationDefinition { scale?: Pair; height?: CoreTypes.PercentLengthType | string; width?: CoreTypes.PercentLengthType | string; - rotate?: number; + rotate?: Point3D; duration?: number; delay?: number; iterations?: number; diff --git a/packages/core/ui/styling/css-animation-parser.ts b/packages/core/ui/styling/css-animation-parser.ts index eefcd342d9..f071c738e0 100644 --- a/packages/core/ui/styling/css-animation-parser.ts +++ b/packages/core/ui/styling/css-animation-parser.ts @@ -3,7 +3,7 @@ import { CssAnimationProperty } from '../core/properties'; import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyframe } from '../animation/keyframe-animation'; import { timeConverter, animationTimingFunctionConverter } from '../styling/converters'; -import { transformConverter } from '../styling/style-properties'; +import { transformConverter } from '../styling/css-transform'; import { cleanupImportantFlags } from './css-utils'; const ANIMATION_PROPERTY_HANDLERS = Object.freeze({ diff --git a/packages/core/ui/styling/css-transform.ts b/packages/core/ui/styling/css-transform.ts new file mode 100644 index 0000000000..ea124bcc6d --- /dev/null +++ b/packages/core/ui/styling/css-transform.ts @@ -0,0 +1,133 @@ +import { Pair, Transformation, TransformationValue, TransformFunctionsInfo } from '../animation'; +import { radiansToDegrees } from '../../utils/number-utils'; +import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix'; +import { hasDuplicates } from '../../utils'; + +type TransformationStyleMap = { + [key: string]: (value: TransformationValue) => Transformation; +}; + +const IDENTITY_TRANSFORMATION = { + translate: { x: 0, y: 0 }, + rotate: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1 }, +}; + +const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); +const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']); + +const STYLE_TRANSFORMATION_MAP: TransformationStyleMap = Object.freeze({ + scale: (value: number) => ({ property: 'scale', value }), + scale3d: (value: number) => ({ property: 'scale', value }), + scaleX: ({ x }: Pair) => ({ + property: 'scale', + value: { x, y: IDENTITY_TRANSFORMATION.scale.y }, + }), + scaleY: ({ y }: Pair) => ({ + property: 'scale', + value: { y, x: IDENTITY_TRANSFORMATION.scale.x }, + }), + translate: (value) => ({ property: 'translate', value }), + translate3d: (value) => ({ property: 'translate', value }), + translateX: ({ x }: Pair) => ({ + property: 'translate', + value: { x, y: IDENTITY_TRANSFORMATION.translate.y }, + }), + translateY: ({ y }: Pair) => ({ + property: 'translate', + value: { y, x: IDENTITY_TRANSFORMATION.translate.x }, + }), + + rotate3d: (value) => ({ property: 'rotate', value }), + rotateX: (x: number) => ({ + property: 'rotate', + value: { + x, + y: IDENTITY_TRANSFORMATION.rotate.y, + z: IDENTITY_TRANSFORMATION.rotate.z, + }, + }), + rotateY: (y: number) => ({ + property: 'rotate', + value: { + x: IDENTITY_TRANSFORMATION.rotate.x, + y, + z: IDENTITY_TRANSFORMATION.rotate.z, + }, + }), + rotate: (z: number) => ({ + property: 'rotate', + value: { + x: IDENTITY_TRANSFORMATION.rotate.x, + y: IDENTITY_TRANSFORMATION.rotate.y, + z, + }, + }), +}); + +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 = { ...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[] { + const 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 matches; +} + +function normalizeTransformation({ property, value }: Transformation): Transformation { + return STYLE_TRANSFORMATION_MAP[property](value); +} + +function convertTransformValue(property: string, stringValue: string): TransformationValue { + const values = stringValue.split(',').map(parseFloat); + const x = values[0]; + + let y = values[1]; + let z = values[2]; + + if (property === 'translate') { + y ??= IDENTITY_TRANSFORMATION.translate.y; + } else { + y ??= x; + z ??= y; + } + + if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') { + return stringValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; + } + + return { x, y, z }; +} diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 61f99e3d0a..086e44d17b 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -1,4 +1,3 @@ -import { TransformFunctionsInfo } from '../animation'; import { CoreTypes } from '../../core-types'; import { Color } from '../../color'; import { CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; @@ -6,12 +5,23 @@ import { Style } from './style'; import { Font, FontStyleType, FontWeightType, FontVariationSettingsType } from './font'; import { Background } from './background'; +export namespace FixedLength { + export function parse(text: string): CoreTypes.FixedLengthType; + export function equals(a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean; + /** + * Converts FixedLengthType unit to device pixels. + * @param length The FixedLengthType to convert. + */ + export function toDevicePixels(length: CoreTypes.FixedLengthType): number; + export function convertToString(length: CoreTypes.FixedLengthType): string; +} + export namespace Length { export function parse(text: string): CoreTypes.LengthType; export function equals(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean; /** - * Converts Length unit to device pixels. - * @param length The Length to convert. + * Converts LengthType unit to device pixels. + * @param length The LengthType to convert. * @param auto Value to use for conversion of "auto". By default is Math.NaN. */ export function toDevicePixels(length: CoreTypes.LengthType, auto?: number): number; @@ -39,8 +49,6 @@ 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; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 50a7ad3a7d..c9804433bd 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1,59 +1,61 @@ // Types import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; import { Style } from './style'; -import { Transformation, TransformationValue, TransformFunctionsInfo } from '../animation'; import { Color } from '../../color'; import { Font, parseFont, FontStyle, FontStyleType, FontWeight, FontWeightType, FontVariationSettings, FontVariationSettingsType } from './font'; import { Background } from './background'; -import { layout, hasDuplicates } from '../../utils'; +import { layout } from '../../utils'; -import { radiansToDegrees } from '../../utils/number-utils'; - -import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix'; import { Trace } from '../../trace'; import { CoreTypes } from '../../core-types'; import { parseBackground } from '../../css/parser'; import { LinearGradient } from './linear-gradient'; import { parseCSSShadow, ShadowCSSValues } from './css-shadow'; +import { transformConverter } from './css-transform'; + +const supportedClipPaths = ['rect', 'circle', 'ellipse', 'polygon', 'inset']; + +interface CSSPositioning { + top: string; + right: string; + bottom: string; + left: string; +} function equalsCommon(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean; function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean; function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean { if (a == 'auto') { - // tslint:disable-line - return b == 'auto'; // tslint:disable-line + return b == 'auto'; } + + if (b == 'auto') { + return false; + } + if (typeof a === 'number') { - if (b == 'auto') { - // tslint:disable-line - return false; - } if (typeof b === 'number') { - return a == b; // tslint:disable-line + return a == b; } if (!b) { return false; } - return b.unit == 'dip' && a == b.value; // tslint:disable-line - } - if (b == 'auto') { - // tslint:disable-line - return false; + return b.unit == 'dip' && a == b.value; } + if (typeof b === 'number') { - return a ? a.unit == 'dip' && a.value == b : false; // tslint:disable-line + return a ? a.unit == 'dip' && a.value == b : false; } if (!a || !b) { return false; } - return a.value == b.value && a.unit == b.unit; // tslint:disable-line + return a.value == b.value && a.unit == b.unit; } function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentLengthType): string { if (length == 'auto') { - // tslint:disable-line return 'auto'; } @@ -71,7 +73,6 @@ function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentL function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number = Number.NaN, parentAvailableWidth: number = Number.NaN): number { if (length == 'auto') { - // tslint:disable-line return auto; } if (typeof length === 'number') { @@ -94,9 +95,9 @@ function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number export namespace PercentLength { export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.PercentLengthType { if (fromValue == 'auto') { - // tslint:disable-line return 'auto'; } + if (typeof fromValue === 'string') { let stringValue = fromValue.trim(); const percentIndex = stringValue.indexOf('%'); @@ -147,12 +148,8 @@ export namespace PercentLength { } = convertToStringCommon; } -export namespace Length { - export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType { - if (fromValue == 'auto') { - // tslint:disable-line - return 'auto'; - } +export namespace FixedLength { + export function parse(fromValue: string | CoreTypes.FixedLengthType): CoreTypes.FixedLengthType { if (typeof fromValue === 'string') { let stringValue = fromValue.trim(); if (stringValue.indexOf('px') !== -1) { @@ -175,6 +172,27 @@ export namespace Length { return fromValue; } } + export const equals: { (a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean } = equalsCommon; + export const toDevicePixels: { + (length: CoreTypes.FixedLengthType): number; + } = toDevicePixelsCommon; + export const convertToString: { + (length: CoreTypes.FixedLengthType): string; + } = convertToStringCommon; +} + +function isNonNegativeFiniteNumber(value: number): boolean { + return isFinite(value) && !isNaN(value) && value >= 0; +} + +export namespace Length { + export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType { + if (fromValue == 'auto') { + return 'auto'; + } + + return FixedLength.parse(fromValue); + } export const equals: { (a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean } = equalsCommon; export const toDevicePixels: { (length: CoreTypes.LengthType, auto?: number): number; @@ -184,6 +202,162 @@ export namespace Length { } = convertToStringCommon; } +function isClipPathValid(value: string): boolean { + if (!value) { + return true; + } + + const functionName = value.substring(0, value.indexOf('(')).trim(); + return supportedClipPaths.indexOf(functionName) !== -1; +} + +function parseShorthandPositioning(value: string): CSSPositioning { + const arr = value.split(/[ ,]+/); + + let top: string; + let right: string; + let bottom: string; + let left: string; + + if (arr.length === 1) { + top = arr[0]; + right = arr[0]; + bottom = arr[0]; + left = arr[0]; + } else if (arr.length === 2) { + top = arr[0]; + bottom = arr[0]; + right = arr[1]; + left = arr[1]; + } else if (arr.length === 3) { + top = arr[0]; + right = arr[1]; + left = arr[1]; + bottom = arr[2]; + } else if (arr.length === 4) { + top = arr[0]; + right = arr[1]; + bottom = arr[2]; + left = arr[3]; + } else { + throw new Error('Expected 1, 2, 3 or 4 parameters. Actual: ' + value); + } + + return { + top: top, + right: right, + bottom: bottom, + left: left, + }; +} + +function parseBorderColorPositioning(value: string): CSSPositioning { + if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) { + return { + top: value, + right: value, + bottom: value, + left: value, + }; + } + + return parseShorthandPositioning(value); +} + +function convertToBackgrounds(value: string): [CssProperty, any][] { + if (typeof value === 'string') { + const backgrounds = parseBackground(value).value; + let backgroundColor = unsetValue; + if (backgrounds.color) { + backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color); + } + + let backgroundImage: string | LinearGradient; + if (typeof backgrounds.image === 'object' && backgrounds.image) { + backgroundImage = LinearGradient.parse(backgrounds.image); + } else { + backgroundImage = backgrounds.image || unsetValue; + } + + const backgroundRepeat = backgrounds.repeat || unsetValue; + const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue; + + return [ + [backgroundColorProperty, backgroundColor], + [backgroundImageProperty, backgroundImage], + [backgroundRepeatProperty, backgroundRepeat], + [backgroundPositionProperty, backgroundPosition], + ]; + } else { + return [ + [backgroundColorProperty, unsetValue], + [backgroundImageProperty, unsetValue], + [backgroundRepeatProperty, unsetValue], + [backgroundPositionProperty, unsetValue], + ]; + } +} + +function convertToMargins(value: string | CoreTypes.PercentLengthType): [CssProperty, CoreTypes.PercentLengthType][] { + if (typeof value === 'string' && value !== 'auto') { + const thickness = parseShorthandPositioning(value); + + return [ + [marginTopProperty, PercentLength.parse(thickness.top)], + [marginRightProperty, PercentLength.parse(thickness.right)], + [marginBottomProperty, PercentLength.parse(thickness.bottom)], + [marginLeftProperty, PercentLength.parse(thickness.left)], + ]; + } else { + return [ + [marginTopProperty, value], + [marginRightProperty, value], + [marginBottomProperty, value], + [marginLeftProperty, value], + ]; + } +} + +function convertToPaddings(value: string | CoreTypes.LengthType): [CssProperty, CoreTypes.LengthType][] { + if (typeof value === 'string' && value !== 'auto') { + const thickness = parseShorthandPositioning(value); + + return [ + [paddingTopProperty, Length.parse(thickness.top)], + [paddingRightProperty, Length.parse(thickness.right)], + [paddingBottomProperty, Length.parse(thickness.bottom)], + [paddingLeftProperty, Length.parse(thickness.left)], + ]; + } else { + return [ + [paddingTopProperty, value], + [paddingRightProperty, value], + [paddingBottomProperty, value], + [paddingLeftProperty, value], + ]; + } +} + +function convertToTransform(value: string): [CssAnimationProperty, any][] { + 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.z], + [rotateXProperty, rotate.x], + [rotateYProperty, rotate.y], + ]; +} + export const minWidthProperty = new CssProperty({ name: 'minWidth', cssName: 'min-width', @@ -416,97 +590,6 @@ export const verticalAlignmentProperty = new CssProperty, any][] { - if (typeof value === 'string' && value !== 'auto') { - const thickness = parseThickness(value); - - return [ - [marginTopProperty, PercentLength.parse(thickness.top)], - [marginRightProperty, PercentLength.parse(thickness.right)], - [marginBottomProperty, PercentLength.parse(thickness.bottom)], - [marginLeftProperty, PercentLength.parse(thickness.left)], - ]; - } else { - return [ - [marginTopProperty, value], - [marginRightProperty, value], - [marginBottomProperty, value], - [marginLeftProperty, value], - ]; - } -} - -function convertToPaddings(this: void, value: string | CoreTypes.LengthType): [CssProperty, any][] { - if (typeof value === 'string' && value !== 'auto') { - const thickness = parseThickness(value); - - return [ - [paddingTopProperty, Length.parse(thickness.top)], - [paddingRightProperty, Length.parse(thickness.right)], - [paddingBottomProperty, Length.parse(thickness.bottom)], - [paddingLeftProperty, Length.parse(thickness.left)], - ]; - } else { - return [ - [paddingTopProperty, value], - [paddingRightProperty, value], - [paddingBottomProperty, value], - [paddingLeftProperty, value], - ]; - } -} - export const rotateProperty = new CssAnimationProperty({ name: 'rotate', cssName: 'rotate', @@ -555,27 +638,21 @@ export const scaleYProperty = new CssAnimationProperty({ }); scaleYProperty.register(Style); -function parseDIPs(value: string): CoreTypes.dip { - if (value.indexOf('px') !== -1) { - return layout.toDeviceIndependentPixels(parseFloat(value.replace('px', '').trim())); - } else { - return parseFloat(value.replace('dip', '').trim()); - } -} - -export const translateXProperty = new CssAnimationProperty({ +export const translateXProperty = new CssAnimationProperty({ name: 'translateX', cssName: 'translateX', defaultValue: 0, - valueConverter: parseDIPs, + equalityComparer: FixedLength.equals, + valueConverter: FixedLength.parse, }); translateXProperty.register(Style); -export const translateYProperty = new CssAnimationProperty({ +export const translateYProperty = new CssAnimationProperty({ name: 'translateY', cssName: 'translateY', defaultValue: 0, - valueConverter: parseDIPs, + equalityComparer: FixedLength.equals, + valueConverter: FixedLength.parse, }); translateYProperty.register(Style); @@ -608,148 +685,6 @@ const transformProperty = new ShorthandProperty({ }); transformProperty.register(Style); -const IDENTITY_TRANSFORMATION = { - translate: { x: 0, y: 0 }, - rotate: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, -}; - -const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); -const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', '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 }, - }), - - rotate3d: (value) => ({ property: 'rotate', value }), - rotateX: (x) => ({ - property: 'rotate', - value: { - x, - y: IDENTITY_TRANSFORMATION.rotate.y, - z: IDENTITY_TRANSFORMATION.rotate.z, - }, - }), - rotateY: (y) => ({ - property: 'rotate', - value: { - x: IDENTITY_TRANSFORMATION.rotate.x, - y, - z: IDENTITY_TRANSFORMATION.rotate.z, - }, - }), - rotate: (z) => ({ - property: 'rotate', - value: { - x: IDENTITY_TRANSFORMATION.rotate.x, - y: IDENTITY_TRANSFORMATION.rotate.y, - z, - }, - }), -}); - -function convertToTransform(value: string): [CssProperty, any][] { - 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.z], - [rotateXProperty, rotate.x], - [rotateYProperty, rotate.y], - ]; -} - -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 = { ...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[] { - const 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 matches; -} - -function normalizeTransformation({ property, value }: Transformation): Transformation { - return STYLE_TRANSFORMATION_MAP[property](value); -} - -function convertTransformValue(property: string, stringValue: string): TransformationValue { - // eslint-disable-next-line prefer-const - let [x, y, z] = stringValue.split(',').map(parseFloat); - if (property === 'translate') { - y ??= IDENTITY_TRANSFORMATION.translate.y; - } else { - y ??= x; - z ??= y; - } - - if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') { - return stringValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; - } - - return { x, y, z }; -} - // Background properties. const backgroundProperty = new ShorthandProperty({ name: 'background', @@ -768,7 +703,6 @@ export const backgroundInternalProperty = new CssProperty({ }); backgroundInternalProperty.register(Style); -// const pattern: RegExp = /url\(('|")(.*?)\1\)/; export const backgroundImageProperty = new CssProperty({ name: 'backgroundImage', cssName: 'background-image', @@ -834,91 +768,6 @@ export const backgroundPositionProperty = new CssProperty({ }); backgroundPositionProperty.register(Style); -function convertToBackgrounds(this: void, value: string): [CssProperty, any][] { - if (typeof value === 'string') { - const backgrounds = parseBackground(value).value; - let backgroundColor = unsetValue; - if (backgrounds.color) { - backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color); - } - - let backgroundImage: string | LinearGradient; - if (typeof backgrounds.image === 'object' && backgrounds.image) { - backgroundImage = LinearGradient.parse(backgrounds.image); - } else { - backgroundImage = backgrounds.image || unsetValue; - } - - const backgroundRepeat = backgrounds.repeat || unsetValue; - const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue; - - return [ - [backgroundColorProperty, backgroundColor], - [backgroundImageProperty, backgroundImage], - [backgroundRepeatProperty, backgroundRepeat], - [backgroundPositionProperty, backgroundPosition], - ]; - } else { - return [ - [backgroundColorProperty, unsetValue], - [backgroundImageProperty, unsetValue], - [backgroundRepeatProperty, unsetValue], - [backgroundPositionProperty, unsetValue], - ]; - } -} - -function parseBorderColor(value: string): { top: Color; right: Color; bottom: Color; left: Color } { - const result: { top: Color; right: Color; bottom: Color; left: Color } = { - top: undefined, - right: undefined, - bottom: undefined, - left: undefined, - }; - if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) { - result.top = result.right = result.bottom = result.left = new Color(value); - - return result; - } - - const arr = value.split(/[ ,]+/); - if (arr.length === 1) { - const arr0 = new Color(arr[0]); - result.top = arr0; - result.right = arr0; - result.bottom = arr0; - result.left = arr0; - } else if (arr.length === 2) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - result.top = arr0; - result.right = arr1; - result.bottom = arr0; - result.left = arr1; - } else if (arr.length === 3) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - const arr2 = new Color(arr[2]); - result.top = arr0; - result.right = arr1; - result.bottom = arr2; - result.left = arr1; - } else if (arr.length === 4) { - const arr0 = new Color(arr[0]); - const arr1 = new Color(arr[1]); - const arr2 = new Color(arr[2]); - const arr3 = new Color(arr[3]); - result.top = arr0; - result.right = arr1; - result.bottom = arr2; - result.left = arr3; - } else { - throw new Error(`Expected 1, 2, 3 or 4 parameters. Actual: ${value}`); - } - - return result; -} - // Border Color properties. const borderColorProperty = new ShorthandProperty({ name: 'borderColor', @@ -932,13 +781,13 @@ const borderColorProperty = new ShorthandProperty({ }, converter: function (value) { if (typeof value === 'string') { - const fourColors = parseBorderColor(value); + const colors = parseBorderColorPositioning(value); return [ - [borderTopColorProperty, fourColors.top], - [borderRightColorProperty, fourColors.right], - [borderBottomColorProperty, fourColors.bottom], - [borderLeftColorProperty, fourColors.left], + [borderTopColorProperty, new Color(colors.top)], + [borderRightColorProperty, new Color(colors.right)], + [borderBottomColorProperty, new Color(colors.bottom)], + [borderLeftColorProperty, new Color(colors.left)], ]; } else { return [ @@ -1009,13 +858,13 @@ const borderWidthProperty = new ShorthandProperty({ }); boxShadowProperty.register(Style); -function isNonNegativeFiniteNumber(value: number): boolean { - return isFinite(value) && !isNaN(value) && value >= 0; -} - -const supportedPaths = ['rect', 'circle', 'ellipse', 'polygon', 'inset']; -function isClipPathValid(value: string): boolean { - if (!value) { - return true; - } - const functionName = value.substring(0, value.indexOf('(')).trim(); - - return supportedPaths.indexOf(functionName) !== -1; -} - export const clipPathProperty = new CssProperty({ name: 'clipPath', cssName: 'clip-path', @@ -1276,36 +1111,32 @@ export const clipPathProperty = new CssProperty({ }); clipPathProperty.register(Style); -function isFloatValueConverter(value: string): number { - const newValue = parseFloat(value); - if (isNaN(newValue)) { - throw new Error(`Invalid value: ${newValue}`); - } - - return newValue; -} - export const zIndexProperty = new CssProperty({ name: 'zIndex', cssName: 'z-index', - valueConverter: isFloatValueConverter, -}); -zIndexProperty.register(Style); + valueConverter: (value: string): number => { + const newValue = parseFloat(value); + if (isNaN(newValue)) { + throw new Error(`Invalid value: ${newValue}`); + } -function opacityConverter(value: any): number { - const newValue = parseFloat(value); - if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) { return newValue; - } - - throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); -} + }, +}); +zIndexProperty.register(Style); export const opacityProperty = new CssAnimationProperty({ name: 'opacity', cssName: 'opacity', defaultValue: 1, - valueConverter: opacityConverter, + valueConverter: (value: any): number => { + const newValue = parseFloat(value); + if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) { + return newValue; + } + + throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); + }, }); opacityProperty.register(Style); From 05293b530f2785b8a3f08e2cda64a11454ec9903 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 1 Feb 2025 18:42:22 +0200 Subject: [PATCH 2/6] ref: Improved clip-path parse and validation mechanism --- packages/core/ui/styling/background-common.ts | 22 ++++--- .../core/ui/styling/background.android.ts | 5 +- packages/core/ui/styling/background.d.ts | 17 ++--- packages/core/ui/styling/background.ios.ts | 45 +++++++------- .../core/ui/styling/clip-path-function.ts | 39 ++++++++++++ .../core/ui/styling/style-properties.d.ts | 6 +- packages/core/ui/styling/style-properties.ts | 62 +++++++++++++------ packages/core/ui/styling/style/index.ts | 3 +- 8 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 packages/core/ui/styling/clip-path-function.ts diff --git a/packages/core/ui/styling/background-common.ts b/packages/core/ui/styling/background-common.ts index 4d52da76cd..e7fdd3479c 100644 --- a/packages/core/ui/styling/background-common.ts +++ b/packages/core/ui/styling/background-common.ts @@ -3,6 +3,7 @@ import { LinearGradient } from './linear-gradient'; // Types. import { Color } from '../../color'; import { BoxShadow } from './box-shadow'; +import { ClipPathFunction } from './clip-path-function'; /** * Flags used to hint the background handler if it has to clear a specific property @@ -39,7 +40,7 @@ export class Background { public borderTopRightRadius = 0; public borderBottomLeftRadius = 0; public borderBottomRightRadius = 0; - public clipPath: string; + public clipPath: string | ClipPathFunction; public boxShadow: BoxShadow; public clearFlags: number = BackgroundClearFlags.NONE; @@ -192,7 +193,7 @@ export class Background { return clone; } - public withClipPath(value: string): Background { + public withClipPath(value: string | ClipPathFunction): Background { const clone = this.clone(); clone.clipPath = value; @@ -224,16 +225,23 @@ export class Background { return false; } - let imagesEqual = false; + let isImageEqual = false; if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) { - imagesEqual = LinearGradient.equals(value1, value2); + isImageEqual = LinearGradient.equals(value1, value2); } else { - imagesEqual = value1.image === value2.image; + isImageEqual = value1.image === value2.image; + } + + let isClipPathEqual = false; + if (value1.clipPath instanceof ClipPathFunction && value2.clipPath instanceof ClipPathFunction) { + isClipPathEqual = ClipPathFunction.equals(value1.clipPath, value2.clipPath); + } else { + isClipPathEqual = value1.clipPath === value2.clipPath; } return ( Color.equals(value1.color, value2.color) && - imagesEqual && + isImageEqual && value1.position === value2.position && value1.repeat === value2.repeat && value1.size === value2.size && @@ -249,7 +257,7 @@ export class Background { value1.borderTopRightRadius === value2.borderTopRightRadius && value1.borderBottomRightRadius === value2.borderBottomRightRadius && value1.borderBottomLeftRadius === value2.borderBottomLeftRadius && - value1.clipPath === value2.clipPath + isClipPathEqual // && value1.clearFlags === value2.clearFlags ); } diff --git a/packages/core/ui/styling/background.android.ts b/packages/core/ui/styling/background.android.ts index 08885273e8..ec4d0bed55 100644 --- a/packages/core/ui/styling/background.android.ts +++ b/packages/core/ui/styling/background.android.ts @@ -1,5 +1,6 @@ import { View } from '../core/view'; import { LinearGradient } from './linear-gradient'; +import { ClipPathFunction } from './clip-path-function'; import { isDataURI, isFileOrResourcePath, RESOURCE_PREFIX, FILE_PREFIX } from '../../utils'; import { parse } from '../../css-value'; import { path, knownFolders } from '../../file-system'; @@ -92,7 +93,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri background.borderBottomRightRadius, background.borderBottomLeftRadius, - background.clipPath, + background.clipPath instanceof ClipPathFunction ? background.clipPath.toString() : background.clipPath, background.color ? background.color.android : 0, imageUri, @@ -103,7 +104,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri background.position, backgroundPositionParsedCSSValues, background.size, - backgroundSizeParsedCSSValues + backgroundSizeParsedCSSValues, ); //console.log(`>>> ${borderDrawable.toDebugString()}`); } diff --git a/packages/core/ui/styling/background.d.ts b/packages/core/ui/styling/background.d.ts index 934e40a4f3..4adc26080f 100644 --- a/packages/core/ui/styling/background.d.ts +++ b/packages/core/ui/styling/background.d.ts @@ -1,7 +1,8 @@ import { Color } from '../../color'; import { View } from '../core/view'; import { BackgroundRepeat } from '../../css/parser'; -import { LinearGradient } from '../styling/linear-gradient'; +import { LinearGradient } from './linear-gradient'; +import { ClipPathFunction } from './clip-path-function'; import { BoxShadow } from './box-shadow'; import { Background as BackgroundDefinition } from './background-common'; @@ -32,7 +33,7 @@ export enum CacheMode { // public borderTopRightRadius: number; // public borderBottomRightRadius: number; // public borderBottomLeftRadius: number; -// public clipPath: string; +// public clipPath: string | ClipPathFunction; // public boxShadow: string | BoxShadow; // public clearFlags: number; @@ -80,12 +81,12 @@ export namespace ios { export function drawBackgroundVisualEffects(view: View): void; export function clearBackgroundVisualEffects(view: View): void; export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: any) => void): void; - export function generateClipPath(view: View, bounds: CGRect): any; - export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any }; - export function getUniformBorderRadius(view: View, bounds: CGRect): number; - export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: CGRect): any; - export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: CGRect): any; - export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: CGRect): Array; + export function generateClipPath(view: View, bounds: any /* CGRect */): any; + export function generateShadowLayerPaths(view: View, bounds: any /* CGRect */): { maskPath: any; shadowPath: any }; + export function getUniformBorderRadius(view: View, bounds: any /* CGRect */): number; + export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: any /* CGRect */): any; + export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: any /* CGRect */): any; + export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: any /* CGRect */): Array; } export namespace ad { diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts index 2e49ab31ab..01632991a5 100644 --- a/packages/core/ui/styling/background.ios.ts +++ b/packages/core/ui/styling/background.ios.ts @@ -11,6 +11,7 @@ import { parse as cssParse } from '../../css-value/reworkcss-value.js'; import { BoxShadow } from './box-shadow'; import { Length } from './style-properties'; import { BackgroundClearFlags } from './background-common'; +import { ClipPathFunction } from './clip-path-function'; export * from './background-common'; @@ -303,30 +304,28 @@ export namespace ios { let path: UIBezierPath; const clipPath = background.clipPath; - const functionName: string = clipPath.substring(0, clipPath.indexOf('(')); - const value: string = clipPath.replace(`${functionName}(`, '').replace(')', ''); - - switch (functionName) { - case 'rect': - path = rectPath(value, position); - break; - - case 'inset': - path = insetPath(value, position); - break; - - case 'circle': - path = circlePath(value, position); - break; - - case 'ellipse': - path = ellipsePath(value, position); - break; - - case 'polygon': - path = polygonPath(value, position); - break; + if (clipPath instanceof ClipPathFunction) { + switch (clipPath.shape) { + case 'rect': + path = rectPath(clipPath.rule, position); + break; + case 'inset': + path = insetPath(clipPath.rule, position); + break; + case 'circle': + path = circlePath(clipPath.rule, position); + break; + case 'ellipse': + path = ellipsePath(clipPath.rule, position); + break; + case 'polygon': + path = polygonPath(clipPath.rule, position); + break; + } + } else { + path = null; } + return path; } diff --git a/packages/core/ui/styling/clip-path-function.ts b/packages/core/ui/styling/clip-path-function.ts new file mode 100644 index 0000000000..c351add29d --- /dev/null +++ b/packages/core/ui/styling/clip-path-function.ts @@ -0,0 +1,39 @@ +type ClipPathShape = 'rect' | 'circle' | 'ellipse' | 'polygon' | 'inset'; + +interface IClipPathFunction { + shape: ClipPathShape; + rule: string; +} + +export class ClipPathFunction implements IClipPathFunction { + private readonly _shape: ClipPathShape; + private readonly _rule: string; + + constructor(shape: ClipPathShape, rule: string) { + this._shape = shape; + this._rule = rule; + } + + get shape(): ClipPathShape { + return this._shape; + } + + get rule(): string { + return this._rule; + } + + public static equals(value1: ClipPathFunction, value2: ClipPathFunction): boolean { + return value1.shape === value2.shape && value1.rule === value2.rule; + } + + toJSON(): IClipPathFunction { + return { + shape: this._shape, + rule: this._rule, + }; + } + + toString(): string { + return `${this._shape}(${this._rule})`; + } +} diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 086e44d17b..0743bd35b6 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -4,6 +4,8 @@ import { CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssPrope import { Style } from './style'; import { Font, FontStyleType, FontWeightType, FontVariationSettingsType } from './font'; import { Background } from './background'; +import { ClipPathFunction } from './clip-path-function'; +import { LinearGradient } from './linear-gradient'; export namespace FixedLength { export function parse(text: string): CoreTypes.FixedLengthType; @@ -49,12 +51,12 @@ export const scaleYProperty: CssAnimationProperty; export const translateXProperty: CssAnimationProperty; export const translateYProperty: CssAnimationProperty; -export const clipPathProperty: CssProperty; +export const clipPathProperty: CssProperty; export const colorProperty: InheritedCssProperty; export const backgroundProperty: ShorthandProperty; export const backgroundColorProperty: CssAnimationProperty; -export const backgroundImageProperty: CssProperty; +export const backgroundImageProperty: CssProperty; export const backgroundRepeatProperty: CssProperty; export const backgroundSizeProperty: CssProperty; export const backgroundPositionProperty: CssProperty; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index c9804433bd..8c0322dd83 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1,4 +1,3 @@ -// Types import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; import { Style } from './style'; @@ -14,10 +13,9 @@ import { parseBackground } from '../../css/parser'; import { LinearGradient } from './linear-gradient'; import { parseCSSShadow, ShadowCSSValues } from './css-shadow'; import { transformConverter } from './css-transform'; +import { ClipPathFunction } from './clip-path-function'; -const supportedClipPaths = ['rect', 'circle', 'ellipse', 'polygon', 'inset']; - -interface CSSPositioning { +interface ShorthandPositioning { top: string; right: string; bottom: string; @@ -202,16 +200,31 @@ export namespace Length { } = convertToStringCommon; } -function isClipPathValid(value: string): boolean { - if (!value) { - return true; - } +function parseClipPath(value: string): string | ClipPathFunction { + const functionStartIndex = value.indexOf('('); - const functionName = value.substring(0, value.indexOf('(')).trim(); - return supportedClipPaths.indexOf(functionName) !== -1; + if (functionStartIndex > -1) { + const functionName = value.substring(0, functionStartIndex).trim(); + + switch (functionName) { + case 'rect': + case 'circle': + case 'ellipse': + case 'polygon': + case 'inset': { + const rule: string = value.replace(`${functionName}(`, '').replace(')', ''); + return new ClipPathFunction(functionName, rule); + } + default: + throw new Error(`Clip-path function ${functionName} is not valid.`); + } + } else { + // Only shape functions are supported for now + throw new Error(`Clip-path value ${value} is not valid.`); + } } -function parseShorthandPositioning(value: string): CSSPositioning { +function parseShorthandPositioning(value: string): ShorthandPositioning { const arr = value.split(/[ ,]+/); let top: string; @@ -251,7 +264,7 @@ function parseShorthandPositioning(value: string): CSSPositioning { }; } -function parseBorderColorPositioning(value: string): CSSPositioning { +function parseBorderColorPositioning(value: string): ShorthandPositioning { if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) { return { top: value, @@ -264,7 +277,7 @@ function parseBorderColorPositioning(value: string): CSSPositioning { return parseShorthandPositioning(value); } -function convertToBackgrounds(value: string): [CssProperty, any][] { +function convertToBackgrounds(value: string): [CssProperty | CssAnimationProperty, any][] { if (typeof value === 'string') { const backgrounds = parseBackground(value).value; let backgroundColor = unsetValue; @@ -283,14 +296,14 @@ function convertToBackgrounds(value: string): [CssProperty, any][] { const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue; return [ - [backgroundColorProperty, backgroundColor], + [backgroundColorProperty, backgroundColor], [backgroundImageProperty, backgroundImage], [backgroundRepeatProperty, backgroundRepeat], [backgroundPositionProperty, backgroundPosition], ]; } else { return [ - [backgroundColorProperty, unsetValue], + [backgroundColorProperty, unsetValue], [backgroundImageProperty, unsetValue], [backgroundRepeatProperty, unsetValue], [backgroundPositionProperty, unsetValue], @@ -1098,15 +1111,24 @@ const boxShadowProperty = new CssProperty({ }); boxShadowProperty.register(Style); -export const clipPathProperty = new CssProperty({ +export const clipPathProperty = new CssProperty({ name: 'clipPath', cssName: 'clip-path', valueChanged: (target, oldValue, newValue) => { - if (!isClipPathValid(newValue)) { - throw new Error('clip-path is not valid.'); + target.backgroundInternal = target.backgroundInternal.withClipPath(newValue); + }, + equalityComparer: (value1, value2) => { + if (value1 instanceof ClipPathFunction && value2 instanceof ClipPathFunction) { + return ClipPathFunction.equals(value1, value2); + } + return value1 === value2; + }, + valueConverter(value: string | ClipPathFunction) { + if (typeof value === 'string') { + return parseClipPath(value); } - target.backgroundInternal = target.backgroundInternal.withClipPath(newValue); + return value; }, }); clipPathProperty.register(Style); @@ -1129,7 +1151,7 @@ export const opacityProperty = new CssAnimationProperty({ name: 'opacity', cssName: 'opacity', defaultValue: 1, - valueConverter: (value: any): number => { + valueConverter: (value: string): number => { const newValue = parseFloat(value); if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) { return newValue; diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 30839455a6..29bffe392a 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -12,6 +12,7 @@ import { CoreTypes } from '../../../core-types'; import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types'; import { ShadowCSSValues } from '../css-shadow'; import { StrokeCSSValues } from '../css-stroke'; +import { ClipPathFunction } from '../clip-path-function'; export interface CommonLayoutParams { width: number; @@ -122,7 +123,7 @@ export class Style extends Observable implements StyleDefinition { public translateX: CoreTypes.dip; public translateY: CoreTypes.dip; - public clipPath: string; + public clipPath: string | ClipPathFunction; public color: Color; public tintColor: Color; public placeholderColor: Color; From 343ac7a41b8f0260c7f3e86f50cd6141c14a6d54 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 2 Feb 2025 02:19:42 +0200 Subject: [PATCH 3/6] fix: Added missing 'none' handling --- packages/core/ui/styling/style-properties.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 8c0322dd83..a4fb027abf 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -219,7 +219,11 @@ function parseClipPath(value: string): string | ClipPathFunction { throw new Error(`Clip-path function ${functionName} is not valid.`); } } else { - // Only shape functions are supported for now + if (value === 'none') { + return null; + } + + // Only shape functions and none are supported for now throw new Error(`Clip-path value ${value} is not valid.`); } } From a0760c9b36e142fd6747bb744a9d5c9c9149cbd6 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 2 Feb 2025 02:34:29 +0200 Subject: [PATCH 4/6] chore: Make use of non-negative number validate function --- packages/core/ui/styling/style-properties.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index a4fb027abf..6d097189bc 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -179,10 +179,6 @@ export namespace FixedLength { } = convertToStringCommon; } -function isNonNegativeFiniteNumber(value: number): boolean { - return isFinite(value) && !isNaN(value) && value >= 0; -} - export namespace Length { export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType { if (fromValue == 'auto') { @@ -200,6 +196,10 @@ export namespace Length { } = convertToStringCommon; } +function isNonNegativeFiniteNumber(value: number): boolean { + return isFinite(value) && !isNaN(value) && value >= 0; +} + function parseClipPath(value: string): string | ClipPathFunction { const functionStartIndex = value.indexOf('('); @@ -1157,11 +1157,11 @@ export const opacityProperty = new CssAnimationProperty({ defaultValue: 1, valueConverter: (value: string): number => { const newValue = parseFloat(value); - if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) { - return newValue; + if (!isNonNegativeFiniteNumber(newValue) || newValue > 1) { + throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); } - throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`); + return newValue; }, }); opacityProperty.register(Style); From 98aca521450cb0542db2de2aebc72ad0b620ae3e Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 2 Feb 2025 02:38:35 +0200 Subject: [PATCH 5/6] chore: Minor type correction --- packages/core/ui/animation/animation-interfaces.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/ui/animation/animation-interfaces.ts b/packages/core/ui/animation/animation-interfaces.ts index 4c2efce1c2..4e3600b546 100644 --- a/packages/core/ui/animation/animation-interfaces.ts +++ b/packages/core/ui/animation/animation-interfaces.ts @@ -32,7 +32,6 @@ export interface AnimationPromise extends Promise, Cancelable { export interface Pair { x: number; y: number; - z: number; } export interface Cancelable { From 62b4a2697ab23601ba7f0a65e5ba764803277488 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sun, 2 Feb 2025 03:16:15 +0200 Subject: [PATCH 6/6] chore: More type improvements for transform module --- .../core/ui/animation/animation-interfaces.ts | 2 +- packages/core/ui/animation/index.d.ts | 2 +- packages/core/ui/styling/css-transform.ts | 26 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/core/ui/animation/animation-interfaces.ts b/packages/core/ui/animation/animation-interfaces.ts index 4e3600b546..a37c7a603a 100644 --- a/packages/core/ui/animation/animation-interfaces.ts +++ b/packages/core/ui/animation/animation-interfaces.ts @@ -8,7 +8,7 @@ export type Transformation = { value: TransformationValue; }; -export type TransformationType = 'rotate' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY'; +export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY'; export type TransformationValue = Point3D | Pair | number; diff --git a/packages/core/ui/animation/index.d.ts b/packages/core/ui/animation/index.d.ts index 0041ba53b5..4eaae4c6cd 100644 --- a/packages/core/ui/animation/index.d.ts +++ b/packages/core/ui/animation/index.d.ts @@ -83,7 +83,7 @@ export type Transformation = { /** * Defines possible css transformations */ -export type TransformationType = 'rotate' | 'rotateX' | 'rotateY' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY'; +export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY'; /** * Defines possible css transformation values diff --git a/packages/core/ui/styling/css-transform.ts b/packages/core/ui/styling/css-transform.ts index ea124bcc6d..0187e6a172 100644 --- a/packages/core/ui/styling/css-transform.ts +++ b/packages/core/ui/styling/css-transform.ts @@ -1,4 +1,4 @@ -import { Pair, Transformation, TransformationValue, TransformFunctionsInfo } from '../animation'; +import { Pair, Transformation, TransformationType, TransformationValue, TransformFunctionsInfo } from '../animation'; import { radiansToDegrees } from '../../utils/number-utils'; import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix'; import { hasDuplicates } from '../../utils'; @@ -14,7 +14,7 @@ const IDENTITY_TRANSFORMATION = { }; const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); -const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']); +const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']); const STYLE_TRANSFORMATION_MAP: TransformationStyleMap = Object.freeze({ scale: (value: number) => ({ property: 'scale', value }), @@ -88,31 +88,31 @@ export function transformConverter(text: string): TransformFunctionsInfo { return decompose2DTransformMatrix(cssMatrix); } +function isTransformType(propertyName: string): propertyName is TransformationType { + return (TRANSFORMATIONS as string[]).indexOf(propertyName) !== -1; +} + // 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[] { const matches: Transformation[] = []; - let match; + let match: RegExpExecArray; 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 })); + if (isTransformType(property)) { + const value = convertTransformValue(property, match[2]); + matches.push(STYLE_TRANSFORMATION_MAP[property](value)); } } return matches; } -function normalizeTransformation({ property, value }: Transformation): Transformation { - return STYLE_TRANSFORMATION_MAP[property](value); -} - -function convertTransformValue(property: string, stringValue: string): TransformationValue { - const values = stringValue.split(',').map(parseFloat); +function convertTransformValue(property: TransformationType, rawValue: string): TransformationValue { + const values = rawValue.split(',').map(parseFloat); const x = values[0]; let y = values[1]; @@ -126,7 +126,7 @@ function convertTransformValue(property: string, stringValue: string): Transform } if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') { - return stringValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; + return rawValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x; } return { x, y, z };