make reanimated work in web (#1886)

Up until now, trying to use reanimated with react-native-svg in react-native-web resulted in an error.
This adds a setNativeProps function to the web implementation to directly modify the transform and style props on a SVGElement ref.

Since there is a need to track the "last merged props" and those need to be reset on every render, the render method has been moved into the WebShape class and a tag string property has been added.

As g had some extra handling for x and y, a prepareProps function was added as well.
This commit is contained in:
Lenz Weber-Tronic
2022-10-13 14:07:20 +02:00
committed by GitHub
parent 795bff5f37
commit afaf500db9
9 changed files with 200 additions and 78 deletions
+4
View File
@@ -1,3 +1,7 @@
module.exports = { module.exports = {
presets: ['module:metro-react-native-babel-preset'], presets: ['module:metro-react-native-babel-preset'],
plugins: [
'@babel/plugin-proposal-export-namespace-from',
'react-native-reanimated/plugin',
],
}; };
+1
View File
@@ -16,6 +16,7 @@
"react": "18.1.0", "react": "18.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-native": "0.70.0", "react-native": "0.70.0",
"react-native-reanimated": "^2.10.0",
"react-native-svg": "link:../", "react-native-svg": "link:../",
"react-native-web": "^0.17.7" "react-native-web": "^0.17.7"
}, },
+1
View File
@@ -112,6 +112,7 @@ const names: (keyof typeof examples)[] = [
'TouchEvents', 'TouchEvents',
'PanResponder', 'PanResponder',
'Reusable', 'Reusable',
'Reanimated',
]; ];
const initialState = { const initialState = {
+2
View File
@@ -15,6 +15,7 @@ import * as Image from './examples/Image';
import * as Reusable from './examples/Reusable'; import * as Reusable from './examples/Reusable';
import * as TouchEvents from './examples/TouchEvents'; import * as TouchEvents from './examples/TouchEvents';
import * as PanResponder from './examples/PanResponder'; import * as PanResponder from './examples/PanResponder';
import * as Reanimated from './examples/Reanimated';
export { export {
Svg, Svg,
@@ -34,4 +35,5 @@ export {
TouchEvents, TouchEvents,
Reusable, Reusable,
PanResponder, PanResponder,
Reanimated,
}; };
+42
View File
@@ -0,0 +1,42 @@
import React, {useEffect} from 'react';
import {StyleSheet, Text} from 'react-native';
import Reanimated, {
useAnimatedProps,
useSharedValue,
withRepeat,
withSpring,
withTiming,
} from 'react-native-reanimated';
import {Svg, Rect} from 'react-native-svg';
const ReanimatedRect = Reanimated.createAnimatedComponent(Rect);
function ReanimatedRectExample() {
const height = useSharedValue(10);
const position = useSharedValue(0);
useEffect(() => {
height.value = withRepeat(withSpring(100), -1, true);
position.value = withRepeat(withTiming(300, {duration: 5000}), -1);
});
const animatedProps = useAnimatedProps(() => ({
width: 30,
height: height.value,
x: position.value,
y: 20,
}));
return (
<Svg height="150" width="300">
<ReanimatedRect animatedProps={animatedProps} fill="red" />
</Svg>
);
}
ReanimatedRectExample.title = 'reanimated rectangle';
const samples = [ReanimatedRectExample];
const style = StyleSheet.create({text: {width: 30, height: 30}});
const icon = <Text style={style.text}>R</Text>;
export {icon, samples};
+4 -1
View File
@@ -1,4 +1,7 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"include": ["src/**/*"] "include": ["src/**/*"],
"compilerOptions": {
"skipLibCheck": true
}
} }
+2
View File
@@ -23,6 +23,7 @@ module.exports = {
fromRoot('index.js'), fromRoot('index.js'),
fromRoot('src'), fromRoot('src'),
fromRoot('node_modules/react-native-svg'), fromRoot('node_modules/react-native-svg'),
fromRoot('node_modules/react-native-reanimated'),
], ],
}, },
{ {
@@ -52,4 +53,5 @@ module.exports = {
'.jsx', '.jsx',
], ],
}, },
plugins: [new (require('webpack').DefinePlugin)({process: {env: {}}})],
}; };
+36 -1
View File
@@ -578,6 +578,13 @@
"@babel/helper-create-regexp-features-plugin" "^7.19.0" "@babel/helper-create-regexp-features-plugin" "^7.19.0"
"@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-plugin-utils" "^7.19.0"
"@babel/plugin-transform-object-assign@^7.16.7":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2"
integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A==
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-object-super@^7.0.0": "@babel/plugin-transform-object-super@^7.0.0":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c"
@@ -699,7 +706,7 @@
"@babel/helper-validator-option" "^7.18.6" "@babel/helper-validator-option" "^7.18.6"
"@babel/plugin-transform-flow-strip-types" "^7.18.6" "@babel/plugin-transform-flow-strip-types" "^7.18.6"
"@babel/preset-typescript@^7.13.0": "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399"
integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==
@@ -1440,6 +1447,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/invariant@^2.2.35":
version "2.2.35"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be"
integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
@@ -5253,6 +5265,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.merge@^4.6.2: lodash.merge@^4.6.2:
version "4.6.2" version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -6544,6 +6561,19 @@ react-native-gradle-plugin@^0.70.2:
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.2.tgz#b5130f2c196e27c4c5912706503d69b8790f1937" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.2.tgz#b5130f2c196e27c4c5912706503d69b8790f1937"
integrity sha512-k7d+CVh0fs/VntA2WaKD58cFB2rtiSLBHYlciH18ncaT4N/B3A4qOGv9pSCEHfQikELm6vAf98KMbE3c8KnH1A== integrity sha512-k7d+CVh0fs/VntA2WaKD58cFB2rtiSLBHYlciH18ncaT4N/B3A4qOGv9pSCEHfQikELm6vAf98KMbE3c8KnH1A==
react-native-reanimated@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz#ed53be66bbb553b5b5e93e93ef4217c87b8c73db"
integrity sha512-jKm3xz5nX7ABtHzzuuLmawP0pFWP77lXNdIC6AWOceBs23OHUaJ29p4prxr/7Sb588GwTbkPsYkDqVFaE3ezNQ==
dependencies:
"@babel/plugin-transform-object-assign" "^7.16.7"
"@babel/preset-typescript" "^7.16.7"
"@types/invariant" "^2.2.35"
invariant "^2.2.4"
lodash.isequal "^4.5.0"
setimmediate "^1.0.5"
string-hash-64 "^1.0.3"
"react-native-svg@link:..": "react-native-svg@link:..":
version "0.0.0" version "0.0.0"
uid "" uid ""
@@ -7353,6 +7383,11 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
string-hash-64@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==
string-length@^4.0.1: string-length@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
+108 -76
View File
@@ -58,13 +58,19 @@ interface BaseProps {
fontWeight?: NumberProp; fontWeight?: NumberProp;
fontSize?: NumberProp; fontSize?: NumberProp;
fontFamily?: string; fontFamily?: string;
forwardedRef: {}; forwardedRef?:
| React.RefCallback<SVGElement>
| React.MutableRefObject<SVGElement | null>;
style: Iterable<{}>; style: Iterable<{}>;
} }
const hasTouchableProperty = (props: BaseProps) => const hasTouchableProperty = (props: BaseProps) =>
props.onPress || props.onPressIn || props.onPressOut || props.onLongPress; props.onPress || props.onPressIn || props.onPressOut || props.onLongPress;
const camelCaseToDashed = (camelCase: string) => {
return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
};
/** /**
* `react-native-svg` supports additional props that aren't defined in the spec. * `react-native-svg` supports additional props that aren't defined in the spec.
* This function replaces them in a spec conforming manner. * This function replaces them in a spec conforming manner.
@@ -156,9 +162,14 @@ const prepare = <T extends BaseProps>(
clean.transform = transform.join(' '); clean.transform = transform.join(' ');
} }
if (forwardedRef) { clean.ref = (el: SVGElement | null) => {
clean.ref = forwardedRef; self.elementRef.current = el;
} if (typeof forwardedRef === 'function') {
forwardedRef(el);
} else if (forwardedRef) {
forwardedRef.current = el;
}
};
const styles: { const styles: {
fontStyle?: string; fontStyle?: string;
@@ -237,6 +248,53 @@ export class WebShape<
C = {}, C = {},
> extends React.Component<P, C> { > extends React.Component<P, C> {
[x: string]: unknown; [x: string]: unknown;
protected tag?: React.ElementType;
protected prepareProps(props: P) {
return props;
}
elementRef =
React.createRef<SVGElement>() as React.MutableRefObject<SVGElement | null>;
lastMergedProps: Partial<P> = {};
/**
* disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it
*/
setNativeProps(props: { style: P }) {
const merged = Object.assign(
{},
this.props,
this.lastMergedProps,
props.style,
);
this.lastMergedProps = merged;
const clean = prepare(this, this.prepareProps(merged));
const current = this.elementRef.current;
if (current) {
for (const cleanAttribute of Object.keys(clean)) {
const cleanValue = clean[cleanAttribute as keyof typeof clean];
switch (cleanAttribute) {
case 'ref':
case 'children':
break;
case 'style':
// style can be an object here or an array, so we convert it to an array and assign each element
for (const partialStyle of ([] as {}[]).concat(clean.style ?? [])) {
// @ts-expect-error "DOM" is not part of `compilerOptions.lib`
Object.assign(current.style, partialStyle);
}
break;
default:
// apply all other incoming prop updates as attributes on the node
// same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39
// @ts-expect-error "DOM" is not part of `compilerOptions.lib`
current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue);
break;
}
}
}
}
_remeasureMetricsOnActivation: () => void; _remeasureMetricsOnActivation: () => void;
touchableHandleStartShouldSetResponder?: ( touchableHandleStartShouldSetResponder?: (
e: GestureResponderEvent, e: GestureResponderEvent,
@@ -258,30 +316,35 @@ export class WebShape<
this._remeasureMetricsOnActivation = remeasure.bind(this); this._remeasureMetricsOnActivation = remeasure.bind(this);
} }
render(): JSX.Element {
if (!this.tag) {
throw new Error(
'When extending `WebShape` you need to overwrite either `tag` or `render`!',
);
}
this.lastMergedProps = {};
return createElement(
this.tag,
prepare(this, this.prepareProps(this.props)),
);
}
} }
export class Circle extends WebShape { export class Circle extends WebShape {
render(): JSX.Element { tag = 'circle' as const;
return createElement('circle', prepare(this));
}
} }
export class ClipPath extends WebShape { export class ClipPath extends WebShape {
render(): JSX.Element { tag = 'clipPath' as const;
return createElement('clipPath', prepare(this));
}
} }
export class Defs extends WebShape { export class Defs extends WebShape {
render(): JSX.Element { tag = 'defs' as const;
return createElement('defs', prepare(this));
}
} }
export class Ellipse extends WebShape { export class Ellipse extends WebShape {
render(): JSX.Element { tag = 'ellipse' as const;
return createElement('ellipse', prepare(this));
}
} }
export class G extends WebShape< export class G extends WebShape<
@@ -291,129 +354,98 @@ export class G extends WebShape<
translate?: string; translate?: string;
} }
> { > {
render(): JSX.Element { tag = 'g' as const;
const { x, y, ...rest } = this.props; prepareProps(
props: BaseProps & {
x?: NumberProp;
y?: NumberProp;
translate?: string;
},
) {
const { x, y, ...rest } = props;
if ((x || y) && !rest.translate) { if ((x || y) && !rest.translate) {
rest.translate = `${x || 0}, ${y || 0}`; rest.translate = `${x || 0}, ${y || 0}`;
} }
return createElement('g', prepare(this, rest)); return rest;
} }
} }
export class Image extends WebShape { export class Image extends WebShape {
render(): JSX.Element { tag = 'image' as const;
return createElement('image', prepare(this));
}
} }
export class Line extends WebShape { export class Line extends WebShape {
render(): JSX.Element { tag = 'line' as const;
return createElement('line', prepare(this));
}
} }
export class LinearGradient extends WebShape { export class LinearGradient extends WebShape {
render(): JSX.Element { tag = 'linearGradient' as const;
return createElement('linearGradient', prepare(this));
}
} }
export class Path extends WebShape { export class Path extends WebShape {
render(): JSX.Element { tag = 'path' as const;
return createElement('path', prepare(this));
}
} }
export class Polygon extends WebShape { export class Polygon extends WebShape {
render(): JSX.Element { tag = 'polygon' as const;
return createElement('polygon', prepare(this));
}
} }
export class Polyline extends WebShape { export class Polyline extends WebShape {
render(): JSX.Element { tag = 'polyline' as const;
return createElement('polyline', prepare(this));
}
} }
export class RadialGradient extends WebShape { export class RadialGradient extends WebShape {
render(): JSX.Element { tag = 'radialGradient' as const;
return createElement('radialGradient', prepare(this));
}
} }
export class Rect extends WebShape { export class Rect extends WebShape {
render(): JSX.Element { tag = 'rect' as const;
return createElement('rect', prepare(this));
}
} }
export class Stop extends WebShape { export class Stop extends WebShape {
render(): JSX.Element { tag = 'stop' as const;
return createElement('stop', prepare(this));
}
} }
export class Svg extends WebShape { export class Svg extends WebShape {
render(): JSX.Element { tag = 'svg' as const;
return createElement('svg', prepare(this));
}
} }
export class Symbol extends WebShape { export class Symbol extends WebShape {
render(): JSX.Element { tag = 'symbol' as const;
return createElement('symbol', prepare(this));
}
} }
export class Text extends WebShape { export class Text extends WebShape {
render(): JSX.Element { tag = 'text' as const;
return createElement('text', prepare(this));
}
} }
export class TSpan extends WebShape { export class TSpan extends WebShape {
render(): JSX.Element { tag = 'tspan' as const;
return createElement('tspan', prepare(this));
}
} }
export class TextPath extends WebShape { export class TextPath extends WebShape {
render(): JSX.Element { tag = 'textPath' as const;
return createElement('textPath', prepare(this));
}
} }
export class Use extends WebShape { export class Use extends WebShape {
render(): JSX.Element { tag = 'use' as const;
return createElement('use', prepare(this));
}
} }
export class Mask extends WebShape { export class Mask extends WebShape {
render(): JSX.Element { tag = 'mask' as const;
return createElement('mask', prepare(this));
}
} }
export class ForeignObject extends WebShape { export class ForeignObject extends WebShape {
render(): JSX.Element { tag = 'foreignObject' as const;
return createElement('foreignObject', prepare(this));
}
} }
export class Marker extends WebShape { export class Marker extends WebShape {
render(): JSX.Element { tag = 'marker' as const;
return createElement('marker', prepare(this));
}
} }
export class Pattern extends WebShape { export class Pattern extends WebShape {
render(): JSX.Element { tag = 'pattern' as const;
return createElement('pattern', prepare(this));
}
} }
export default Svg; export default Svg;