diff --git a/example/App.js b/example/App.js index 38fb947..2107c21 100644 --- a/example/App.js +++ b/example/App.js @@ -4,6 +4,7 @@ import {StyleSheet, ScrollView, SafeAreaView} from 'react-native'; import Heart from './components/Heart'; import CustomShape from './components/CustomShape'; import CustomText from './components/CustomText'; +import AnimatedCircles from './components/AnimatedCircles'; export default function App() { return ( @@ -12,6 +13,7 @@ export default function App() { + ); diff --git a/example/components/AnimatedCircles.js b/example/components/AnimatedCircles.js new file mode 100644 index 0000000..31a77c0 --- /dev/null +++ b/example/components/AnimatedCircles.js @@ -0,0 +1,75 @@ +// @flow +import React from 'react'; +import {Animated, Easing, StyleSheet, Dimensions} from 'react-native'; +import {Surface, Shape, Group} from '@react-native-community/art'; + +/* +An example of Animated Shapes + +Animated uses 'setNativeProps' on the AnimatedShape, preventing rerendering +for each step in the animation +*/ + +const CIRCLE = 'M 25, 50 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0'; + +export default function AnimatedCircles() { + const surfaceWidth = Dimensions.get('window').width; + + const AnimatedShape = Animated.createAnimatedComponent(Shape); + + const [animatedOpacity] = React.useState(new Animated.Value(1)); + + const blink = React.useCallback( + toValue => + Animated.timing(animatedOpacity, { + duration: 900, + easing: Easing.linear, + toValue, + }).start(() => blink(toValue === 0 ? 1 : 0)), + [animatedOpacity], + ); + + React.useEffect(() => { + blink(0); + }, [blink]); + + return ( + + + + + + + + + + + + ); +} +const styles = StyleSheet.create({ + surface: { + backgroundColor: '#000', + }, +}); diff --git a/lib/Group.js b/lib/Group.js index a4bb485..35cf71b 100644 --- a/lib/Group.js +++ b/lib/Group.js @@ -11,33 +11,46 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import invariant from 'invariant'; import {NativeGroup} from './nativeComponents'; -import {extractOpacity, extractTransform, extractShadow} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type {OpacityProps, TransformProps, ShadowProps} from './types'; -type GroupProps = OpacityProps & +export type GroupProps = OpacityProps & ShadowProps & TransformProps & { children: React.Node, }; +const nativePropList = ['opacity', 'transform']; + export default class Group extends React.Component { static contextTypes = { isInSurface: PropTypes.bool.isRequired, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: GroupProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { invariant( this.context.isInSurface, 'ART: must be a child of a ', ); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); - return ( - - {this.props.children} - - ); + return {this.props.children}; } } diff --git a/lib/Shape.js b/lib/Shape.js index a7c3dc1..fb5faca 100644 --- a/lib/Shape.js +++ b/lib/Shape.js @@ -10,16 +10,7 @@ import * as React from 'react'; import {NativeShape} from './nativeComponents'; import Path from './ARTSerializablePath'; -import { - extractTransform, - extractShadow, - extractOpacity, - childrenAsString, - extractColor, - extractStrokeJoin, - extractStrokeCap, - extractBrush, -} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type { TransformProps, ShadowProps, @@ -45,6 +36,18 @@ export type ShapeProps = TransformProps & height: number, }; +const nativePropList = [ + 'd', + 'fill', + 'opacity', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', +]; + export default class Shape extends React.Component { static defaultProps = { strokeWidth: 1, @@ -52,23 +55,30 @@ export default class Shape extends React.Component { height: 0, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: ShapeProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { - const props = this.props; - const path = props.d || childrenAsString(props.children); - const d = (path instanceof Path ? path : new Path(path)).toJSON(); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); return ( (this._rootComponent = component)} + {...nativeProps} /> ); } diff --git a/lib/Text.js b/lib/Text.js index 5615a71..35d4c0a 100644 --- a/lib/Text.js +++ b/lib/Text.js @@ -10,18 +10,7 @@ import * as React from 'react'; import Path from './ARTSerializablePath'; import {NativeText} from './nativeComponents'; -import { - extractBrush, - extractOpacity, - extractColor, - extractStrokeCap, - extractStrokeJoin, - extractTransform, - extractShadow, - extractAlignment, - childrenAsString, - extractFontAndLines, -} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type { TransformProps, ShadowProps, @@ -50,6 +39,20 @@ export type TextProps = TransformProps & path?: string | Path, }; +const nativePropList = [ + 'alignment', + 'frame', + 'fill', + 'opacity', + 'path', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', +]; + export default class Text extends React.Component { static defaultProps = { strokeWidth: 1, @@ -57,30 +60,30 @@ export default class Text extends React.Component { height: 0, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: TextProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { - const props = this.props; - const path = props.path; - const textPath = path - ? (path instanceof Path ? path : new Path(path)).toJSON() - : null; - const textFrame = extractFontAndLines( - props.font, - childrenAsString(props.children), - ); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); + return ( (this._rootComponent = component)} + {...nativeProps} /> ); } diff --git a/lib/__tests__/helpers.test.js b/lib/__tests__/helpers.test.js index f787a73..49e520e 100644 --- a/lib/__tests__/helpers.test.js +++ b/lib/__tests__/helpers.test.js @@ -6,6 +6,7 @@ import { extractStrokeJoin, extractStrokeCap, extractAlignment, + translatePropsToNativeProps, } from '../helpers'; describe('testing childrenAsString function', () => { @@ -78,3 +79,72 @@ describe('testing extractAlignment', () => { expect(extractAlignment()).toBe(LEFT); }); }); + +describe('testing translatePropsToNativeProps', () => { + it('returns correct data for each prop', () => { + // asserting the values in the returned object will fail if any of our helpers + // are changed, but this is required to ensure the correct arguments are passed to + // each of them at the right key: + const props = { + alignment: 'center', + d: 'M 10,30 A 20,20 z', + fill: '#D9003A', + font: 'trebuchet', + height: 100, + opacity: 0.5, + stroke: '#2DCD71', + strokeCap: 'butt', + strokeDash: 2, + strokeJoin: 'miter', + strokeWidth: 3, + scale: 1.1, + width: 200, + }; + const nativeProps1 = translatePropsToNativeProps(props, [ + 'alignment', + 'd', + 'fill', + 'frame', + 'opacity', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', + ]); + expect(nativeProps1).toEqual({ + alignment: 2, + d: [0, 10, 30], + fill: [0, 0.8509803921568627, 0, 0.22745098039215686, 1], + frame: { + font: { + fontFamily: 'trebuchet', + fontSize: 12, + fontStyle: 'normal', + fontWeight: 'normal', + }, + lines: [''], + }, + opacity: 0.5, + stroke: '#2dcd71', + strokeCap: 0, + strokeDash: 2, + strokeJoin: 0, + strokeWidth: 3, + transform: [1.1, 0, 0, 1.1, 0, 0], + }); + + // test that we can select only a subset of nativeProps: + const nativeProps2 = translatePropsToNativeProps(props, [ + 'fill', + 'opacity', + 'stroke', + ]); + expect(nativeProps2).toEqual({ + fill: [0, 0.8509803921568627, 0, 0.22745098039215686, 1], + opacity: 0.5, + stroke: '#2dcd71', + }); + }); +}); diff --git a/lib/helpers.js b/lib/helpers.js index 4b30788..45a56c9 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -23,6 +23,10 @@ import type { TransformProps, ShadowProps, } from './types'; +import Path from './ARTSerializablePath'; +import type {ShapeProps} from './Shape'; +import type {GroupProps} from './Group'; +import type {TextProps} from './Text'; export function childrenAsString(children?: string | Array) { if (!children) { @@ -355,3 +359,56 @@ export function insertDoubleColorStopsIntoArray( lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false); insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true); } + +// a lookup of the methods we use to translate props to native props +export const propsToNativePropsTranslations = { + alignment: ({alignment}: {alignment: Alignment}) => + extractAlignment(alignment), + d: ({children, d}: {children?: string | Array, d: string | Path}) => { + const path = d || childrenAsString(children); + return (path instanceof Path ? path : new Path(path)).toJSON(); + }, + fill: ({ + fill, + height, + width, + }: { + fill?: Brush | string, + height: number, + width: number, + }) => extractBrush(fill, {height, width}), + frame: ({ + children, + font, + }: { + children?: string | Array, + font?: string | Font, + }) => extractFontAndLines(font, childrenAsString(children)), + opacity: extractOpacity, + shadow: extractShadow, + stroke: ({stroke}: {stroke: ColorType}) => extractColor(stroke), + strokeCap: ({strokeCap}: {strokeCap: StrokeCap}) => + extractStrokeCap(strokeCap), + strokeDash: ({strokeDash}: {strokeDash: Array}) => strokeDash || null, + strokeJoin: ({strokeJoin}: {strokeJoin: StrokeJoin}) => + extractStrokeJoin(strokeJoin), + strokeWidth: ({strokeWidth}: {strokeWidth: number}) => strokeWidth, + transform: extractTransform, + path: ({path}: {path: string | Path}) => + path ? (path instanceof Path ? path : new Path(path)).toJSON() : null, +}; + +// take props, and an array of the required nativeProps, and return an object with +// values for those nativeProps +export function translatePropsToNativeProps( + props: GroupProps | ShapeProps | TextProps, + requiredProps: Array, +) { + return requiredProps.reduce( + (nativeProps, prop) => ({ + ...nativeProps, + [prop]: propsToNativePropsTranslations[prop](props), + }), + {}, + ); +}