Skip to content

Commit

Permalink
JS plumbing to get filters into native
Browse files Browse the repository at this point in the history
Summary:
This is the JS plumbing to get it so that views can now use filters. The typing looks like

`filter: [{brightness: 1.5}, {hueRotate: '90deg'}]`

which is different than web which would look like `filter: brightness(1.5) hue-rotate(90deg)`. I feel like the web version is overly complicated and not very *react native-y*. Transform uses the array based approach (albeit they also accept a string). Open to changing this but really feel like the web format is silly and bad since it would just involve parsing some arbitrary string.

The diff includes:

* Style sheet changes so typing is valid
* Process function to turn filter format into {name: string, amount: string}
* Test for process function
* View config changes on Android, iOS and ReactNativeStyleAttributes

Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D56845572
  • Loading branch information
joevilches authored and facebook-github-bot committed May 7, 2024
1 parent 00428f5 commit 768c309
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes';

import processAspectRatio from '../../StyleSheet/processAspectRatio';
import processColor from '../../StyleSheet/processColor';
import processFilter from '../../StyleSheet/processFilter';
import processFontVariant from '../../StyleSheet/processFontVariant';
import processTransform from '../../StyleSheet/processTransform';
import processTransformOrigin from '../../StyleSheet/processTransformOrigin';
Expand Down Expand Up @@ -114,6 +115,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
transform: {process: processTransform},
transformOrigin: {process: processTransformOrigin},

/**
* Filter
*/
experimental_filter: {process: processFilter},

/**
* View
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ const validAttributesForNonEventProps = {
backgroundColor: {process: require('../StyleSheet/processColor').default},
transform: true,
transformOrigin: true,
experimental_filter: {
process: require('../StyleSheet/processFilter').default,
},
opacity: true,
elevation: true,
shadowColor: {process: require('../StyleSheet/processColor').default},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ const validAttributesForNonEventProps = {
hitSlop: {diff: require('../Utilities/differ/insetsDiffer')},
collapsable: true,
collapsableChildren: true,
experimental_filter: {
process: require('../StyleSheet/processFilter').default,
},

borderTopWidth: true,
borderTopColor: {process: require('../StyleSheet/processColor').default},
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'use strict';

import type AnimatedNode from '../Animated/nodes/AnimatedNode';
import type {FilterPrimitive} from '../StyleSheet/processFilter';
import type {
____DangerouslyImpreciseStyle_InternalOverrides,
____ImageStyle_InternalOverrides,
Expand Down Expand Up @@ -690,10 +691,15 @@ export type ____ShadowStyle_Internal = $ReadOnly<{
...____ShadowStyle_InternalOverrides,
}>;

type ____FilterStyle_Internal = $ReadOnly<{
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
}>;

export type ____ViewStyle_InternalCore = $ReadOnly<{
...$Exact<____LayoutStyle_Internal>,
...$Exact<____ShadowStyle_Internal>,
...$Exact<____TransformStyle_Internal>,
...____FilterStyle_Internal,
backfaceVisibility?: 'visible' | 'hidden',
backgroundColor?: ____ColorValue_Internal,
borderColor?: ____ColorValue_Internal,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
* @flow strict-local
*/

'use strict';

import type {FilterPrimitive} from '../processFilter';

const processFilter = require('../processFilter').default;

// js1 test processFilter
describe('processFilter', () => {
testStandardFilter('brightness');
testStandardFilter('opacity');
testStandardFilter('contrast');
testStandardFilter('saturate');
testStandardFilter('grayscale');
testStandardFilter('sepia');
testStandardFilter('invert');

testNumericFilter('blur', 5, [
{
blur: 5,
},
]);
testNumericFilter('blur', -5, []);
testUnitFilter('blur', 5, '%', []);
testUnitFilter('blur', 5, 'px', [
{
blur: 5,
},
]);

testNumericFilter('hueRotate', 0, [{hueRotate: 0}]);
testUnitFilter('hueRotate', 90, 'deg', [{hueRotate: 90}]);
testUnitFilter('hueRotate', 1.5708, 'rad', [
{hueRotate: (180 * 1.5708) / Math.PI},
]);
testUnitFilter('hueRotate', -90, 'deg', [{hueRotate: -90}]);
testUnitFilter('hueRotate', 1.5, 'grad', []);
testNumericFilter('hueRotate', 90, []);
testUnitFilter('hueRotate', 50, '%', []);

it('multiple filters', () => {
expect(
processFilter([
{brightness: 0.5},
{opacity: 0.5},
{blur: 5},
{hueRotate: '90deg'},
]),
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
});
it('multiple filters one invalid', () => {
expect(
processFilter([
{brightness: 0.5},
{opacity: 0.5},
{blur: 5},
{hueRotate: '90foo'},
]),
).toEqual([]);
});
it('multiple same filters', () => {
expect(
processFilter([
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
]),
).toEqual([
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
]);
});
it('empty', () => {
expect(processFilter([])).toEqual([]);
});
it('Non filter', () => {
// $FlowExpectedError[incompatible-call]
expect(processFilter([{foo: 5}])).toEqual([]);
});
it('Invalid amount type', () => {
// $FlowExpectedError[incompatible-call]
expect(processFilter([{brightness: {}}])).toEqual([]);
});
it('string multiple filters', () => {
expect(
processFilter('brightness(0.5) opacity(0.5) blur(5) hueRotate(90deg)'),
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
});
it('string multiple filters one invalid', () => {
expect(
processFilter('brightness(0.5) opacity(0.5) blur(5) hueRotate(90foo)'),
).toEqual([]);
});
it('string multiple same filters', () => {
expect(
processFilter(
'brightness(0.5) brightness(0.5) brightness(0.5) brightness(0.5)',
),
).toEqual([
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
{brightness: 0.5},
]);
});
it('string empty', () => {
expect(processFilter('')).toEqual([]);
});
it('string non filter', () => {
// $FlowExpectedError[incompatible-call]
expect(processFilter('foo: 5')).toEqual([]);
});
it('string invalid amount type', () => {
// $FlowExpectedError[incompatible-call]
expect(processFilter('brightness: {}')).toEqual([]);
});
it('string brightness(.5)', () => {
// $FlowExpectedError[incompatible-call]
expect(processFilter('brightness(.5)')).toEqual([{brightness: 0.5}]);
});
});

function testStandardFilter(filter: string): void {
const value = 0.5;
const expected = createFilterPrimitive(filter, value);
const percentExpected = createFilterPrimitive(filter, value / 100);

testNumericFilter(filter, value, [expected]);
testNumericFilter(filter, -value, []);
testUnitFilter(filter, value, 'px', [expected]);
testUnitFilter(filter, value, '%', [percentExpected]);
}

function testNumericFilter(
filter: string,
value: number,
expected: Array<FilterPrimitive>,
): void {
const filterObject = createFilterPrimitive(filter, value);
const filterString = filter + '(' + value.toString() + ')';

it(filterString, () => {
expect(processFilter([filterObject])).toEqual(expected);
});
it('string ' + filterString, () => {
expect(processFilter(filterString)).toEqual(expected);
});
}

function testUnitFilter(
filter: string,
value: number,
unit: string,
expected: Array<FilterPrimitive>,
): void {
const unitAmount = value + unit;
const filterObject = createFilterPrimitive(filter, unitAmount);
const filterString = filter + '(' + unitAmount + ')';

it(filterString, () => {
expect(processFilter([filterObject])).toEqual(expected);
});
it('string ' + filterString, () => {
expect(processFilter(filterString)).toEqual(expected);
});
}

function createFilterPrimitive(
filter: string,
value: number | string,
): FilterPrimitive {
switch (filter) {
case 'brightness':
return {brightness: value};
case 'blur':
return {blur: value};
case 'contrast':
return {contrast: value};
case 'grayscale':
return {grayscale: value};
case 'hueRotate':
return {hueRotate: value};
case 'invert':
return {invert: value};
case 'opacity':
return {opacity: value};
case 'saturate':
return {saturate: value};
case 'sepia':
return {sepia: value};
default:
throw new Error('Invalid filter: ' + filter);
}
}

0 comments on commit 768c309

Please sign in to comment.