├── .eslintrc.js ├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── index.js ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | eslint: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: npm install 26 | - name: ESLint check 27 | run: npm run lint 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | endOfLine: 'auto', 7 | }; 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wolf Financial 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-svg-path-gradient 2 | 3 | [![Version](https://img.shields.io/npm/v/react-native-svg-path-gradient.svg)](https://www.npmjs.com/package/react-native-svg-path-gradient) [![Downloads](https://img.shields.io/npm/dw/react-native-svg-path-gradient)](https://www.npmjs.com/package/react-native-svg-path-gradient) 4 | 5 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/investingwolf/react-native-svg-path-gradient)](https://github.com/investingwolf/react-native-svg-path-gradient/pulls) [![GitHub issues](https://img.shields.io/github/issues-raw/investingwolf/react-native-svg-path-gradient)](https://github.com/investingwolf/react-native-svg-path-gradient/issues) [![GitHub Repo stars](https://img.shields.io/github/stars/investingwolf/react-native-svg-path-gradient?style=social)](https://github.com/investingwolf/react-native-svg-path-gradient/stargazers) 6 | 7 | Adds a `` component used for creating color gradients along a custom path 8 | 9 | ## Table of Contents 10 | 11 | - [Installation](#installation) 12 | - [Props](#props) 13 | - [Examples](#examples) 14 | - [License](#license) 15 | 16 | ## Installation 17 | 18 | Using npm 19 | 20 | ```sh 21 | npm install react-native-svg-path-gradient 22 | ``` 23 | 24 | Using Yarn 25 | 26 | ```sh 27 | yarn add react-native-svg-path-gradient 28 | ``` 29 | 30 | ## Props 31 | 32 | This component does **NOT** share the same props as `` from `react-native-svg`. Only the props listed below are available: 33 | 34 | | Prop Name | Type | Default Value | 35 | | -------------- | -------- | ------------- | 36 | | d | String | - | 37 | | colors | String[] | - | 38 | | strokeWidth | Number | 1 | 39 | | precision | Number | 8 | 40 | | roundedCorners | Boolean | false | 41 | | percent | Number | 1 | 42 | 43 | ### d 44 | 45 | A string with an svg path. For refrence on how to make a path see the [Path documentation of SVG](https://www.w3.org/TR/SVG/paths.html) or the [react-native-svg path component](https://github.com/react-native-svg/react-native-svg#path) 46 | 47 | ### colors 48 | 49 | An array of color strings. These can be hex (`#FF0000`), rgb (`rgba(255, 0, 0)`), or css color names (`red`). 50 | 51 | **Note:** rgba and hexa are not supported. While they will work the alpha value will be ignored. The color interpolationg algorithm ignores these since they appear different between stroke and fill colors on the `` component of `react-native-svg`. 52 | 53 | ### strokeWidth 54 | 55 | Takes a number value for the stroke width of the path, default is 1. 56 | 57 | ### precision 58 | 59 | Takes a number value for the precision of the path segments. Lower is more accurate. The default value is 8. Reccomended values are between 5 and 20. 60 | 61 | **Note:** Going outside the reccomended range can cause issues with render times or the accuracy of the render. Use at your own risk. 62 | 63 | ### roundedCorners 64 | 65 | Takes a boolean value, default is `false`. If `true` the corners of the path become rounded. Due to being a boolean value it is possible to just write `roundedCorners` as a prop with no value and `react-native` will treat it as true 66 | 67 | **Note:** Rounded Corners effectively doesn't round the corners as much as it extrudes the line caps. This means that your path can become slightly longer when the linecap is added. Also linecaps can be added for "closed" paths which will present an issue so this prop should be avoided 68 | 69 | ### percent 70 | 71 | Takes a number, default is `1`. This draws a percent of the actual path. e.g. `0.5` will draw roughly half of the path. This value can be used to make animations. 72 | 73 | ## Examples 74 | 75 | ### Ribbon 76 | 77 | The following code will produce something like this: 78 | 79 | ![Example code result](https://i.imgur.com/z4pbYKY.png) 80 | 81 | ```javascript 82 | import GradientPath from 'react-native-svg-path-gradient'; 83 | import {Svg} from 'react-native-svg'; 84 | 85 | 86 | 99 | ; 100 | ``` 101 | 102 | ### HorseShoe 103 | 104 | The following code will produce something like this: 105 | 106 | ![Example code result](https://i.imgur.com/CSlMyoR.png) 107 | 108 | ```javascript 109 | import GradientPath from 'react-native-svg-path-gradient'; 110 | import {Svg} from 'react-native-svg'; 111 | 112 | 113 | 121 | ; 122 | ``` 123 | 124 | ### Animated Heart 125 | 126 | **Note:** `useNativeDriver` must be `false`. The performance of this animation can be impacted heavily by the complexity of the path. This library is not designed for animating svgs, we suggest using a library like [Airbnb's Lottie](https://github.com/lottie-react-native/lottie-react-native) 127 | 128 | The following code will produce something like this: 129 | 130 | ![Example code result](https://i.imgur.com/uM8UtLn.gif) 131 | 132 | ```javascript 133 | import {useRef, useEffect} from 'react'; 134 | import {Easing, Animated} from 'react-native'; 135 | import GradientPath from 'react-native-svg-path-gradient'; 136 | import {Svg} from 'react-native-svg'; 137 | const AnimatedGradientPath = Animated.createAnimatedComponent(GradientPath); 138 | 139 | const fillPercent = useRef(new Animated.Value(0)).current; 140 | 141 | useEffect(() => { 142 | const fillAnim = () => { 143 | Animated.timing(fillPercent, { 144 | toValue: 1, 145 | duration: 800, 146 | delay: 0, 147 | easing: Easing.inOut(Easing.cubic), 148 | useNativeDriver: false, 149 | }).start(); 150 | }; 151 | fillAnim(); 152 | }, []); 153 | 154 | 155 | 164 | ; 165 | ``` 166 | 167 | ## License 168 | 169 | MIT 170 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Path, G, Circle} from 'react-native-svg'; 3 | import PropTypes from 'prop-types'; 4 | import {svgPathProperties} from 'svg-path-properties'; 5 | import Color from 'color'; 6 | 7 | export default class GradientPath extends Component { 8 | render() { 9 | const { 10 | d, 11 | colors, 12 | strokeWidth, 13 | precision, 14 | roundedCorners, 15 | percent, 16 | } = this.props; 17 | const path = new svgPathProperties(d); 18 | const pathList = quads(samples(path, precision)); 19 | const gradientArray = interpolateColors(colors, pathList.length); 20 | 21 | const percent_ = Math.max(0, Math.min(percent, 1)); 22 | const croppedPathIndex = Math.round(pathList.length * percent_); 23 | 24 | const PATH_START = path.getPointAtLength(0); 25 | const PATH_END = path.getPointAtLength( 26 | Math.round(path.getTotalLength() * percent_), 27 | ); 28 | 29 | return ( 30 | 31 | {roundedCorners && ( 32 | 33 | 39 | 45 | 46 | )} 47 | {pathList.map((pathSegment, i) => { 48 | if (i < croppedPathIndex) { 49 | return ( 50 | 62 | ); 63 | } 64 | })} 65 | 66 | ); 67 | } 68 | } 69 | 70 | GradientPath.defaultProps = { 71 | strokeWidth: 1, 72 | precision: 8, 73 | roundedCorners: false, 74 | percent: 1, 75 | }; 76 | 77 | GradientPath.propTypes = { 78 | d: PropTypes.string.isRequired, 79 | colors: PropTypes.arrayOf(PropTypes.string).isRequired, 80 | strokeWidth: PropTypes.number, 81 | precision: PropTypes.number, 82 | roundedCorners: PropTypes.bool, 83 | percent: PropTypes.number, 84 | }; 85 | 86 | // color interpolation function 87 | function interpolateColors(colors, colorCount) { 88 | if (colors.length === 0) { 89 | return Array(colorCount).fill('#000000'); 90 | } 91 | if (colors.length === 1) { 92 | return Array(colorCount).fill(colors[0]); 93 | } 94 | const colorArray = []; 95 | 96 | for (let i = 0; i < colors.length - 1; i++) { 97 | const start = Color(colors[i]).object(); 98 | const end = Color(colors[i + 1]).object(); 99 | colorArray.push(Color(start).hex()); 100 | const segmentLength = 101 | i === colors.length - 2 102 | ? colorCount - colorArray.length - 1 103 | : Math.round(colorCount / colors.length); 104 | 105 | const deltaBlend = 1.0 / (segmentLength + 1); 106 | for ( 107 | let j = 0, blend = deltaBlend; 108 | j < segmentLength; 109 | j++, blend += deltaBlend 110 | ) { 111 | const r = end.r * blend + (1 - blend) * start.r; 112 | const g = end.g * blend + (1 - blend) * start.g; 113 | const b = end.b * blend + (1 - blend) * start.b; 114 | 115 | colorArray.push(Color.rgb(r, g, b).hex()); 116 | } 117 | } 118 | 119 | colorArray.push(Color(colors[colors.length - 1]).hex()); 120 | return colorArray; 121 | } 122 | 123 | // Sample the SVG path uniformly with the specified precision. 124 | function samples(path, precision) { 125 | const n = path.getTotalLength(); 126 | const normalizedLengths = [0]; 127 | const dt = precision; 128 | for (let i = dt; i < n; i += dt) { 129 | normalizedLengths.push(i); 130 | } 131 | normalizedLengths.push(n); 132 | return normalizedLengths.map((t) => { 133 | var p = path.getPointAtLength(t), 134 | a = [p.x, p.y]; 135 | a.t = t / n; 136 | return a; 137 | }); 138 | } 139 | 140 | // Compute quads of adjacent points [p0, p1, p2, p3]. 141 | function quads(points) { 142 | return [...Array(points.length - 1).keys()].map(function (i) { 143 | const a = [points[i - 1], points[i], points[i + 1], points[i + 2]]; 144 | a.t = (points[i].t + points[i + 1].t) / 2; 145 | return a; 146 | }); 147 | } 148 | 149 | // Compute stroke outline for segment p12. 150 | function lineJoin(p0, p1, p2, p3, width) { 151 | const u12 = perp(p1, p2); 152 | const r = width / 2; 153 | let a = [p1[0] + u12[0] * r, p1[1] + u12[1] * r]; 154 | let b = [p2[0] + u12[0] * r, p2[1] + u12[1] * r]; 155 | let c = [p2[0] - u12[0] * r, p2[1] - u12[1] * r]; 156 | let d = [p1[0] - u12[0] * r, p1[1] - u12[1] * r]; 157 | 158 | if (p0) { 159 | // clip ad and dc using average of u01 and u12 160 | var u01 = perp(p0, p1), 161 | e = [p1[0] + u01[0] + u12[0], p1[1] + u01[1] + u12[1]]; 162 | a = lineIntersect(p1, e, a, b); 163 | d = lineIntersect(p1, e, d, c); 164 | } 165 | 166 | if (p3) { 167 | // clip ab and dc using average of u12 and u23 168 | var u23 = perp(p2, p3), 169 | e = [p2[0] + u23[0] + u12[0], p2[1] + u23[1] + u12[1]]; 170 | b = lineIntersect(p2, e, a, b); 171 | c = lineIntersect(p2, e, d, c); 172 | } 173 | 174 | return 'M' + a + 'L' + b + ' ' + c + ' ' + d + 'Z'; 175 | } 176 | 177 | // Compute intersection of two infinite lines ab and cd. 178 | function lineIntersect(a, b, c, d) { 179 | const x1 = c[0]; 180 | const x3 = a[0]; 181 | const x21 = d[0] - x1; 182 | const x43 = b[0] - x3; 183 | const y1 = c[1]; 184 | const y3 = a[1]; 185 | const y21 = d[1] - y1; 186 | const y43 = b[1] - y3; 187 | const ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); 188 | return [x1 + ua * x21, y1 + ua * y21]; 189 | } 190 | 191 | // Compute unit vector perpendicular to p01. 192 | function perp(p0, p1) { 193 | const u01x = p0[1] - p1[1]; 194 | const u01y = p1[0] - p0[0]; 195 | const u01d = Math.sqrt(u01x * u01x + u01y * u01y); 196 | return [u01x / u01d, u01y / u01d]; 197 | } 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-svg-path-gradient", 3 | "version": "0.4.0", 4 | "description": "A utility for creating gradient paths with react-native-svg", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "npx eslint .", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/investingwolf/react-native-svg-path-gradient.git" 13 | }, 14 | "author": "Michel Kassabov", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/investingwolf/react-native-svg-path-gradient/issues" 18 | }, 19 | "homepage": "https://github.com/investingwolf/react-native-svg-path-gradient#readme", 20 | "keywords": [ 21 | "svg", 22 | "gradient", 23 | "path", 24 | "linear", 25 | "react-native", 26 | "react-native-component", 27 | "ios", 28 | "android" 29 | ], 30 | "peerDependencies": { 31 | "react": "*", 32 | "react-native": ">=0.50.0", 33 | "react-native-svg": ">=6.0.0" 34 | }, 35 | "dependencies": { 36 | "color": "^3.1.3", 37 | "prop-types": "^15.7.2", 38 | "svg-path-properties": "^1.0.10" 39 | }, 40 | "devDependencies": { 41 | "@react-native-community/eslint-config": "^2.0.0", 42 | "eslint": "^7.15.0", 43 | "react": "^17.0.1", 44 | "react-native": "^0.63.4", 45 | "react-native-svg": "^12.1.0" 46 | } 47 | } 48 | --------------------------------------------------------------------------------