Repository: 73R3WY/react-native-svg-animations Branch: master Commit: 7779a7613d7a Files: 20 Total size: 45.2 KB Directory structure: gitextract_nvt6lyug/ ├── .gitignore ├── LICENSE ├── README.md ├── components/ │ ├── AnimatedPath/ │ │ └── index.tsx │ ├── AnimatedSVG/ │ │ └── index.tsx │ ├── AnimatedSVGPath/ │ │ └── index.tsx │ └── AnimatedSVGPaths/ │ └── index.tsx ├── examples/ │ ├── HiSVG/ │ │ ├── d.js │ │ └── index.js │ └── IngenuityPreloaderSVG/ │ ├── d.js │ └── index.js ├── index.js ├── package.json └── utils/ ├── AnimatedListener/ │ └── index.js ├── AnimatedSVG/ │ └── index.js ├── AnimatedSVGBrush/ │ └── index.js ├── AnimatedSVGPropString/ │ └── index.js ├── AnimatedSVGState/ │ └── index.js ├── AnimatedSVGTransform/ │ └── index.js └── Matrix2D/ └── index.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .vscode/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Jeremy Patrick Pacabis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # react-native-svg-animations [![npm version](https://badge.fury.io/js/react-native-svg-animations.svg)](https://badge.fury.io/js/react-native-svg-animations) > SVG Animations wrapper for react-native. Based on examples from this project: https://github.com/ethantran/react-native-examples ## Dependencies - `svg-path-properties` - `react-native-svg` ## Installation ``` npm i react-native-svg-animations --save ``` or ``` yarn add react-native-svg-animations ``` ## Demo
AnimatedSVGPath AnimatedSVGPaths
## Usage This package contains wrapper components for displaying animated SVG in react-native, currently, this contains the following: - AnimatedSVGPath - AnimatedSVGPaths ###### AnimatedSVGPath Component to display a single animated SVG Path. See Hi example for the complete implementation. ```javascript import { AnimatedSVGPath } from "react-native-svg-animations"; ``` ... ```javascript ``` where the properties are: - `d` - the SVG Path to be animated. (required) - `strokeColor` - the color of the path stroke. (defaults to black) - `strokeWidth` - the thickness of the path stroke. (defaults to 1) - `strokeLinecap` - the shape to be used at the end of open subpaths when they are stroked. (defaults to butt) - `strokeDashArray` - the number and length of strokes. (defaults to total length obtained from properties of d) - `height` - the height of the base SVG. (defaults to screen viewport height) - `width` - the width of the base SVG. (defaults to screen viewport width) - `scale` - the scale of the output SVG based on the width and height of the base SVG. (defaults to 1.0 or 100%) - `delay` - time in `ms` before starting animation. (defaults to 1000ms or 1s) - `pause` - time in `ms` to pause at the end of the animation or at the half point of a rewound animation. (defaults 0) - `duration` - time in `ms` to complete the path drawing from starting point to ending point. (defaults to 1000ms or 1s) - `fill` - the color fill of the closed path. (defaults to none) - `loop` - whether the animation loops infinitely. (defaults to true) - `reverse` - Begins drawn and fades as you go . (defaults to false) - `rewind` - the path is rewound when it was complete. (defaults to false) ###### AnimatedSVGPaths Component to display a multiple animated SVG Paths. See Ingenuity preloader example for the complete implementation. ```javascript import { AnimatedSVGPaths } from "react-native-svg-animations"; ``` ... ```javascript ``` where the properties are: - `strokeColor` - the color of the path stroke. (defaults to black) - `strokeWidth` - the thickness of the path stroke. (defaults to 1) - `height` - the height of the base SVG. (defaults to screen viewport height) - `width` - the width of the base SVG. (defaults to screen viewport width) - `scale` - the scale of the output SVG based on the width and height of the base SVG. (defaults to 1.0 or 100%) - `delay` - time in `ms` before starting animation. (defaults to 1000ms or 1s) - `pause` - time in `ms` to pause at the end of the animation or at the half point of a rewound animation. (defaults 0) - `duration` - time in `ms` to complete the path drawing from starting point to ending point. (defaults to 1000ms or 1s) - `fill` - the color fill of the closed path. (defaults to none) - `loop` - whether the animation loops infinitely. (defaults to true) - `reverse` - Begins drawn and fades as you go . (defaults to false) - `rewind` - the path is rewound when it was complete. (defaults to false) - `sequential` - paths start to animate sequentially with the delay between them specified in the `delay` prop, and wait for the last one to finish before a new animation begins. (defaults to false) - `ds` - the SVG Paths to be animated, must be an array; either this or `customSvgProps` is required. - `customSvgProps` - an array of objects to define path properties; other properties will be overwritten by the values defined in these objects; either this or `ds` is required. ```javascript ``` ## TODO - Other animated SVG objects. ## Contributing 1. Fork it! 2. Create your feature branch: `git checkout -b my-new-feature` 3. Commit your changes: `git commit -am 'Add some feature'` 4. Push to the branch: `git push origin my-new-feature` 5. Submit a pull request :D ## Contributors Special thanks to these contributors:
Mr-Bhardwa7
Mr-Bhardwa7
dimofte
dimofte
jvt
jvt
b8ne
b8ne
hovlev
hovlev
qvick1pro
qvick1pro
TristanTouchain
TristanTouchain
nitzanbener
nitzanbener
muh-hamada
muh-hamada
benomatis
benomatis
## License MIT ================================================ FILE: components/AnimatedPath/index.tsx ================================================ import React, { useEffect, useState } from 'react' import { Animated, Easing } from 'react-native' import { svgPathProperties } from 'svg-path-properties' import { PathProps } from 'react-native-svg' import Path from '../AnimatedSVG' export interface AnimatedSvgPathProps { d: string strokeColor?: PathProps[`stroke`] strokeWidth?: PathProps[`strokeWidth`] strokeLinecap?: PathProps[`strokeLinecap`] easing?: Animated.TimingAnimationConfig[`easing`] fill?: PathProps[`fill`] duration?: number delay?: number scale?: number loop?: boolean transform?: PathProps[`transform`] reverse?: boolean rewind?: boolean pause?: number } const AnimatedSvgPath: React.FC = ({ d, strokeColor = 'black', strokeWidth = 1, strokeLinecap = 'butt', easing = Easing.inOut(Easing.ease), duration = 1000, delay = 1000, fill = 'none', scale = 1, loop = true, transform = '', reverse = false, rewind = false, pause = 0, }) => { let loopCount = 0 const properties = new svgPathProperties(d) const length = properties.getTotalLength() const strokeDashoffset = new Animated.Value(!reverse ? length : 0) const animate = () => { strokeDashoffset.setValue(!reverse ? length : 0) const animationsSequence = [].concat( [ Animated.delay(delay * (loopCount && rewind ? 2 : 1)), Animated.timing(strokeDashoffset, { toValue: !reverse ? 0 : length, duration: duration, useNativeDriver: true, easing, }), ], pause ? [Animated.delay(pause)] : [], rewind ? [ Animated.timing(strokeDashoffset, { toValue: !reverse ? length : 0, duration: duration, useNativeDriver: true, easing, }), ] : [] ) Animated.sequence(animationsSequence).start(() => { if (loop) { animate() } }) loopCount = 1 } useEffect(() => { animate() }, []) return ( ) } export default AnimatedSvgPath ================================================ FILE: components/AnimatedSVG/index.tsx ================================================ import React, { Component, Ref } from 'react' import { Path, PathProps } from 'react-native-svg' import AnimatedSvg from '../../utils/AnimatedSVG' interface AnimatedSvgPathBaseProps extends PathProps { ref?: Ref } class AnimatedSvgPathBase extends Component { #_component: Path | null = null setNativeProps = (props: AnimatedSvgPathBaseProps = {}) => { this.#_component && this.#_component.setNativeProps(props) } render() { return ( (this.#_component = component)} {...this.props} /> ) } } const AnimatedSvgPath = AnimatedSvg(AnimatedSvgPathBase) export default AnimatedSvgPath ================================================ FILE: components/AnimatedSVGPath/index.tsx ================================================ import React, { useEffect, useState } from 'react' import Svg, { PathProps } from 'react-native-svg' import { Animated, Dimensions, Easing } from 'react-native' import { svgPathProperties } from 'svg-path-properties' import Path from '../AnimatedSVG' const { height: windowHeight, width: windowWidth, } = Dimensions.get('window') export interface AnimatedSVGPathProps { d: string strokeColor?: PathProps[`stroke`] strokeWidth?: PathProps[`strokeWidth`] strokeLinecap?: PathProps[`strokeLinecap`] easing?: Animated.TimingAnimationConfig[`easing`] fill?: PathProps[`fill`] duration?: number height?: number delay?: number width?: number scale?: number loop?: boolean transform?: PathProps[`transform`] reverse?: boolean rewind?: boolean strokeDasharray?: string[] pause?: number } const AnimatedSVGPath: React.FC = ({ d, strokeColor = 'black', strokeWidth = 1, strokeLinecap = 'butt', easing = Easing.inOut(Easing.ease), duration = 1000, delay = 1000, fill = 'none', scale = 1, loop = true, transform = '', reverse = false, rewind = false, height = windowHeight, width = windowWidth, strokeDasharray, pause = 0, }) => { let loopCount = 0 const properties = new svgPathProperties(d) const length = properties.getTotalLength() const strokeDashoffset = new Animated.Value(!reverse ? length : 0) const animate = () => { strokeDashoffset.setValue(!reverse ? length : 0) const animationsSequence = [].concat( [ Animated.delay(delay * (loopCount ? 2 : 1)), Animated.timing(strokeDashoffset, { toValue: !reverse ? 0 : length, duration: duration, useNativeDriver: true, easing: typeof easing === 'function' ? easing : Easing[easing], }), ], pause ? [Animated.delay(pause)] : [], rewind ? [ Animated.timing(strokeDashoffset, { toValue: !reverse ? length : 0, duration: duration, useNativeDriver: true, easing: typeof easing === 'function' ? easing : Easing[easing], }), ] : [] ) Animated.sequence(animationsSequence).start(() => { if (loop) { animate() } }) loopCount = 1 } useEffect(() => { animate() }, []) return ( ) } export default AnimatedSVGPath ================================================ FILE: components/AnimatedSVGPaths/index.tsx ================================================ import React from 'react' import Svg, { PathProps } from 'react-native-svg' import { Dimensions } from 'react-native' import Path from '../AnimatedPath' import { AnimatedSVGPathProps } from '../AnimatedSVGPath' const { height: windowHeight, width: windowWidth, } = Dimensions.get('window') export type CustomSVGPathProps = AnimatedSVGPathProps export interface AnimatedSvgPathsProps { ds?: string[], customSVGProps?: CustomSVGPathProps[] strokeColor?: PathProps[`stroke`] strokeWidth?: PathProps[`strokeWidth`] fill?: PathProps[`fill`] duration?: number height?: number delay?: number width?: number scale?: number loop?: boolean rewind?: boolean pause?: number sequential?: boolean } const AnimatedSVGPaths: React.FC = ({ ds = [], customSVGProps = [], strokeColor = 'black', strokeWidth = 1, duration = 1000, delay = 1000, fill = 'none', scale = 1, loop = true, rewind = false, pause = 0, sequential = false, height = windowHeight, width = windowWidth, }) => { const svgArray = ds.length > 0 ? ds : customSVGProps.length > 0 ? customSVGProps : [] const svgPaths = svgArray.map((d: string | CustomSVGPathProps, index: number) => { let pathProps: AnimatedSVGPathProps & { key: number } = { key: index, strokeWidth, strokeColor, duration, delay: sequential ? index * delay : delay, scale, fill, loop, rewind, d: '', height, width, } if (typeof d === 'string') { pathProps.d = d } else { Object.assign(pathProps, d) } if (sequential) { pathProps.pause = (svgArray.length - 1 - index) * delay * (rewind ? 2 : 1) } if (pause) { pathProps.pause = pause } return ( ) }) return ( {svgPaths} ) } export default AnimatedSVGPaths ================================================ FILE: examples/HiSVG/d.js ================================================ export default 'M366.2,204.2c-9.8,0-15-5.6-15-15.1V77.2h-85v28h19.5c9.8,0,8.5,2.1,8.5,11.6v72.4c0,9.5,0.5,15.1-9.3,15.1H277h-20.7c-8.5,0-14.2-4.1-14.2-12.9V52.4c0-8.5,5.7-12.3,14.2-12.3h18.8v-28h-127v28h18.1c8.5,0,9.9,2.1,9.9,8.9v56.1h-75V53.4c0-11.5,8.6-13.3,17-13.3h11v-28H2.2v28h26c8.5,0,12,2.1,12,7.9v142.2c0,8.5-3.6,13.9-12,13.9h-21v33h122v-33h-11c-8.5,0-17-4.1-17-12.2v-57.8h75v58.4c0,9.1-1.4,11.6-9.9,11.6h-18.1v33h122.9h5.9h102.2v-33H366.2z'; ================================================ FILE: examples/HiSVG/index.js ================================================ import React, { Component, } from 'react'; import { View, } from 'react-native'; import AnimatedSVGPath from '../../components/AnimatedSVGPath'; import d from './d'; export default class HiSVG extends Component { render() { return ( ); } } ================================================ FILE: examples/IngenuityPreloaderSVG/d.js ================================================ export default preloaderLines = [ 'M498.465,1L1,288.188', 'M600.984,288.188L103.53,1', 'M462.988,601V1', 'M107.176,601l493.651-285', 'M1,315.91L494.822,601', 'M139,1v600', 'M1,322.827L558.475,1', 'M43.537,1L601,322.844', 'M432.986,1v600l0,0', 'M600.828,281.357L600.828,281.357L47.172,601', 'M554.82,601L1,281.268', 'M169,601V1', 'M300.581,288.648c-33.954,0-61.577-27.622-61.577-61.574c0-33.952,27.624-61.573,61.577-61.573c33.952,0,61.573,27.622,61.573,61.573C362.154,261.026,334.533,288.648,300.581,288.648z', 'M300.582,318.592c-50.466,0-91.522-41.055-91.522-91.518c0-50.463,41.058-91.517,91.522-91.517c50.462,0,91.517,41.055,91.517,91.517C392.099,277.537,351.044,318.592,300.582,318.592z', 'M299.774,441.807c-33.953,0-61.576-27.625-61.576-61.58c0-33.953,27.623-61.578,61.576-61.578c33.954,0,61.575,27.625,61.575,61.578C361.351,414.182,333.728,441.807,299.774,441.807z', 'M299.774,471.807c-50.494,0-91.574-41.083-91.574-91.58c0-50.496,41.08-91.578,91.574-91.578c50.495,0,91.571,41.082,91.571,91.578C391.349,430.724,350.269,471.807,299.774,471.807z', 'M1,303.598h599.828', 'M303,1v601', 'M1,1h599.828v600H1V1z' ]; ================================================ FILE: examples/IngenuityPreloaderSVG/index.js ================================================ import React, { Component } from 'react'; import { View } from 'react-native'; import ds from './d'; import AnimatedSVGPaths from '../../components/AnimatedSVGPaths'; class IngenuityPreloaderSVG extends Component { render() { return ( ); } } export default IngenuityPreloaderSVG; ================================================ FILE: index.js ================================================ import AnimatedSVGPath from './components/AnimatedSVGPath'; import AnimatedSVGPaths from './components/AnimatedSVGPaths'; /* Export ==================================================================== */ module.exports = { AnimatedSVGPath: AnimatedSVGPath, AnimatedSVGPaths: AnimatedSVGPaths, } ================================================ FILE: package.json ================================================ { "name": "react-native-svg-animations", "version": "1.0.0", "description": "SVG Animations wrapper for react-native.", "main": "index.js", "directories": { "example": "examples" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git@github.com:73R3WY/react-native-svg-animations.git" }, "keywords": [ "react-native", "svg", "animations" ], "author": "Jeremy Patrick Pacabis (https://ingenuity.ph)", "license": "ISC", "bugs": { "url": "https://github.com/73R3WY/react-native-svg-animations/issues" }, "homepage": "https://github.com/73R3WY/react-native-svg-animations", "dependencies": { "color": "^4.2.3", "react-native-svg": "^14.1.0", "svg-path-properties": "^1.0.3" }, "devDependencies": { "@babel/core": "^7.20.0" } } ================================================ FILE: utils/AnimatedListener/index.js ================================================ // @flow import { Animated } from 'react-native'; type ResultArray = { values: any[], listeners: any[] }; type ResultObject = { values: Object, listeners: Object }; type ResultAnimated = { values: number | string, listeners: string }; type ResultOther = { values: any }; type RecResult = ResultArray | ResultObject; type Result = RecResult | ResultAnimated | ResultOther; export type AnimatedListener = Result; type AnimatedValue = typeof Animated.Value | typeof Animated.Interpolation; /** * Iterates object keys to find Animated objects to add listeners to and returns an object of values and listeners. The values field will be shaped identically to the passed in object with the Animated objects replaced with the value from __getValue at the time it was called. */ function recursiveListen( object, accumulator: ResultArray | ResultObject, accIndex: number, cb: Function ): RecResult { if (Array.isArray(object)) { let initialValue = new AnimatedTracker([], []); return object.reduce( (previousValue, currentValue, currentIndex, array) => { const result = recursiveListen( currentValue, previousValue.values, currentIndex, cb ); previousValue.values[currentIndex] = result.values; previousValue.listeners[currentIndex] = result.listeners; return previousValue; }, initialValue ); } if ( object instanceof Animated.Value || object instanceof Animated.Interpolation ) { const id = addListenerForAnimated(object, accumulator, accIndex, cb); return new AnimatedTracker( object.__getValue(), new ListenerTracker(id, object) ); } if (typeof object === 'object' && object !== null) { let initialValue = new AnimatedTracker({}, {}); return Object.keys(object).reduce((previousValue, currentValue) => { const value = object[currentValue]; const result = recursiveListen( value, previousValue.values, currentValue, cb ); previousValue.values[currentValue] = result.values; previousValue.listeners[currentValue] = result.listeners; return previousValue; }, initialValue); } return new AnimatedTracker(object); } function addListenerForAnimated( animatedValue: AnimatedValue, accumulator: ResultArray | ResultObject, accIndex: number, cb: Function ) { const addListener = animatedValue._parent ? animatedValue._parent.addListener.bind(animatedValue._parent) : animatedValue.addListener.bind(animatedValue); const interpolator = animatedValue._interpolation; let callback = e => e; if (interpolator) { callback = _value => interpolator(_value); } let prevCallback = callback; callback = e => { const value = prevCallback(e.value); accumulator[accIndex] = value; cb(); }; return addListener(callback); } export function listen(object: Object | any[], cb: Function): Result { return recursiveListen(object, null, null, cb); } /** * Iterates object keys to find a ListenerTracker with an Animated object and listener id to remove */ function recursiveRemoveListeners(listeners: Object | any[]) { if (Array.isArray(listeners)) { listeners.forEach((listener, index) => recursiveRemoveListeners(listener) ); } else if (listeners instanceof ListenerTracker) { const id = listeners.id; const object = listeners.object; object._parent ? object._parent.removeListener(id) : object.removeListener(id); } else if (typeof listeners === 'object' && listeners !== null) { Object.keys(listeners).forEach(key => recursiveRemoveListeners(listeners[key]) ); } } export function removeListeners(listeners: Object | any[] | AnimatedTracker) { if (listeners instanceof AnimatedTracker) { recursiveRemoveListeners(listeners.listeners); } else { recursiveRemoveListeners(listeners); } } class ListenerTracker { id: string; object: Animated.Value | Animated.Interpolation; constructor(id: string, object: Animated.Value | Animated.Interpolation) { this.id = id; this.object = object; } } class AnimatedTracker { values: any[] | Object | number | string; listeners: ?(ListenerTracker | ListenerTracker[] | Object); constructor( values?: any[] | Object | string, listeners?: ListenerTracker | ListenerTracker[] | Object ) { this.values = values; this.listeners = listeners; } } ================================================ FILE: utils/AnimatedSVG/index.js ================================================ import { Animated, } from 'react-native'; import AnimatedSVGBrush from '../AnimatedSVGBrush'; import AnimatedSVGPropString from '../AnimatedSVGPropString'; import AnimatedSVGState from '../AnimatedSVGState'; import AnimatedSVGTransform from '../AnimatedSVGTransform'; function AnimatedSVG (Component, { state, propString, keepXY } = {}) { Component = AnimatedSVGState(Component, state); Component = AnimatedSVGBrush(Component); Component = AnimatedSVGPropString(Component, propString); Component = AnimatedSVGTransform(Component, { keepXY }); Component = Animated.createAnimatedComponent(Component); return Component; } export default AnimatedSVG; ================================================ FILE: utils/AnimatedSVGBrush/index.js ================================================ // @flow /** * Problem: Color props such as fill and stroke cannot be animated through setNativeProps. They can be animated through state, but setNativeProps is better * Solution: Extract brush, update propList (does not seem to do anything but to be consistent extractProps I did it anyway) */ import React, { Component } from 'react'; import Color from 'color'; import pick from 'lodash/pick'; import { listen, removeListeners } from '../AnimatedListener'; import type { AnimatedListener } from '../AnimatedListener'; // https://github.com/react-native-community/react-native-svg/blob/master/lib/extract/extractBrush.js const patternReg = /^url\(#(.+?)\)$/; function extractBrush(colorOrBrush) { if (colorOrBrush === 'none' || !colorOrBrush) { return null; } try { let matched = colorOrBrush.match(patternReg); // brush if (matched) { return [1, matched[1]]; //todo: } else { // solid color let [r, g, b, a = 1] = Color(colorOrBrush).rgb().array(); return [0, r / 255, g / 255, b / 255, a]; } } catch (err) { console.warn(`"${colorOrBrush}" is not a valid color or brush`); return null; } } const fillKeys = ['fill', 'fillOpacity', 'fillRule']; const strokeKeys = [ 'stroke', 'strokeWidth', 'strokeOpacity', 'strokeDasharray', 'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit' ]; function getPropList(nextProps, prevProps) { let propList = []; fillKeys.forEach(name => { if (nextProps.hasOwnProperty(name) || prevProps.hasOwnProperty(name)) { propList.push(name); } }); strokeKeys.forEach(name => { if (nextProps.hasOwnProperty(name) || prevProps.hasOwnProperty(name)) { propList.push(name); } }); return propList; } const separator = /\s*,\s*/; function getStrokeDasharray(strokeDasharray) { if (typeof strokeDasharray === 'string') { strokeDasharray = strokeDasharray.split(separator).map(dash => +dash); } if (strokeDasharray && strokeDasharray.length === 1) { strokeDasharray.push(strokeDasharray[0]); } // have to clone array to allow animation with mutable changes return strokeDasharray ? [...strokeDasharray] : strokeDasharray; } function getStrokeDashoffset(nextProps, prevProps) { const strokeDasharray = getStrokeDasharray(nextProps.strokeDasharray) || getStrokeDasharray(prevProps.strokeDasharray); return strokeDasharray ? +nextProps.strokeDashoffset || +prevProps.strokeDashoffset || 0 : null; } const KEYS = ['fill', 'stroke', 'strokeDashoffset']; export default function SvgBrushFix(WrappedComponent) { return class extends Component { strokeDasharray: AnimatedListener; constructor(props) { super(props); this.updateCache(props); this.strokeDasharray = listen(props.strokeDasharray, _ => this.setNativeProps({ updateStrokeDasharray: true }) ); } updateCache(props) { this.prevProps = pick(props, KEYS); } setNativeProps = props => { props.propList = getPropList(props, this.prevProps); if (props.fill) { props.fill = extractBrush(props.fill); } if (props.stroke) { props.stroke = extractBrush(props.stroke); } if (props.updateStrokeDasharray || props.strokeDashoffset) { props.strokeDasharray = getStrokeDasharray( this.strokeDasharray.values ); props.strokeDashoffset = getStrokeDashoffset( props, this.prevProps ); } this._component && this._component.setNativeProps(props); }; componentDidUpdate(prevProps) { this.updateCache(prevProps); if (this.props.strokeDasharray !== prevProps.strokeDasharray) { removeListeners(this.strokeDasharray); this.strokeDasharray = listen(prevProps.strokeDasharray, _ => this.setNativeProps({ updateStrokeDasharray: true })); } } componentWillUnmount() { removeListeners(this.strokeDasharray); } render() { const strokeDasharray = getStrokeDasharray( this.strokeDasharray.values ); return ( (this._component = component)} {...this.props} strokeDasharray={strokeDasharray} /> ); } }; } ================================================ FILE: utils/AnimatedSVGPropString/index.js ================================================ import React, { Component } from 'react'; /** * Problem: Props cannot be animated, too many times you have to do val.toString() in setNativeProps * Solution: Use a higher order component to do that for you */ const KEYS = [ 'strokeWidth', 'strokeOpacity', 'fillOpacity' ]; export default function SvgPropStringFix(WrappedComponent, propKeys = []) { propKeys = [...KEYS, ...propKeys]; return class extends Component { setNativeProps = (props) => { propKeys.reduce((acc, key) => { const val = props[key]; if (val != null) { acc[key] = val.toString(); } return acc; }, props); this._component && this._component.setNativeProps(props); } render() { return ( (this._component = component)} {...this.props} /> ); } }; } ================================================ FILE: utils/AnimatedSVGState/index.js ================================================ import React, { Component } from 'react'; import omit from 'lodash/omit'; /** * Problem: Some props cannot be animated through setNativeProps * Solution: Use state for those and use setNativeProps for the rest */ function createState(props, keys) { return keys.reduce((acc, key) => { const value = props[key]; if (value != null) { acc[key] = value; } return acc; }, {}); } export default function SvgStateFix(WrappedComponent, propToStateKeys = [], { cancelSetNativeProps } = {}) { return class extends Component { state = createState(this.props, propToStateKeys); setNativeProps = (props) => { if (!cancelSetNativeProps) { const nativeProps = omit(props, propToStateKeys); this._component && this._component.setNativeProps(nativeProps); } const newState = createState(props, propToStateKeys); this.setState(newState); } render() { return ( (this._component = component)} {...this.props} {...this.state} /> ); } }; } ================================================ FILE: utils/AnimatedSVGTransform/index.js ================================================ import React, { Component } from 'react'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import Matrix2D from '../Matrix2D'; /** * Problem: Animating transform props is not easy * Solution: Use Animated.ValueXY in universal props, create matrix when ever transform props change, use setNativeProps * BUG: Using Animated.ValueXY does not work for some reason */ // https://github.com/react-native-community/react-native-svg/blob/master/lib/extract/extractTransform.js // modified to deal with nextProps and prevProps situations with both static props on this.props and dynamic props passing through setNativeProps function _universal2axis(universal, axisX, axisY, defaultValue) { let x; let y; if (typeof universal === 'object') { x = universal.x; y = universal.y; } else if (typeof universal === 'number') { x = y = universal; } axisX = +axisX; if (!isNaN(axisX)) { x = axisX; } axisY = +axisY; if (!isNaN(axisY)) { y = axisY; } return [x || defaultValue || 0, y || defaultValue || 0]; } function universal2axis(key, nextProps, prevProps, defaultValue) { let [nextX, nextY] = _universal2axis(nextProps[key], nextProps[key + 'X'], nextProps[key + 'Y']); let [prevX, prevY] = _universal2axis(prevProps[key], prevProps[key + 'X'], prevProps[key + 'Y'], defaultValue); return [nextX || prevX, nextY || prevY]; } function createTransformObject(nextProps, prevProps) { const [originX, originY] = universal2axis('origin', nextProps, prevProps); const [scaleX, scaleY] = universal2axis('scale', nextProps, prevProps, 1); const [skewX, skewY] = universal2axis('skew', nextProps, prevProps); const [translateX, translateY] = universal2axis('translate', nextProps, prevProps); const x = translateX || nextProps.x || prevProps.x || 0; const y = translateY || nextProps.y || prevProps.y || 0; const rotation = +nextProps.rotation || +nextProps.rotate || +prevProps.rotation || +prevProps.rotate || 0; return { rotation, originX, originY, scaleX, scaleY, skewX, skewY, x, y }; } let pooledMatrix = new Matrix2D(); function createTransformMatrix(props, transform) { pooledMatrix.reset(); appendTransform(props); if (transform) { appendTransform(transform); } return pooledMatrix.toArray(); } function appendTransform(transform) { pooledMatrix .appendTransform( transform.x + transform.originX, transform.y + transform.originY, transform.scaleX, transform.scaleY, transform.rotation, transform.skewX, transform.skewY, transform.originX, transform.originY ); } const UNIVERSAL_KEYS = ['origin', 'scale', 'skew', 'translate']; const KEYS = [...UNIVERSAL_KEYS, 'originX', 'originY', 'scaleX', 'scaleY', 'skewX', 'skewY', 'translateX', 'translateY', 'x', 'y', 'rotation', 'rotate']; export default function SvgTransformFix(WrappedComponent, { keepXY } = {}) { return class extends Component { prevProps = pick(this.props, KEYS) setNativeProps = (props) => { // if some transform key exists in props, create a new matrix if (KEYS.some((key, index) => props[key] != null)) { const matrix = createTransformMatrix(createTransformObject(props, this.prevProps)); let x, y; // some components like rect need these still if (keepXY) { x = props.x && props.x.toString(); y = props.y && props.y.toString(); } // remove transform props since they are moved into matrix prop props = omit(props, KEYS); props.matrix = matrix; props.x = x; props.y = y; // cache dynamic prop values since you need them to generate an accurate matrix this.prevProps = Object.assign(this.prevProps, pick(props, KEYS)); } this._component && this._component.setNativeProps(props); } render() { return ( (this._component = component)} {...this.props} /> ); } }; } ================================================ FILE: utils/Matrix2D/index.ts ================================================ /** * based on * https://github.com/CreateJS/EaselJS/blob/631cdffb85eff9413dab43b4676f059b4232d291/src/easeljs/geom/Matrix2D.js */ const DEG_TO_RAD = Math.PI / 180 /** * Represents an affine transformation matrix, and provides tools for constructing and concatenating matrices. * * This matrix can be visualized as: * * [ a c tx * b d ty * 0 0 1 ] * * Note the locations of b and c. * * @class Matrix2D * @param {Number} [a=1] Specifies the a property for the new matrix. * @param {Number} [b=0] Specifies the b property for the new matrix. * @param {Number} [c=0] Specifies the c property for the new matrix. * @param {Number} [d=1] Specifies the d property for the new matrix. * @param {Number} [tx=0] Specifies the tx property for the new matrix. * @param {Number} [ty=0] Specifies the ty property for the new matrix. * @constructor **/ export default class Matrix2D { constructor(a, b, c, d, tx, ty) { this.setTransform(a, b, c, d, tx, ty) // public properties: // assigned in the setValues method. /** * Position (0, 0) in a 3x3 affine transformation matrix. * @property a * @type Number **/ /** * Position (0, 1) in a 3x3 affine transformation matrix. * @property b * @type Number **/ /** * Position (1, 0) in a 3x3 affine transformation matrix. * @property c * @type Number **/ /** * Position (1, 1) in a 3x3 affine transformation matrix. * @property d * @type Number **/ /** * Position (2, 0) in a 3x3 affine transformation matrix. * @property tx * @type Number **/ /** * Position (2, 1) in a 3x3 affine transformation matrix. * @property ty * @type Number **/ } /** * Set current matrix to new absolute matrix. * @method setTransform * @param {Number} [a=1] Specifies the a property for the new matrix. * @param {Number} [b=0] Specifies the b property for the new matrix. * @param {Number} [c=0] Specifies the c property for the new matrix. * @param {Number} [d=1] Specifies the d property for the new matrix. * @param {Number} [tx=0] Specifies the tx property for the new matrix. * @param {Number} [ty=0] Specifies the ty property for the new matrix. * @return {Matrix2D} This instance. Useful for chaining method calls. */ setTransform = function(a, b, c, d, tx, ty) { /*eslint eqeqeq:0*/ this.a = a == null ? 1 : a this.b = b || 0 this.c = c || 0 this.d = b == null ? 1 : d this.tx = tx || 0 this.ty = ty || 0 return this } /** * Reset current matrix to an identity matrix. * @method reset * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ reset = function() { this.a = this.d = 1 this.b = this.c = this.tx = this.ty = 0 return this } /** * Returns an array with current matrix values. * @method toArray * @return {Array} an array with current matrix values. **/ toArray = function() { return [this.a, this.b, this.c, this.d, this.tx, this.ty] } /** * Copies all properties from the specified matrix to this matrix. * @method copy * @param {Matrix2D} matrix The matrix to copy properties from. * @return {Matrix2D} This matrix. Useful for chaining method calls. */ copy = function(matrix) { return this.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty) } /** * Clones current instance and returning a new matrix. * @method clone * @return {Matrix2D} a clone of the Matrix2D instance. **/ clone = function() { return new Matrix2D(this.a, this.b, this.c, this.d, this.tx, this.ty) } /** * Prepends the specified matrix properties to this matrix. * This is the equivalent of multiplying `(specified matrix) * (this matrix)`. * All parameters are required. * @method prepend * @param {Number} a * @param {Number} b * @param {Number} c * @param {Number} d * @param {Number} tx * @param {Number} ty * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ prepend = function(a, b, c, d, tx, ty) { var a1 = this.a var c1 = this.c var tx1 = this.tx this.a = a * a1 + c * this.b this.b = b * a1 + d * this.b this.c = a * c1 + c * this.d this.d = b * c1 + d * this.d this.tx = a * tx1 + c * this.ty + tx this.ty = b * tx1 + d * this.ty + ty return this } /** * Appends the specified matrix properties to this matrix. All parameters are required. * This is the equivalent of multiplying `(this matrix) * (specified matrix)`. * @method append * @param {Number} a * @param {Number} b * @param {Number} c * @param {Number} d * @param {Number} tx * @param {Number} ty * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ append = function(a, b, c, d, tx, ty) { var a1 = this.a var b1 = this.b var c1 = this.c var d1 = this.d if (a !== 1 || b !== 0 || c !== 0 || d !== 1) { this.a = a1 * a + c1 * b this.b = b1 * a + d1 * b this.c = a1 * c + c1 * d this.d = b1 * c + d1 * d } this.tx = a1 * tx + c1 * ty + this.tx this.ty = b1 * tx + d1 * ty + this.ty return this } /** * Generates matrix properties from the specified display object transform properties, and appends them to this matrix. * For example, you can use this to generate a matrix representing the transformations of a display object: * * var mtx = new createjs.Matrix2D() * mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation) * @method appendTransform * @param {Number} x * @param {Number} y * @param {Number} scaleX * @param {Number} scaleY * @param {Number} rotation * @param {Number} skewX * @param {Number} skewY * @param {Number} regX Optional. * @param {Number} regY Optional. * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ appendTransform = function(x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { if (rotation % 360) { var r = rotation * DEG_TO_RAD var cos = Math.cos(r) var sin = Math.sin(r) } else { cos = 1 sin = 0 } if (skewX || skewY) { // TODO: can this be combined into a single append operation? skewX *= DEG_TO_RAD skewY *= DEG_TO_RAD this.append(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y) this.append(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, 0, 0) } else { this.append(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, x, y) } if (regX || regY) { // append the registration offset: this.tx -= regX * this.a + regY * this.c this.ty -= regX * this.b + regY * this.d } return this } /** * Generates matrix properties from the specified display object transform properties, and prepends them to this matrix. * For example, you could calculate the combined transformation for a child object using: * * var o = myDisplayObject * var mtx = new createjs.Matrix2D() * do { * // prepend each parent's transformation in turn: * mtx.prependTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY) * } while (o = o.parent) * * Note that the above example would not account for {{#crossLink "DisplayObject/transformMatrix:property"}}{{/crossLink}} * values. See {{#crossLink "Matrix2D/prependMatrix"}}{{/crossLink}} for an example that does. * @method prependTransform * @param {Number} x * @param {Number} y * @param {Number} scaleX * @param {Number} scaleY * @param {Number} rotation * @param {Number} skewX * @param {Number} skewY * @param {Number} regX Optional. * @param {Number} regY Optional. * @return {Matrix2D} This matrix. Useful for chaining method calls. **/ prependTransform = function(x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { if (rotation % 360) { var r = rotation * DEG_TO_RAD var cos = Math.cos(r) var sin = Math.sin(r) } else { cos = 1 sin = 0 } if (regX || regY) { // prepend the registration offset: this.tx -= regX this.ty -= regY } if (skewX || skewY) { // TODO: can this be combined into a single prepend operation? skewX *= DEG_TO_RAD skewY *= DEG_TO_RAD this.prepend(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, 0, 0) this.prepend(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y) } else { this.prepend(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, x, y) } return this } }