├── .gitignore ├── NOTICE ├── README.md ├── package-lock.json ├── package.json └── src ├── convert.js ├── flutter_path.js ├── index.js └── utils └── svg_utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | # from: https://github.com/github/gitignore/blob/main/Node.gitignore 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # vuepress v2.x temp and cache directory 109 | .temp 110 | .cache 111 | 112 | # Docusaurus cache and generated files 113 | .docusaurus 114 | 115 | # Serverless directories 116 | .serverless/ 117 | 118 | # FuseBox cache 119 | .fusebox/ 120 | 121 | # DynamoDB Local files 122 | .dynamodb/ 123 | 124 | # TernJS port file 125 | .tern-port 126 | 127 | # Stores VSCode versions used for testing VSCode extensions 128 | .vscode-test 129 | 130 | # yarn v2 131 | .yarn/cache 132 | .yarn/unplugged 133 | .yarn/build-state.yml 134 | .yarn/install-state.gz 135 | .pnp.* 136 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Svg to flutter path converter is licensed under the Apache license version 2.0, January 2004 (see LICENSE file). 2 | 3 | Svg to flutter path converter uses the following third party libraries that may have licenses 4 | differing from that of Snap.svg itself. You can find the libraries and their 5 | respective licenses below. 6 | 7 | 8 | - Commander.js ./node_modules/commander 9 | 10 | https://github.com/tj/commander.js 11 | 12 | Copyright (c) 2011 TJ Holowaychuk 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining 15 | a copy of this software and associated documentation files (the 16 | 'Software'), to deal in the Software without restriction, including 17 | without limitation the rights to use, copy, modify, merge, publish, 18 | distribute, sublicense, and/or sell copies of the Software, and to 19 | permit persons to whom the Software is furnished to do so, subject to 20 | the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be 23 | included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 28 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 29 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 30 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 31 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | 33 | - svgo ./node_modules/svgo 34 | 35 | https://github.com/svg/svgo 36 | 37 | Copyright (c) Kir Belevich 38 | 39 | Permission is hereby granted, free of charge, to any person obtaining a copy 40 | of this software and associated documentation files (the "Software"), to deal 41 | in the Software without restriction, including without limitation the rights 42 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 43 | copies of the Software, and to permit persons to whom the Software is 44 | furnished to do so, subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all 47 | copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 50 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 51 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 52 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 53 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 54 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 55 | SOFTWARE. 56 | 57 | - svgson ./node_modules/svgson 58 | 59 | https://github.com/elrumordelaluz/svgson 60 | 61 | Copyright (c) 2018 Lionel Tzatzkin 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 78 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 79 | SOFTWARE. 80 | 81 | // end of Svg to flutter path converter notice 82 | 83 | // start of Snap.svg notice file 84 | 85 | Snap.svg is licensed under the Apache license version 2.0, January 2004 (see LICENSE file). 86 | 87 | Snap.svg uses the following third party libraries that may have licenses 88 | differing from that of Snap.svg itself. You can find the libraries and their 89 | respective licenses below. 90 | 91 | - eve ./node_modules/eve 92 | 93 | https://github.com/adobe-webplatform/eve/ 94 | 95 | Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 96 | 97 | Licensed under the Apache License, Version 2.0 (the "License"); 98 | you may not use this file except in compliance with the License. 99 | You may obtain a copy of the License at 100 | 101 | http://www.apache.org/licenses/LICENSE-2.0 102 | 103 | Unless required by applicable law or agreed to in writing, software 104 | distributed under the License is distributed on an "AS IS" BASIS, 105 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 106 | See the License for the specific language governing permissions and 107 | limitations under the License. 108 | 109 | - Mocha ./node_modules/mocha 110 | 111 | https://github.com/visionmedia/mocha/ 112 | 113 | (The MIT License) 114 | 115 | Copyright (c) 2011-2013 TJ Holowaychuk 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining 118 | a copy of this software and associated documentation files (the 119 | 'Software'), to deal in the Software without restriction, including 120 | without limitation the rights to use, copy, modify, merge, publish, 121 | distribute, sublicense, and/or sell copies of the Software, and to 122 | permit persons to whom the Software is furnished to do so, subject to 123 | the following conditions: 124 | 125 | The above copyright notice and this permission notice shall be 126 | included in all copies or substantial portions of the Software. 127 | 128 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 129 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 130 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 131 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 132 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 133 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 134 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 135 | 136 | - Expect ./node_modules/expect.js 137 | 138 | https://github.com/LearnBoost/expect.js 139 | 140 | (The MIT License) 141 | 142 | Copyright (c) 2011 Guillermo Rauch 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining 145 | a copy of this software and associated documentation files (the 146 | 'Software'), to deal in the Software without restriction, including 147 | without limitation the rights to use, copy, modify, merge, publish, 148 | distribute, sublicense, and/or sell copies of the Software, and to 149 | permit persons to whom the Software is furnished to do so, subject to 150 | the following conditions: 151 | 152 | The above copyright notice and this permission notice shall be 153 | included in all copies or substantial portions of the Software. 154 | 155 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 156 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 157 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 158 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 159 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 160 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 161 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 162 | 163 | - Grunt ./node_modules/grunt 164 | 165 | http://gruntjs.com 166 | 167 | Copyright (c) 2013 "Cowboy" Ben Alman 168 | 169 | Permission is hereby granted, free of charge, to any person 170 | obtaining a copy of this software and associated documentation 171 | files (the "Software"), to deal in the Software without 172 | restriction, including without limitation the rights to use, 173 | copy, modify, merge, publish, distribute, sublicense, and/or sell 174 | copies of the Software, and to permit persons to whom the 175 | Software is furnished to do so, subject to the following 176 | conditions: 177 | 178 | The above copyright notice and this permission notice shall be 179 | included in all copies or substantial portions of the Software. 180 | 181 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 182 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 183 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 184 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 185 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 186 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 187 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 188 | OTHER DEALINGS IN THE SOFTWARE. 189 | 190 | 191 | - Backbone ./demos/animated-game/js/backbone.js 192 | 193 | http://backbonejs.org/ 194 | 195 | (The MIT License) 196 | 197 | Copyright (c) 2010-2013 Jeremy Ashkenas, DocumentCloud 198 | 199 | Permission is hereby granted, free of charge, to any person obtaining 200 | a copy of this software and associated documentation files (the 201 | 'Software'), to deal in the Software without restriction, including 202 | without limitation the rights to use, copy, modify, merge, publish, 203 | distribute, sublicense, and/or sell copies of the Software, and to 204 | permit persons to whom the Software is furnished to do so, subject to 205 | the following conditions: 206 | 207 | The above copyright notice and this permission notice shall be 208 | included in all copies or substantial portions of the Software. 209 | 210 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 211 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 212 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 213 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 214 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 215 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 216 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 217 | 218 | 219 | - Underscore ./demos/animated-game/js/underscore.js 220 | 221 | http://underscorejs.org 222 | 223 | (The MIT License) 224 | 225 | Copyright (c) 2010-2013 Jeremy Ashkenas, DocumentCloud 226 | 227 | Permission is hereby granted, free of charge, to any person obtaining 228 | a copy of this software and associated documentation files (the 229 | 'Software'), to deal in the Software without restriction, including 230 | without limitation the rights to use, copy, modify, merge, publish, 231 | distribute, sublicense, and/or sell copies of the Software, and to 232 | permit persons to whom the Software is furnished to do so, subject to 233 | the following conditions: 234 | 235 | The above copyright notice and this permission notice shall be 236 | included in all copies or substantial portions of the Software. 237 | 238 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 239 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 240 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 241 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 242 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 243 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 244 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 245 | 246 | 247 | - jQuery ./demos/animated-game/js/jquery-1.9.0.min.js 248 | 249 | http://http://jquery.com/ 250 | 251 | (The MIT License) 252 | 253 | Copyright 2013 jQuery Foundation and other contributors 254 | http://jquery.com/ 255 | 256 | Permission is hereby granted, free of charge, to any person obtaining 257 | a copy of this software and associated documentation files (the 258 | 'Software'), to deal in the Software without restriction, including 259 | without limitation the rights to use, copy, modify, merge, publish, 260 | distribute, sublicense, and/or sell copies of the Software, and to 261 | permit persons to whom the Software is furnished to do so, subject to 262 | the following conditions: 263 | 264 | The above copyright notice and this permission notice shall be 265 | included in all copies or substantial portions of the Software. 266 | 267 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 268 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 269 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 270 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 271 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 272 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 273 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svg-to-flutter-path-converter 2 | 3 | Convert your SVG file directly to Flutter paths and prevent all the messing with bezier curves. 4 | 5 | ![https://www.flutterclutter.dev/images/tools/svg-to-flutter-path-converter.png](https://www.flutterclutter.dev/images/tools/svg-to-flutter-path-converter.png) 6 | 7 | # Flutter Clutter 8 | 9 | The tool was made in the context of [my blog](https://www.flutterclutter.dev). 10 | Find a demo [here](https://www.flutterclutter.dev/tools/svg-to-flutter-path-converter/). 11 | Also, a _how to_ can be found [here](https://www.flutterclutter.dev/flutter/tutorials/svg-to-flutter-path/2020/678/). 12 | 13 | # Usage as CLI Tool 14 | 15 | To use this tool via CLI: 16 | 17 | * Clone this repository 18 | * `cd` into cloned directory 19 | 20 | ## Use locally 21 | 22 | If you want to install it locally to prevent pollution of your global node namespace, do this: 23 | 24 | ``` 25 | npm i 26 | ``` 27 | 28 | Then you can run the conversion using 29 | 30 | ``` 31 | npm start convert [options] 32 | ``` 33 | 34 | ## Use globally 35 | 36 | If you want use it outside of the repository directory as well, use this: 37 | 38 | ``` 39 | npm i -g 40 | ``` 41 | 42 | The syntax to call the conversion via CLI is as follows: 43 | 44 | ``` 45 | svg-to-flutter convert [options] 46 | ``` 47 | 48 | The general usage looks like this: 49 | 50 | ``` 51 | Usage: svg-to-flutter [options] [command] 52 | 53 | Commands: 54 | convert [options] Convert svg file to Flutter path 55 | help [command] display help for command 56 | ``` 57 | 58 | ## Store the result on the file system 59 | 60 | When you run it without any options, it will directly return the output. 61 | In order to store the result on the file system, use the optional `output` argument (`-o` or `--output`). 62 | You can either provide a directory, which will create a file `output.dart` or a path to a file you want to have created. 63 | 64 | ``` 65 | svg-to-flutter convert input.svg -o . 66 | ``` 67 | 68 | ``` 69 | svg-to-flutter convert input.svg -o my-output.dart 70 | ``` 71 | 72 | # Collaborators 73 | 74 | * [Wojciech Warwas](https://github.com/obiwanzenobi) - Thankfully converted the JS+HTML tool into a separate node module 75 | 76 | `npm` integration coming soon! -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-to-flutter-path-converter", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "svg-to-flutter-path-converter", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "color-name-list": "^9.8.0", 13 | "commander": "9.2.0", 14 | "svgo": "2.8.0", 15 | "svgson": "5.2.1" 16 | }, 17 | "bin": { 18 | "svg-to-flutter": "src/index.js" 19 | } 20 | }, 21 | "node_modules/@trysound/sax": { 22 | "version": "0.2.0", 23 | "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", 24 | "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", 25 | "engines": { 26 | "node": ">=10.13.0" 27 | } 28 | }, 29 | "node_modules/boolbase": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 32 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 33 | }, 34 | "node_modules/color-name-list": { 35 | "version": "9.8.0", 36 | "resolved": "https://registry.npmjs.org/color-name-list/-/color-name-list-9.8.0.tgz", 37 | "integrity": "sha512-bTFOzBZTgcyTjogt/qMgOIXBo7SW1eaeqhhD/oj+BkqnMqO7zPbB4NOpfYYYENq2ltNXyGltn3kX4ZYL4VN6LA==", 38 | "engines": { 39 | "node": ">=8", 40 | "npm": ">=5" 41 | } 42 | }, 43 | "node_modules/commander": { 44 | "version": "9.2.0", 45 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", 46 | "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", 47 | "engines": { 48 | "node": "^12.20.0 || >=14" 49 | } 50 | }, 51 | "node_modules/css-select": { 52 | "version": "4.3.0", 53 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", 54 | "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", 55 | "dependencies": { 56 | "boolbase": "^1.0.0", 57 | "css-what": "^6.0.1", 58 | "domhandler": "^4.3.1", 59 | "domutils": "^2.8.0", 60 | "nth-check": "^2.0.1" 61 | }, 62 | "funding": { 63 | "url": "https://github.com/sponsors/fb55" 64 | } 65 | }, 66 | "node_modules/css-tree": { 67 | "version": "1.1.3", 68 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", 69 | "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", 70 | "dependencies": { 71 | "mdn-data": "2.0.14", 72 | "source-map": "^0.6.1" 73 | }, 74 | "engines": { 75 | "node": ">=8.0.0" 76 | } 77 | }, 78 | "node_modules/css-what": { 79 | "version": "6.1.0", 80 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 81 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 82 | "engines": { 83 | "node": ">= 6" 84 | }, 85 | "funding": { 86 | "url": "https://github.com/sponsors/fb55" 87 | } 88 | }, 89 | "node_modules/csso": { 90 | "version": "4.2.0", 91 | "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", 92 | "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", 93 | "dependencies": { 94 | "css-tree": "^1.1.2" 95 | }, 96 | "engines": { 97 | "node": ">=8.0.0" 98 | } 99 | }, 100 | "node_modules/deep-rename-keys": { 101 | "version": "0.2.1", 102 | "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", 103 | "integrity": "sha1-7eeFN9emaivmFRfir5Vtf1ij8dg=", 104 | "dependencies": { 105 | "kind-of": "^3.0.2", 106 | "rename-keys": "^1.1.2" 107 | }, 108 | "engines": { 109 | "node": ">=0.10.0" 110 | } 111 | }, 112 | "node_modules/dom-serializer": { 113 | "version": "1.4.1", 114 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", 115 | "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", 116 | "dependencies": { 117 | "domelementtype": "^2.0.1", 118 | "domhandler": "^4.2.0", 119 | "entities": "^2.0.0" 120 | }, 121 | "funding": { 122 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 123 | } 124 | }, 125 | "node_modules/domelementtype": { 126 | "version": "2.3.0", 127 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 128 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 129 | "funding": [ 130 | { 131 | "type": "github", 132 | "url": "https://github.com/sponsors/fb55" 133 | } 134 | ] 135 | }, 136 | "node_modules/domhandler": { 137 | "version": "4.3.1", 138 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", 139 | "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", 140 | "dependencies": { 141 | "domelementtype": "^2.2.0" 142 | }, 143 | "engines": { 144 | "node": ">= 4" 145 | }, 146 | "funding": { 147 | "url": "https://github.com/fb55/domhandler?sponsor=1" 148 | } 149 | }, 150 | "node_modules/domutils": { 151 | "version": "2.8.0", 152 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", 153 | "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", 154 | "dependencies": { 155 | "dom-serializer": "^1.0.1", 156 | "domelementtype": "^2.2.0", 157 | "domhandler": "^4.2.0" 158 | }, 159 | "funding": { 160 | "url": "https://github.com/fb55/domutils?sponsor=1" 161 | } 162 | }, 163 | "node_modules/entities": { 164 | "version": "2.2.0", 165 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 166 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", 167 | "funding": { 168 | "url": "https://github.com/fb55/entities?sponsor=1" 169 | } 170 | }, 171 | "node_modules/eventemitter3": { 172 | "version": "2.0.3", 173 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", 174 | "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" 175 | }, 176 | "node_modules/get-value": { 177 | "version": "2.0.6", 178 | "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", 179 | "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", 180 | "engines": { 181 | "node": ">=0.10.0" 182 | } 183 | }, 184 | "node_modules/has-value": { 185 | "version": "0.3.1", 186 | "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", 187 | "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", 188 | "dependencies": { 189 | "get-value": "^2.0.3", 190 | "has-values": "^0.1.4", 191 | "isobject": "^2.0.0" 192 | }, 193 | "engines": { 194 | "node": ">=0.10.0" 195 | } 196 | }, 197 | "node_modules/has-value/node_modules/isobject": { 198 | "version": "2.1.0", 199 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 200 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 201 | "dependencies": { 202 | "isarray": "1.0.0" 203 | }, 204 | "engines": { 205 | "node": ">=0.10.0" 206 | } 207 | }, 208 | "node_modules/has-values": { 209 | "version": "0.1.4", 210 | "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", 211 | "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", 212 | "engines": { 213 | "node": ">=0.10.0" 214 | } 215 | }, 216 | "node_modules/is-buffer": { 217 | "version": "1.1.6", 218 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 219 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 220 | }, 221 | "node_modules/is-plain-object": { 222 | "version": "2.0.4", 223 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 224 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 225 | "dependencies": { 226 | "isobject": "^3.0.1" 227 | }, 228 | "engines": { 229 | "node": ">=0.10.0" 230 | } 231 | }, 232 | "node_modules/isarray": { 233 | "version": "1.0.0", 234 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 235 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 236 | }, 237 | "node_modules/isobject": { 238 | "version": "3.0.1", 239 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 240 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 241 | "engines": { 242 | "node": ">=0.10.0" 243 | } 244 | }, 245 | "node_modules/kind-of": { 246 | "version": "3.2.2", 247 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 248 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 249 | "dependencies": { 250 | "is-buffer": "^1.1.5" 251 | }, 252 | "engines": { 253 | "node": ">=0.10.0" 254 | } 255 | }, 256 | "node_modules/mdn-data": { 257 | "version": "2.0.14", 258 | "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", 259 | "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" 260 | }, 261 | "node_modules/nth-check": { 262 | "version": "2.0.1", 263 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", 264 | "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", 265 | "dependencies": { 266 | "boolbase": "^1.0.0" 267 | }, 268 | "funding": { 269 | "url": "https://github.com/fb55/nth-check?sponsor=1" 270 | } 271 | }, 272 | "node_modules/omit-deep": { 273 | "version": "0.3.0", 274 | "resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz", 275 | "integrity": "sha1-IcivNJm8rdKWUaIyy8rLxSRF6+w=", 276 | "dependencies": { 277 | "is-plain-object": "^2.0.1", 278 | "unset-value": "^0.1.1" 279 | }, 280 | "engines": { 281 | "node": ">=0.10.0" 282 | } 283 | }, 284 | "node_modules/picocolors": { 285 | "version": "1.0.0", 286 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 287 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 288 | }, 289 | "node_modules/rename-keys": { 290 | "version": "1.2.0", 291 | "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", 292 | "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==", 293 | "engines": { 294 | "node": ">= 0.8.0" 295 | } 296 | }, 297 | "node_modules/source-map": { 298 | "version": "0.6.1", 299 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 300 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 301 | "engines": { 302 | "node": ">=0.10.0" 303 | } 304 | }, 305 | "node_modules/stable": { 306 | "version": "0.1.8", 307 | "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", 308 | "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" 309 | }, 310 | "node_modules/svgo": { 311 | "version": "2.8.0", 312 | "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", 313 | "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", 314 | "dependencies": { 315 | "@trysound/sax": "0.2.0", 316 | "commander": "^7.2.0", 317 | "css-select": "^4.1.3", 318 | "css-tree": "^1.1.3", 319 | "csso": "^4.2.0", 320 | "picocolors": "^1.0.0", 321 | "stable": "^0.1.8" 322 | }, 323 | "bin": { 324 | "svgo": "bin/svgo" 325 | }, 326 | "engines": { 327 | "node": ">=10.13.0" 328 | } 329 | }, 330 | "node_modules/svgo/node_modules/commander": { 331 | "version": "7.2.0", 332 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 333 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 334 | "engines": { 335 | "node": ">= 10" 336 | } 337 | }, 338 | "node_modules/svgson": { 339 | "version": "5.2.1", 340 | "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.2.1.tgz", 341 | "integrity": "sha512-nbM6QuyZiKzQ0Uo51VDta93YJAr96ikyT40PsgJRrzynOGsOlnmJ6zAK5hUFyE5gnxcg7yuOPUWbUlmV9K0+Dg==", 342 | "dependencies": { 343 | "deep-rename-keys": "^0.2.1", 344 | "omit-deep": "0.3.0", 345 | "xml-reader": "2.4.3" 346 | } 347 | }, 348 | "node_modules/unset-value": { 349 | "version": "0.1.2", 350 | "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", 351 | "integrity": "sha1-UGgQuGfyfCpabpsEgzYx9t5Y0xA=", 352 | "dependencies": { 353 | "has-value": "^0.3.1", 354 | "isobject": "^3.0.0" 355 | }, 356 | "engines": { 357 | "node": ">=0.10.0" 358 | } 359 | }, 360 | "node_modules/xml-lexer": { 361 | "version": "0.2.2", 362 | "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", 363 | "integrity": "sha1-UYGTpKozTVj8fSSLVJB5uJkH4EY=", 364 | "dependencies": { 365 | "eventemitter3": "^2.0.0" 366 | } 367 | }, 368 | "node_modules/xml-reader": { 369 | "version": "2.4.3", 370 | "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", 371 | "integrity": "sha1-n4EMr3xCWlqvuEixxFEDyecddTA=", 372 | "dependencies": { 373 | "eventemitter3": "^2.0.0", 374 | "xml-lexer": "^0.2.2" 375 | } 376 | } 377 | }, 378 | "dependencies": { 379 | "@trysound/sax": { 380 | "version": "0.2.0", 381 | "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", 382 | "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" 383 | }, 384 | "boolbase": { 385 | "version": "1.0.0", 386 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 387 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 388 | }, 389 | "color-name-list": { 390 | "version": "9.8.0", 391 | "resolved": "https://registry.npmjs.org/color-name-list/-/color-name-list-9.8.0.tgz", 392 | "integrity": "sha512-bTFOzBZTgcyTjogt/qMgOIXBo7SW1eaeqhhD/oj+BkqnMqO7zPbB4NOpfYYYENq2ltNXyGltn3kX4ZYL4VN6LA==" 393 | }, 394 | "commander": { 395 | "version": "9.2.0", 396 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", 397 | "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" 398 | }, 399 | "css-select": { 400 | "version": "4.3.0", 401 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", 402 | "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", 403 | "requires": { 404 | "boolbase": "^1.0.0", 405 | "css-what": "^6.0.1", 406 | "domhandler": "^4.3.1", 407 | "domutils": "^2.8.0", 408 | "nth-check": "^2.0.1" 409 | } 410 | }, 411 | "css-tree": { 412 | "version": "1.1.3", 413 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", 414 | "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", 415 | "requires": { 416 | "mdn-data": "2.0.14", 417 | "source-map": "^0.6.1" 418 | } 419 | }, 420 | "css-what": { 421 | "version": "6.1.0", 422 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 423 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" 424 | }, 425 | "csso": { 426 | "version": "4.2.0", 427 | "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", 428 | "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", 429 | "requires": { 430 | "css-tree": "^1.1.2" 431 | } 432 | }, 433 | "deep-rename-keys": { 434 | "version": "0.2.1", 435 | "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", 436 | "integrity": "sha1-7eeFN9emaivmFRfir5Vtf1ij8dg=", 437 | "requires": { 438 | "kind-of": "^3.0.2", 439 | "rename-keys": "^1.1.2" 440 | } 441 | }, 442 | "dom-serializer": { 443 | "version": "1.4.1", 444 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", 445 | "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", 446 | "requires": { 447 | "domelementtype": "^2.0.1", 448 | "domhandler": "^4.2.0", 449 | "entities": "^2.0.0" 450 | } 451 | }, 452 | "domelementtype": { 453 | "version": "2.3.0", 454 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 455 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 456 | }, 457 | "domhandler": { 458 | "version": "4.3.1", 459 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", 460 | "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", 461 | "requires": { 462 | "domelementtype": "^2.2.0" 463 | } 464 | }, 465 | "domutils": { 466 | "version": "2.8.0", 467 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", 468 | "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", 469 | "requires": { 470 | "dom-serializer": "^1.0.1", 471 | "domelementtype": "^2.2.0", 472 | "domhandler": "^4.2.0" 473 | } 474 | }, 475 | "entities": { 476 | "version": "2.2.0", 477 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 478 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" 479 | }, 480 | "eventemitter3": { 481 | "version": "2.0.3", 482 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", 483 | "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" 484 | }, 485 | "get-value": { 486 | "version": "2.0.6", 487 | "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", 488 | "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" 489 | }, 490 | "has-value": { 491 | "version": "0.3.1", 492 | "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", 493 | "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", 494 | "requires": { 495 | "get-value": "^2.0.3", 496 | "has-values": "^0.1.4", 497 | "isobject": "^2.0.0" 498 | }, 499 | "dependencies": { 500 | "isobject": { 501 | "version": "2.1.0", 502 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 503 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 504 | "requires": { 505 | "isarray": "1.0.0" 506 | } 507 | } 508 | } 509 | }, 510 | "has-values": { 511 | "version": "0.1.4", 512 | "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", 513 | "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" 514 | }, 515 | "is-buffer": { 516 | "version": "1.1.6", 517 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 518 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 519 | }, 520 | "is-plain-object": { 521 | "version": "2.0.4", 522 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 523 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 524 | "requires": { 525 | "isobject": "^3.0.1" 526 | } 527 | }, 528 | "isarray": { 529 | "version": "1.0.0", 530 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 531 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 532 | }, 533 | "isobject": { 534 | "version": "3.0.1", 535 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 536 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" 537 | }, 538 | "kind-of": { 539 | "version": "3.2.2", 540 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 541 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 542 | "requires": { 543 | "is-buffer": "^1.1.5" 544 | } 545 | }, 546 | "mdn-data": { 547 | "version": "2.0.14", 548 | "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", 549 | "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" 550 | }, 551 | "nth-check": { 552 | "version": "2.0.1", 553 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", 554 | "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", 555 | "requires": { 556 | "boolbase": "^1.0.0" 557 | } 558 | }, 559 | "omit-deep": { 560 | "version": "0.3.0", 561 | "resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz", 562 | "integrity": "sha1-IcivNJm8rdKWUaIyy8rLxSRF6+w=", 563 | "requires": { 564 | "is-plain-object": "^2.0.1", 565 | "unset-value": "^0.1.1" 566 | } 567 | }, 568 | "picocolors": { 569 | "version": "1.0.0", 570 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 571 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 572 | }, 573 | "rename-keys": { 574 | "version": "1.2.0", 575 | "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", 576 | "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==" 577 | }, 578 | "source-map": { 579 | "version": "0.6.1", 580 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 581 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 582 | }, 583 | "stable": { 584 | "version": "0.1.8", 585 | "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", 586 | "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" 587 | }, 588 | "svgo": { 589 | "version": "2.8.0", 590 | "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", 591 | "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", 592 | "requires": { 593 | "@trysound/sax": "0.2.0", 594 | "commander": "^7.2.0", 595 | "css-select": "^4.1.3", 596 | "css-tree": "^1.1.3", 597 | "csso": "^4.2.0", 598 | "picocolors": "^1.0.0", 599 | "stable": "^0.1.8" 600 | }, 601 | "dependencies": { 602 | "commander": { 603 | "version": "7.2.0", 604 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 605 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" 606 | } 607 | } 608 | }, 609 | "svgson": { 610 | "version": "5.2.1", 611 | "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.2.1.tgz", 612 | "integrity": "sha512-nbM6QuyZiKzQ0Uo51VDta93YJAr96ikyT40PsgJRrzynOGsOlnmJ6zAK5hUFyE5gnxcg7yuOPUWbUlmV9K0+Dg==", 613 | "requires": { 614 | "deep-rename-keys": "^0.2.1", 615 | "omit-deep": "0.3.0", 616 | "xml-reader": "2.4.3" 617 | } 618 | }, 619 | "unset-value": { 620 | "version": "0.1.2", 621 | "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", 622 | "integrity": "sha1-UGgQuGfyfCpabpsEgzYx9t5Y0xA=", 623 | "requires": { 624 | "has-value": "^0.3.1", 625 | "isobject": "^3.0.0" 626 | } 627 | }, 628 | "xml-lexer": { 629 | "version": "0.2.2", 630 | "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", 631 | "integrity": "sha1-UYGTpKozTVj8fSSLVJB5uJkH4EY=", 632 | "requires": { 633 | "eventemitter3": "^2.0.0" 634 | } 635 | }, 636 | "xml-reader": { 637 | "version": "2.4.3", 638 | "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", 639 | "integrity": "sha1-n4EMr3xCWlqvuEixxFEDyecddTA=", 640 | "requires": { 641 | "eventemitter3": "^2.0.0", 642 | "xml-lexer": "^0.2.2" 643 | } 644 | } 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-to-flutter-path-converter", 3 | "version": "1.0.0", 4 | "description": "Convert your SVG file directly to Flutter paths and prevent all the messing with bezier curves.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/flutter-clutter/svg-to-flutter-path-converter.git" 13 | }, 14 | "keywords": [ 15 | "svg", 16 | "flutter", 17 | "path", 18 | "paint", 19 | "canvas" 20 | ], 21 | "author": "Marc Gerken", 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/flutter-clutter/svg-to-flutter-path-converter/issues" 25 | }, 26 | "homepage": "https://github.com/flutter-clutter/svg-to-flutter-path-converter#readme", 27 | "dependencies": { 28 | "color-name-list": "9.8.0", 29 | "commander": "9.2.0", 30 | "svgo": "2.8.0", 31 | "svgson": "5.2.1" 32 | }, 33 | "bin": { 34 | "svg-to-flutter": "./src/index.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/convert.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { parse, stringify, parseSync } = require('svgson') 3 | const { optimize } = require('svgo'); 4 | const svgUtils = require('./utils/svg_utils') 5 | const flutterPath = require('./flutter_path') 6 | const namedColors = require('color-name-list'); 7 | 8 | class SvgNode { 9 | constructor(name, type, attributes) { 10 | this.name = name; 11 | this.type = type; 12 | this.attributes = attributes; 13 | } 14 | } 15 | 16 | class SvgToFlutterPathConverter { 17 | static supportedShapeDefinitions = ['path', 'circle', 'rect']; 18 | 19 | convertFromFilePath(filePath, config) { 20 | const data = fs.readFileSync(filePath, 'utf8'); 21 | 22 | const optimized = optimize(data, { 23 | convertStyleToAttrs: true, 24 | }).data; 25 | 26 | return this.convertFromString(optimized, config); 27 | } 28 | 29 | convertFromString(svgString, config) { 30 | let wholeSvg = parseSync(svgString); 31 | 32 | let parsedNodes = wholeSvg.children; 33 | 34 | var width = wholeSvg.attributes.width; 35 | var height = wholeSvg.attributes.height; 36 | 37 | if(wholeSvg.attributes.viewBox) { 38 | let viewBoxValues = wholeSvg.attributes.viewBox.split(" "); 39 | width = parseFloat(viewBoxValues[2]) - parseFloat(viewBoxValues[0]); 40 | height = parseFloat(viewBoxValues[1]) - parseFloat(viewBoxValues[3]); 41 | } 42 | 43 | 44 | let groups = this.flattenGroupAttribute(parsedNodes); 45 | let filteredNodes = this.filterSupportedNodes(groups, parsedNodes); 46 | 47 | let svgNodes = filteredNodes 48 | .map((element) => { 49 | element.attributes.style = this.styleStringToObject(element.attributes.style); 50 | return element; 51 | }) 52 | .map((element) => new SvgNode(element.name, element.type, element.attributes)); 53 | 54 | return shapesToFlutterCodeConverter(filteredNodes, width, height, config); 55 | } 56 | 57 | filterSupportedNodes(nodes, groups) { 58 | let supportedShapeDefinitions = ['path', 'circle', 'rect']; 59 | 60 | return groups 61 | .filter((value) => supportedShapeDefinitions.includes(value.name)) 62 | .concat( 63 | nodes.filter((value) => supportedShapeDefinitions.includes(value.name)) 64 | ); 65 | } 66 | 67 | flattenGroupAttribute(nodes) { 68 | let groups = nodes.filter((svgNode) => svgNode.name == "g"); 69 | 70 | return groups 71 | .flatMap( 72 | (group) => group.children.map( 73 | (svgNode) => this.mergeGroupStylesIntoElements(group, svgNode) 74 | ) 75 | ); 76 | } 77 | 78 | styleStringToObject(styleString) { 79 | let regex = /([\w-]*)\s*:\s*([^;]*)/g; 80 | let match, properties = {}; 81 | while (match = regex.exec(styleString)) properties[match[1]] = match[2].trim(); 82 | 83 | return properties; 84 | } 85 | 86 | mergeGroupStylesIntoElements(group, element) { 87 | if (group.attributes != null) { 88 | return { 89 | ...element, 90 | "attributes": { 91 | ...element.attributes, 92 | ...group.attributes, 93 | } 94 | } 95 | } else { 96 | return element; 97 | } 98 | }; 99 | } 100 | 101 | class ShapeToPathConverter { 102 | fromRect(pathObject) { 103 | let x = parseInt(pathObject.attributes.x); 104 | let y = parseInt(pathObject.attributes.y); 105 | let width = parseInt(pathObject.attributes.width); 106 | let height = parseInt(pathObject.attributes.height); 107 | 108 | let p = 'M ' + x + ',' + y + ' H ' + width + ' V ' + height + ' H ' + x + ' Z'; 109 | 110 | return p; 111 | } 112 | 113 | fromCircle(pathObject) { 114 | let cx = parseInt(pathObject.attributes.cx); 115 | let cy = parseInt(pathObject.attributes.cy); 116 | let r = parseInt(pathObject.attributes.r); 117 | let p = 'M ' + (+r + +cx) + ',' + cy + ' A ' + r + ',' + r + ' 0 0 1 ' + cx + ',' + (+r + +cy) + ' ' + r + ',' + r + ' 0 0 1 ' + (+cx - +r) + ',' + cy + ' ' + r + ',' + r + ' 0 0 1 ' + cx + ',' + (+cy - +r) + ' ' + r + ',' + r + ' 0 0 1 ' + (+cx + +r) + ',' + cy; 118 | 119 | return p; 120 | } 121 | } 122 | 123 | function getPathData(paths, width, height) { 124 | let pathData = { 125 | width: width, 126 | height: height, 127 | paths: [] 128 | } 129 | 130 | let shapeToPathConverter = new ShapeToPathConverter(); 131 | 132 | paths.forEach((svgNode) => { 133 | let pathString; 134 | if (svgNode.name === 'circle') { 135 | pathString = shapeToPathConverter.fromCircle(svgNode); 136 | } else if (svgNode.name === 'rect') { 137 | pathString = shapeToPathConverter.fromRect(svgNode); 138 | } else { 139 | pathString = svgNode.attributes.d; 140 | } 141 | 142 | let closed = pathString.endsWith("Z"); 143 | 144 | path = svgUtils.path2curve(pathString) 145 | path = changeLineCubicsToLines(path); 146 | 147 | pathData.paths.push({ 148 | type: svgNode.name, 149 | node: svgNode, 150 | preparedPath: path, 151 | closed: closed 152 | }); 153 | 154 | let pathBox = svgUtils.pathBBox(path); 155 | 156 | if (pathBox.width > pathData.width) { 157 | pathData.width = pathBox.width; 158 | } 159 | 160 | if (pathBox.height > pathData.height) { 161 | pathData.height = pathBox.height; 162 | } 163 | }); 164 | 165 | return pathData; 166 | } 167 | 168 | 169 | function getFillFromNode(node) { 170 | if (node.attributes.fill != null) { 171 | return node.attributes.fill; 172 | } 173 | 174 | if (node.attributes.style !== undefined && node.attributes.style.fill !== undefined) { 175 | return node.attributes.style.fill; 176 | } 177 | 178 | return ''; 179 | } 180 | 181 | function getStrokeFromNode(node) { 182 | if (node.attributes.stroke != null) { 183 | return node.attributes.stroke; 184 | } 185 | 186 | if (node.attributes.style !== undefined && node.attributes.style.stroke !== undefined) { 187 | return node.attributes.style.stroke; 188 | } 189 | 190 | return ''; 191 | } 192 | 193 | function colorStringToObject(value) { 194 | if (value == 'none') { 195 | return null; 196 | } 197 | if (value == null) { 198 | return null; 199 | } 200 | if (value == '') { 201 | return null; 202 | } 203 | let namedColor = namedColors.find(color => color.name.toLowerCase() === value.toLowerCase()); 204 | if (namedColor != null) { 205 | return namedColor.hex.replace("#", ""); 206 | } 207 | 208 | if (value[0] == '#' && value.length === 4) { 209 | let color = value; 210 | color = color.split("").map((item) => { 211 | if (item == "#") { return item } 212 | return item + item; 213 | }).join("") 214 | 215 | if (color[0] != "#") { 216 | color = "#" + color; 217 | } 218 | return color.substr(1); 219 | } 220 | if (value[0] == '#') { 221 | return value.substr(1); 222 | } 223 | return (value.match(/\d+/g)).map(function (o) { 224 | let val = (o * 1).toString(16); 225 | val = (val.length > 1) ? val : "0" + val; 226 | return val; 227 | }).join("") 228 | } 229 | 230 | function normalizeNumber(number) { 231 | return number.replace(/[^0-9]/g, ''); 232 | } 233 | 234 | function changeLineCubicsToLines(values) { 235 | function isCubicThatCouldBeLine(element) { 236 | return (element[0] == "C") && element.length >= 6 && element[3] == element[5] && element[4] == element[6]; 237 | } 238 | 239 | return values.map((element) => { 240 | if (isCubicThatCouldBeLine(element)) { 241 | return ["L", ...element.slice(3, 5)]; 242 | } 243 | return element; 244 | }); 245 | } 246 | 247 | function shapesToFlutterCodeConverter(shapes, width, height, config) { 248 | let printer = new flutterPath.FlutterCustomPaintPrinter(); 249 | let flutterPaths = []; 250 | 251 | lines = []; 252 | let pathData = getPathData(shapes, width, height); 253 | 254 | pathData.paths.forEach((path, index) => { 255 | let pathOperations = []; 256 | 257 | if (path.type === 'circle') { 258 | pathOperations.push( 259 | new flutterPath.AddOvalOperation( 260 | normalizeNumber(path.node.attributes.cx) / pathData.width, 261 | normalizeNumber(path.node.attributes.cy) / pathData.height, 262 | normalizeNumber(path.node.attributes.r) / pathData.width 263 | ) 264 | ); 265 | 266 | let color = colorStringToObject(getFillFromNode(path.node)); 267 | let opacity = path.node.attributes.style['fill-opacity'] == '' ? null : path.node.attributes.style['fill-opacity']; 268 | if (color == null) { 269 | color = 'ffffff'; 270 | opacity = '1'; 271 | } 272 | if (path.node.attributes['fill'] != 'none') { 273 | flutterPaths.push(new flutterPath.FlutterPath(pathOperations, color, opacity, flutterPath.PaintType.Fill)); 274 | } 275 | if (path.node.attributes['stroke'] != null) { 276 | let strokeColor = colorStringToObject(getStrokeFromNode(path.node)); 277 | let strokeOpacity = path.node.attributes.style['stroke-opacity'] == '' ? null : path.node.attributes.style['stroke-opacity']; 278 | flutterPaths.push(new flutterPath.FlutterPath(pathOperations, strokeColor, strokeOpacity, flutterPath.PaintType.Stroke)); 279 | } 280 | 281 | return; 282 | } 283 | 284 | if (path.type !== 'path') { 285 | return; 286 | } 287 | 288 | path.preparedPath.forEach((segment) => { 289 | switch (segment[0]) { 290 | case "M": 291 | pathOperations.push( 292 | new flutterPath.MoveToOperation( 293 | segment[1] / pathData.width, segment[2] / pathData.height 294 | ) 295 | ); 296 | break; 297 | case "L": 298 | pathOperations.push( 299 | new flutterPath.LineToOperation( 300 | segment[1] / pathData.width, segment[2] / pathData.height 301 | ) 302 | ); 303 | break; 304 | case "C": 305 | pathOperations.push( 306 | new flutterPath.CubicToOperation( 307 | segment[1] / pathData.width, 308 | segment[2] / pathData.height, 309 | segment[3] / pathData.width, 310 | segment[4] / pathData.height, 311 | segment[5] / pathData.width, 312 | segment[6] / pathData.height, 313 | ) 314 | ); 315 | break; 316 | } 317 | }); 318 | 319 | let color = colorStringToObject(getFillFromNode(path.node)); 320 | 321 | let opacity = path.node.attributes.style['fill-opacity'] == '' ? null : path.node.attributes.style['fill-opacity']; 322 | if (color == null) { 323 | opacity = '1'; 324 | } 325 | 326 | if (path.node.attributes['fill'] != 'none') { 327 | flutterPaths.push(new flutterPath.FlutterPath(pathOperations, color, opacity, flutterPath.PaintType.Fill)); 328 | } 329 | 330 | if (path.node.attributes['stroke'] != null) { 331 | let strokeColor = colorStringToObject(getStrokeFromNode(path.node)); 332 | let strokeOpacity = path.node.attributes.style['stroke-opacity'] == '' ? null : path.node.attributes.style['stroke-opacity']; 333 | let strokeWidth = path.node.attributes['stroke-width'] == '' ? null : path.node.attributes['stroke-width']; 334 | flutterPaths.push(new flutterPath.FlutterPath(pathOperations, strokeColor, strokeOpacity, flutterPath.PaintType.Stroke, strokeWidth, path.closed)); 335 | } 336 | }); 337 | 338 | return printer.print(flutterPaths, config); 339 | } 340 | 341 | module.exports = SvgToFlutterPathConverter 342 | -------------------------------------------------------------------------------- /src/flutter_path.js: -------------------------------------------------------------------------------- 1 | class FlutterPath { 2 | constructor(operations, color, opacity, paintType, strokeWidth, closed) { 3 | this.operations = operations; 4 | this.color = color; 5 | this.opacity = opacity; 6 | this.paintType = paintType; 7 | this.strokeWidth = strokeWidth; 8 | this.closed = closed; 9 | } 10 | } 11 | 12 | const PaintType = { 13 | Fill: 'Fill', 14 | Stroke: 'Stroke', 15 | } 16 | 17 | class PathOperation { 18 | createSizeDependentToken(sizeProperty, number, round) { 19 | const roundedNumber = helpers.roundNumber(number, round); 20 | 21 | if (roundedNumber == 0) { 22 | return '0'; 23 | } 24 | 25 | if (roundedNumber == 1) { 26 | return `size.${sizeProperty}`; 27 | } 28 | 29 | return `size.${sizeProperty} * ${roundedNumber}`; 30 | } 31 | } 32 | 33 | class MoveToOperation extends PathOperation { 34 | constructor(x, y) { 35 | super(); 36 | this.x = x; 37 | this.y = y; 38 | } 39 | 40 | toFlutterCommand(round = 2) { 41 | const x = this.createSizeDependentToken('width', this.x, round); 42 | const y = this.createSizeDependentToken('height', this.y, round); 43 | 44 | return `path.moveTo(${x}, ${y});`; 45 | } 46 | } 47 | 48 | class LineToOperation extends PathOperation { 49 | constructor(x, y) { 50 | super(); 51 | this.x = x; 52 | this.y = y; 53 | } 54 | 55 | toFlutterCommand(round = 2) { 56 | const x = this.createSizeDependentToken('width', this.x, round); 57 | const y = this.createSizeDependentToken('height', this.y, round); 58 | 59 | return `path.lineTo(${x}, ${y});`; 60 | } 61 | } 62 | 63 | class CubicToOperation extends PathOperation { 64 | constructor(x1, y1, x2, y2, x3, y3) { 65 | super(); 66 | this.x1 = x1; 67 | this.y1 = y1; 68 | this.x2 = x2; 69 | this.y2 = y2; 70 | this.x3 = x3; 71 | this.y3 = y3; 72 | } 73 | 74 | toFlutterCommand(round = 2) { 75 | const x1 = this.createSizeDependentToken('width', this.x1, round); 76 | const y1 = this.createSizeDependentToken('height', this.y1, round); 77 | const x2 = this.createSizeDependentToken('width', this.x2, round); 78 | const y2 = this.createSizeDependentToken('height', this.y2, round); 79 | const x3 = this.createSizeDependentToken('width', this.x3, round); 80 | const y3 = this.createSizeDependentToken('height', this.y3, round); 81 | 82 | return `path.cubicTo(${x1}, ${y1}, ${x2}, ${y2}, ${x3}, ${y3});`; 83 | } 84 | } 85 | 86 | class AddOvalOperation extends PathOperation { 87 | constructor(x, y, radius) { 88 | super(); 89 | this.x = x; 90 | this.y = y; 91 | this.radius = radius; 92 | } 93 | 94 | toFlutterCommand(round = 2) { 95 | const x = this.createSizeDependentToken('width', this.x, round); 96 | const y = this.createSizeDependentToken('height', this.y, round); 97 | const radius = this.createSizeDependentToken('width', this.radius, round); 98 | 99 | return `path.addOval(Rect.fromCircle(center: Offset(${x}, ${y}), radius: ${radius}));`; 100 | } 101 | } 102 | 103 | class FlutterPathPrinter { 104 | constructor(path) { 105 | this.path = path; 106 | } 107 | 108 | print() { 109 | return "TEST!"; 110 | } 111 | } 112 | 113 | class FlutterCustomPaintPrinter { 114 | print(paths, config) { 115 | let definition = [`class ${config?.name ?? 'MyPainter'} extends CustomPainter {`]; 116 | 117 | if (config?.pathTracing) { 118 | definition = definition.concat([ 119 | '', 120 | '\tfinal double progress;', 121 | '', 122 | '\tMyPainter({this.progress = 1.0});' 123 | ]); 124 | } 125 | 126 | const linesBefore = [ 127 | '\t@override', 128 | '\tvoid paint(Canvas canvas, Size size) {', 129 | '\t\tPath path = Path();', 130 | '\t\tfinal Paint paint = Paint();' 131 | ]; 132 | 133 | const linesAfter = [ 134 | '\t}', 135 | '', 136 | '\t@override', 137 | '\tbool shouldRepaint(CustomPainter oldDelegate) {', 138 | '\t\treturn true;', 139 | '\t}', 140 | '}' 141 | ]; 142 | 143 | let linesPaths = []; 144 | 145 | paths.forEach((path, index) => { 146 | linesPaths.push(''); 147 | linesPaths.push(`\t\t// Path ${index + 1} ${path.paintType}`); 148 | 149 | if (index > 0) { 150 | linesPaths.push('\t\tpath = Path();'); 151 | } 152 | 153 | 154 | let color = path.color; 155 | 156 | if (color == null) { 157 | color = '000000'; 158 | } 159 | 160 | const opacityString = path.opacity ? `.withOpacity(${path.opacity})` : ''; 161 | const colorCommand = "paint.color = const Color(0xff" + color + ")" + opacityString + ";" 162 | const colorCommandString = `\t\t${colorCommand}`; 163 | 164 | linesPaths.push(colorCommandString); 165 | if (path.paintType == PaintType.Stroke) { 166 | linesPaths.push('\t\tpaint.style = PaintingStyle.stroke;'); 167 | linesPaths.push('\t\tpaint.strokeWidth = ' + (path.strokeWidth ? path.strokeWidth : '1') + ';'); 168 | } 169 | 170 | path.operations.forEach((operation) => { 171 | linesPaths.push(`\t\t${operation.toFlutterCommand()}`); 172 | }); 173 | 174 | if (path.paintType == PaintType.Stroke && path.closed) { 175 | linesPaths.push('\t\tpath.close();'); 176 | } 177 | 178 | console.log("Config : " + JSON.stringify(config)); 179 | if (config?.pathTracingAll) { 180 | linesPaths.push('\t\tPathMetrics pathMetrics = path.computeMetrics();'); 181 | linesPaths.push('\t\tfor (PathMetric pathMetric in pathMetrics) {'); 182 | linesPaths.push('\t\t\tPath extractPath = pathMetric.extractPath('); 183 | linesPaths.push('\t\t\t\t0.0,'); 184 | linesPaths.push('\t\t\t\tpathMetric.length * progress,'); 185 | linesPaths.push('\t\t\t);'); 186 | 187 | linesPaths.push('\t\t\tcanvas.drawPath(extractPath, paint);'); 188 | linesPaths.push('\t\t}'); 189 | } else if (config?.pathTracing) { 190 | linesPaths.push(''); 191 | linesPaths.push('\t\tList pathMetrics = path.computeMetrics().toList();'); 192 | linesPaths.push(''); 193 | 194 | linesPaths.push('\t\tfinal numberOfOperations = pathMetrics.length;'); 195 | linesPaths.push('\t\tfinal singleOperationTime = 1.0 / numberOfOperations;'); 196 | linesPaths.push('\t\tfinal index = (progress / singleOperationTime).floor();'); 197 | linesPaths.push(''); 198 | 199 | linesPaths.push('\t\tif(index > 0) {'); 200 | linesPaths.push('\t\t\tList completePaths = pathMetrics.sublist(0, index);'); 201 | linesPaths.push('\t\t\tfor (final path in completePaths) {'); 202 | linesPaths.push('\t\t\t\tPath extractPath = path.extractPath('); 203 | linesPaths.push('\t\t\t\t\t0.0,'); 204 | linesPaths.push('\t\t\t\t\tpath.length,'); 205 | linesPaths.push('\t\t\t\t);'); 206 | linesPaths.push('\t\t\t\tcanvas.drawPath(extractPath, paint);'); 207 | linesPaths.push('\t\t\t}'); 208 | linesPaths.push('\t\t}'); 209 | 210 | linesPaths.push(''); 211 | 212 | linesPaths.push('\t\tif(index >= numberOfOperations) {'); 213 | linesPaths.push('\t\t\treturn;'); 214 | linesPaths.push('\t\t}'); 215 | 216 | linesPaths.push(''); 217 | 218 | linesPaths.push('\t\tfinal actualMetric = pathMetrics.elementAt(index);'); 219 | linesPaths.push('\t\tfinal localProgress = (progress - (singleOperationTime * index)) / singleOperationTime;'); 220 | linesPaths.push('\t\tPath extractPath = actualMetric.extractPath('); 221 | linesPaths.push('\t\t\t0.0,'); 222 | linesPaths.push('\t\t\tactualMetric.length * localProgress,'); 223 | linesPaths.push('\t\t);'); 224 | linesPaths.push('\t\tcanvas.drawPath(extractPath, paint);'); 225 | } else { 226 | linesPaths.push('\t\tcanvas.drawPath(path, paint);'); 227 | } 228 | }); 229 | 230 | return definition 231 | .concat(linesBefore) 232 | .concat(linesPaths) 233 | .concat(linesAfter).join('\n'); 234 | } 235 | } 236 | 237 | let helpers = { 238 | roundNumber: function (num, scale) { 239 | if (!("" + num).includes("e")) { 240 | return +(Math.round(num + "e+" + scale) + "e-" + scale); 241 | } else { 242 | let arr = ("" + num).split("e"); 243 | let sig = "" 244 | if (+arr[1] + scale > 0) { 245 | sig = "+"; 246 | } 247 | return +(Math.round(+arr[0] + "e" + sig + (+arr[1] + scale)) + "e-" + scale); 248 | } 249 | } 250 | } 251 | 252 | module.exports = { 253 | FlutterCustomPaintPrinter, 254 | FlutterPath, 255 | MoveToOperation, 256 | LineToOperation, 257 | CubicToOperation, 258 | AddOvalOperation, 259 | PaintType, 260 | }; 261 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const { program } = require('commander'); 4 | const SvgToFlutterPathConverter = require('./convert'); 5 | const fs = require('fs'); 6 | 7 | program 8 | .command('convert') 9 | .description('Convert svg file to Flutter path') 10 | .argument('') 11 | .option('-o --output ', 'Where to store the output file') 12 | .option('--path-tracing', 'Calculate path metrics and expose progress property. Default to false') 13 | .option('--path-tracing-all', 'Calculate path metrics and expose progress property. Draw all path segments at once. Default to false') 14 | .action(function(filePath, options) { 15 | converter = new SvgToFlutterPathConverter(); 16 | let tracing = options.pathTracing; 17 | let tracingAll = options.pathTracingAll; 18 | 19 | let config = { 20 | pathTracing: tracing, 21 | pathTracingAll: tracingAll 22 | } 23 | 24 | flutterPathString = converter.convertFromFilePath(filePath, config); 25 | let outputPath = options.output; 26 | 27 | if (!outputPath) { 28 | console.log(flutterPathString); 29 | return; 30 | } 31 | 32 | outputPathFs = !fs.existsSync(outputPath) ? null : fs.lstatSync(outputPath); 33 | 34 | if (outputPathFs !== null && outputPathFs.isDirectory()) { 35 | outputPath += '/output.dart'; 36 | } 37 | 38 | try { 39 | fs.writeFileSync(outputPath, flutterPathString); 40 | } catch (err) { 41 | console.error(err); 42 | } 43 | }) 44 | 45 | program.parse() 46 | -------------------------------------------------------------------------------- /src/utils/svg_utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2013 Adobe Systems Incorporated 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | */ 205 | 206 | /* 207 | Code needed to parse svg path data and convert it to bezier curves. 208 | Copied here because of snap svg library web browser dependencies (incompatible with node cli) 209 | 210 | Changes: Snap.* references are renamed or removed for simplicity (eg: Snap.path() is just path()) 211 | 212 | original repository: https://github.com/adobe-webplatform/Snap.svg/ 213 | 214 | @obiwanzenobi 24.04.2022 215 | */ 216 | 217 | var Str = String, 218 | objectToString = Object.prototype.toString, 219 | path = paths, 220 | pathCommand = /([a-z])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/ig, 221 | pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\s]*,?[\s]*/ig, 222 | has = "hasOwnProperty", 223 | math = Math, 224 | mmax = math.max, 225 | mmin = math.min, 226 | toFloat = parseFloat, 227 | toInt = parseInt, 228 | p2s = /,?([a-z]),?/gi, 229 | abs = math.abs, 230 | pow = math.pow, 231 | PI = math.PI, 232 | round = math.round 233 | 234 | function path2curve(path, path2) { 235 | var pth = !path2 && paths(path); 236 | if (!path2 && pth.curve) { 237 | return pathClone(pth.curve); 238 | } 239 | var p = pathToAbsolute(path), 240 | p2 = path2 && pathToAbsolute(path2), 241 | attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, 242 | attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, 243 | processPath = function (path, d, pcom) { 244 | var nx, ny; 245 | if (!path) { 246 | return ["C", d.x, d.y, d.x, d.y, d.x, d.y]; 247 | } 248 | !(path[0] in {T: 1, Q: 1}) && (d.qx = d.qy = null); 249 | switch (path[0]) { 250 | case "M": 251 | d.X = path[1]; 252 | d.Y = path[2]; 253 | break; 254 | case "A": 255 | path = ["C"].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1)))); 256 | break; 257 | case "S": 258 | if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S. 259 | nx = d.x * 2 - d.bx; // And reflect the previous 260 | ny = d.y * 2 - d.by; // command's control point relative to the current point. 261 | } 262 | else { // or some else or nothing 263 | nx = d.x; 264 | ny = d.y; 265 | } 266 | path = ["C", nx, ny].concat(path.slice(1)); 267 | break; 268 | case "T": 269 | if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T. 270 | d.qx = d.x * 2 - d.qx; // And make a reflection similar 271 | d.qy = d.y * 2 - d.qy; // to case "S". 272 | } 273 | else { // or something else or nothing 274 | d.qx = d.x; 275 | d.qy = d.y; 276 | } 277 | path = ["C"].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); 278 | break; 279 | case "Q": 280 | d.qx = path[1]; 281 | d.qy = path[2]; 282 | path = ["C"].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4])); 283 | break; 284 | case "L": 285 | path = ["C"].concat(l2c(d.x, d.y, path[1], path[2])); 286 | break; 287 | case "H": 288 | path = ["C"].concat(l2c(d.x, d.y, path[1], d.y)); 289 | break; 290 | case "V": 291 | path = ["C"].concat(l2c(d.x, d.y, d.x, path[1])); 292 | break; 293 | case "Z": 294 | path = ["C"].concat(l2c(d.x, d.y, d.X, d.Y)); 295 | break; 296 | } 297 | return path; 298 | }, 299 | fixArc = function (pp, i) { 300 | if (pp[i].length > 7) { 301 | pp[i].shift(); 302 | var pi = pp[i]; 303 | while (pi.length) { 304 | pcoms1[i] = "A"; // if created multiple C:s, their original seg is saved 305 | p2 && (pcoms2[i] = "A"); // the same as above 306 | pp.splice(i++, 0, ["C"].concat(pi.splice(0, 6))); 307 | } 308 | pp.splice(i, 1); 309 | ii = mmax(p.length, p2 && p2.length || 0); 310 | } 311 | }, 312 | fixM = function (path1, path2, a1, a2, i) { 313 | if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") { 314 | path2.splice(i, 0, ["M", a2.x, a2.y]); 315 | a1.bx = 0; 316 | a1.by = 0; 317 | a1.x = path1[i][1]; 318 | a1.y = path1[i][2]; 319 | ii = mmax(p.length, p2 && p2.length || 0); 320 | } 321 | }, 322 | pcoms1 = [], // path commands of original path p 323 | pcoms2 = [], // path commands of original path p2 324 | pfirst = "", // temporary holder for original path command 325 | pcom = ""; // holder for previous path command of original path 326 | for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) { 327 | p[i] && (pfirst = p[i][0]); // save current path command 328 | 329 | if (pfirst != "C") // C is not saved yet, because it may be result of conversion 330 | { 331 | pcoms1[i] = pfirst; // Save current path command 332 | i && ( pcom = pcoms1[i - 1]); // Get previous path command pcom 333 | } 334 | p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath 335 | 336 | if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command 337 | // which may produce multiple C:s 338 | // so we have to make sure that C is also C in original path 339 | 340 | fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1 341 | 342 | if (p2) { // the same procedures is done to p2 343 | p2[i] && (pfirst = p2[i][0]); 344 | if (pfirst != "C") { 345 | pcoms2[i] = pfirst; 346 | i && (pcom = pcoms2[i - 1]); 347 | } 348 | p2[i] = processPath(p2[i], attrs2, pcom); 349 | 350 | if (pcoms2[i] != "A" && pfirst == "C") { 351 | pcoms2[i] = "C"; 352 | } 353 | 354 | fixArc(p2, i); 355 | } 356 | fixM(p, p2, attrs, attrs2, i); 357 | fixM(p2, p, attrs2, attrs, i); 358 | var seg = p[i], 359 | seg2 = p2 && p2[i], 360 | seglen = seg.length, 361 | seg2len = p2 && seg2.length; 362 | attrs.x = seg[seglen - 2]; 363 | attrs.y = seg[seglen - 1]; 364 | attrs.bx = toFloat(seg[seglen - 4]) || attrs.x; 365 | attrs.by = toFloat(seg[seglen - 3]) || attrs.y; 366 | attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x); 367 | attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y); 368 | attrs2.x = p2 && seg2[seg2len - 2]; 369 | attrs2.y = p2 && seg2[seg2len - 1]; 370 | } 371 | if (!p2) { 372 | pth.curve = pathClone(p); 373 | } 374 | return p2 ? [p, p2] : p; 375 | } 376 | 377 | function mapPath(path, matrix) { 378 | if (!matrix) { 379 | return path; 380 | } 381 | var x, y, i, j, ii, jj, pathi; 382 | path = path2curve(path); 383 | for (i = 0, ii = path.length; i < ii; i++) { 384 | pathi = path[i]; 385 | for (j = 1, jj = pathi.length; j < jj; j += 2) { 386 | x = matrix.x(pathi[j], pathi[j + 1]); 387 | y = matrix.y(pathi[j], pathi[j + 1]); 388 | pathi[j] = x; 389 | pathi[j + 1] = y; 390 | } 391 | } 392 | return path; 393 | } 394 | 395 | function pathBBox(path) { 396 | var pth = paths(path); 397 | if (pth.bbox) { 398 | return clone(pth.bbox); 399 | } 400 | if (!path) { 401 | return box(); 402 | } 403 | path = path2curve(path); 404 | var x = 0, 405 | y = 0, 406 | X = [], 407 | Y = [], 408 | p; 409 | for (var i = 0, ii = path.length; i < ii; i++) { 410 | p = path[i]; 411 | if (p[0] == "M") { 412 | x = p[1]; 413 | y = p[2]; 414 | X.push(x); 415 | Y.push(y); 416 | } else { 417 | var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); 418 | X = X.concat(dim.min.x, dim.max.x); 419 | Y = Y.concat(dim.min.y, dim.max.y); 420 | x = p[5]; 421 | y = p[6]; 422 | } 423 | } 424 | var xmin = mmin.apply(0, X), 425 | ymin = mmin.apply(0, Y), 426 | xmax = mmax.apply(0, X), 427 | ymax = mmax.apply(0, Y), 428 | bb = box(xmin, ymin, xmax - xmin, ymax - ymin); 429 | pth.bbox = clone(bb); 430 | return bb; 431 | } 432 | 433 | function paths(ps) { 434 | var p = paths.ps = paths.ps || {}; 435 | if (p[ps]) { 436 | p[ps].sleep = 100; 437 | } else { 438 | p[ps] = { 439 | sleep: 100 440 | }; 441 | } 442 | setTimeout(function () { 443 | for (var key in p) if (p[has](key) && key != ps) { 444 | p[key].sleep--; 445 | !p[key].sleep && delete p[key]; 446 | } 447 | }); 448 | return p[ps]; 449 | } 450 | 451 | function pathToAbsolute(pathArray) { 452 | var pth = paths(pathArray); 453 | if (pth.abs) { 454 | return pathClone(pth.abs); 455 | } 456 | if (!is(pathArray, "array") || !is(pathArray && pathArray[0], "array")) { // rough assumption 457 | pathArray = parsePathString(pathArray); 458 | } 459 | if (!pathArray || !pathArray.length) { 460 | return [["M", 0, 0]]; 461 | } 462 | var res = [], 463 | x = 0, 464 | y = 0, 465 | mx = 0, 466 | my = 0, 467 | start = 0, 468 | pa0; 469 | if (pathArray[0][0] == "M") { 470 | x = +pathArray[0][1]; 471 | y = +pathArray[0][2]; 472 | mx = x; 473 | my = y; 474 | start++; 475 | res[0] = ["M", x, y]; 476 | } 477 | var crz = pathArray.length == 3 && 478 | pathArray[0][0] == "M" && 479 | pathArray[1][0].toUpperCase() == "R" && 480 | pathArray[2][0].toUpperCase() == "Z"; 481 | for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) { 482 | res.push(r = []); 483 | pa = pathArray[i]; 484 | pa0 = pa[0]; 485 | if (pa0 != pa0.toUpperCase()) { 486 | r[0] = pa0.toUpperCase(); 487 | switch (r[0]) { 488 | case "A": 489 | r[1] = pa[1]; 490 | r[2] = pa[2]; 491 | r[3] = pa[3]; 492 | r[4] = pa[4]; 493 | r[5] = pa[5]; 494 | r[6] = +pa[6] + x; 495 | r[7] = +pa[7] + y; 496 | break; 497 | case "V": 498 | r[1] = +pa[1] + y; 499 | break; 500 | case "H": 501 | r[1] = +pa[1] + x; 502 | break; 503 | case "R": 504 | var dots = [x, y].concat(pa.slice(1)); 505 | for (var j = 2, jj = dots.length; j < jj; j++) { 506 | dots[j] = +dots[j] + x; 507 | dots[++j] = +dots[j] + y; 508 | } 509 | res.pop(); 510 | res = res.concat(catmullRom2bezier(dots, crz)); 511 | break; 512 | case "O": 513 | res.pop(); 514 | dots = ellipsePath(x, y, pa[1], pa[2]); 515 | dots.push(dots[0]); 516 | res = res.concat(dots); 517 | break; 518 | case "U": 519 | res.pop(); 520 | res = res.concat(ellipsePath(x, y, pa[1], pa[2], pa[3])); 521 | r = ["U"].concat(res[res.length - 1].slice(-2)); 522 | break; 523 | case "M": 524 | mx = +pa[1] + x; 525 | my = +pa[2] + y; 526 | default: 527 | for (j = 1, jj = pa.length; j < jj; j++) { 528 | r[j] = +pa[j] + (j % 2 ? x : y); 529 | } 530 | } 531 | } else if (pa0 == "R") { 532 | dots = [x, y].concat(pa.slice(1)); 533 | res.pop(); 534 | res = res.concat(catmullRom2bezier(dots, crz)); 535 | r = ["R"].concat(pa.slice(-2)); 536 | } else if (pa0 == "O") { 537 | res.pop(); 538 | dots = ellipsePath(x, y, pa[1], pa[2]); 539 | dots.push(dots[0]); 540 | res = res.concat(dots); 541 | } else if (pa0 == "U") { 542 | res.pop(); 543 | res = res.concat(ellipsePath(x, y, pa[1], pa[2], pa[3])); 544 | r = ["U"].concat(res[res.length - 1].slice(-2)); 545 | } else { 546 | for (var k = 0, kk = pa.length; k < kk; k++) { 547 | r[k] = pa[k]; 548 | } 549 | } 550 | pa0 = pa0.toUpperCase(); 551 | if (pa0 != "O") { 552 | switch (r[0]) { 553 | case "Z": 554 | x = +mx; 555 | y = +my; 556 | break; 557 | case "H": 558 | x = r[1]; 559 | break; 560 | case "V": 561 | y = r[1]; 562 | break; 563 | case "M": 564 | mx = r[r.length - 2]; 565 | my = r[r.length - 1]; 566 | default: 567 | x = r[r.length - 2]; 568 | y = r[r.length - 1]; 569 | } 570 | } 571 | } 572 | res.toString = toString; 573 | pth.abs = pathClone(res); 574 | return res; 575 | } 576 | function l2c(x1, y1, x2, y2) { 577 | return [x1, y1, x2, y2, x2, y2]; 578 | } 579 | function q2c(x1, y1, ax, ay, x2, y2) { 580 | var _13 = 1 / 3, 581 | _23 = 2 / 3; 582 | return [ 583 | _13 * x1 + _23 * ax, 584 | _13 * y1 + _23 * ay, 585 | _13 * x2 + _23 * ax, 586 | _13 * y2 + _23 * ay, 587 | x2, 588 | y2 589 | ]; 590 | } 591 | 592 | function pathClone(pathArray) { 593 | var res = clone(pathArray); 594 | res.toString = toString; 595 | return res; 596 | } 597 | 598 | function is(o, type) { 599 | type = Str.prototype.toLowerCase.call(type); 600 | if (type == "finite") { 601 | return isFinite(o); 602 | } 603 | if (type == "array" && 604 | (o instanceof Array || Array.isArray && Array.isArray(o))) { 605 | return true; 606 | } 607 | return type == "null" && o === null || 608 | type == typeof o && o !== null || 609 | type == "object" && o === Object(o) || 610 | objectToString.call(o).slice(8, -1).toLowerCase() == type; 611 | } 612 | 613 | parsePathString = function (pathString) { 614 | if (!pathString) { 615 | return null; 616 | } 617 | var pth = path(pathString); 618 | if (pth.arr) { 619 | return pathClone(pth.arr); 620 | } 621 | 622 | var paramCounts = {a: 7, c: 6, o: 2, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, u: 3, z: 0}, 623 | data = []; 624 | if (is(pathString, "array") && is(pathString[0], "array")) { // rough assumption 625 | data = pathClone(pathString); 626 | } 627 | if (!data.length) { 628 | Str(pathString).replace(pathCommand, function (a, b, c) { 629 | var params = [], 630 | name = b.toLowerCase(); 631 | c.replace(pathValues, function (a, b) { 632 | b && params.push(+b); 633 | }); 634 | if (name == "m" && params.length > 2) { 635 | data.push([b].concat(params.splice(0, 2))); 636 | name = "l"; 637 | b = b == "m" ? "l" : "L"; 638 | } 639 | if (name == "o" && params.length == 1) { 640 | data.push([b, params[0]]); 641 | } 642 | if (name == "r") { 643 | data.push([b].concat(params)); 644 | } else while (params.length >= paramCounts[name]) { 645 | data.push([b].concat(params.splice(0, paramCounts[name]))); 646 | if (!paramCounts[name]) { 647 | break; 648 | } 649 | } 650 | }); 651 | } 652 | data.toString = toString; 653 | pth.arr = pathClone(data); 654 | return data; 655 | }; 656 | 657 | function paths(ps) { 658 | var p = paths.ps = paths.ps || {}; 659 | if (p[ps]) { 660 | p[ps].sleep = 100; 661 | } else { 662 | p[ps] = { 663 | sleep: 100 664 | }; 665 | } 666 | setTimeout(function () { 667 | for (var key in p) if (p[has](key) && key != ps) { 668 | p[key].sleep--; 669 | !p[key].sleep && delete p[key]; 670 | } 671 | }); 672 | return p[ps]; 673 | } 674 | 675 | function toString() { 676 | return this.join(",").replace(p2s, "$1"); 677 | } 678 | 679 | function clone(obj) { 680 | if (typeof obj == "function" || Object(obj) !== obj) { 681 | return obj; 682 | } 683 | var res = new obj.constructor; 684 | for (var key in obj) if (obj[has](key)) { 685 | res[key] = clone(obj[key]); 686 | } 687 | return res; 688 | } 689 | 690 | function curveDim(x0, y0, x1, y1, x2, y2, x3, y3) { 691 | var tvalues = [], 692 | bounds = [[], []], 693 | a, b, c, t, t1, t2, b2ac, sqrtb2ac; 694 | for (var i = 0; i < 2; ++i) { 695 | if (i == 0) { 696 | b = 6 * x0 - 12 * x1 + 6 * x2; 697 | a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; 698 | c = 3 * x1 - 3 * x0; 699 | } else { 700 | b = 6 * y0 - 12 * y1 + 6 * y2; 701 | a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; 702 | c = 3 * y1 - 3 * y0; 703 | } 704 | if (abs(a) < 1e-12) { 705 | if (abs(b) < 1e-12) { 706 | continue; 707 | } 708 | t = -c / b; 709 | if (0 < t && t < 1) { 710 | tvalues.push(t); 711 | } 712 | continue; 713 | } 714 | b2ac = b * b - 4 * c * a; 715 | sqrtb2ac = math.sqrt(b2ac); 716 | if (b2ac < 0) { 717 | continue; 718 | } 719 | t1 = (-b + sqrtb2ac) / (2 * a); 720 | if (0 < t1 && t1 < 1) { 721 | tvalues.push(t1); 722 | } 723 | t2 = (-b - sqrtb2ac) / (2 * a); 724 | if (0 < t2 && t2 < 1) { 725 | tvalues.push(t2); 726 | } 727 | } 728 | 729 | var x, y, j = tvalues.length, 730 | jlen = j, 731 | mt; 732 | while (j--) { 733 | t = tvalues[j]; 734 | mt = 1 - t; 735 | bounds[0][j] = mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3; 736 | bounds[1][j] = mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3; 737 | } 738 | 739 | bounds[0][jlen] = x0; 740 | bounds[1][jlen] = y0; 741 | bounds[0][jlen + 1] = x3; 742 | bounds[1][jlen + 1] = y3; 743 | bounds[0].length = bounds[1].length = jlen + 2; 744 | 745 | 746 | return { 747 | min: {x: mmin.apply(0, bounds[0]), y: mmin.apply(0, bounds[1])}, 748 | max: {x: mmax.apply(0, bounds[0]), y: mmax.apply(0, bounds[1])} 749 | }; 750 | } 751 | 752 | function box(x, y, width, height) { 753 | if (x == null) { 754 | x = y = width = height = 0; 755 | } 756 | if (y == null) { 757 | y = x.y; 758 | width = x.width; 759 | height = x.height; 760 | x = x.x; 761 | } 762 | return { 763 | x: x, 764 | y: y, 765 | width: width, 766 | w: width, 767 | height: height, 768 | h: height, 769 | x2: x + width, 770 | y2: y + height, 771 | cx: x + width / 2, 772 | cy: y + height / 2, 773 | r1: math.min(width, height) / 2, 774 | r2: math.max(width, height) / 2, 775 | r0: math.sqrt(width * width + height * height) / 2, 776 | path: rectPath(x, y, width, height), 777 | vb: [x, y, width, height].join(" ") 778 | }; 779 | } 780 | 781 | function rectPath(x, y, w, h, r) { 782 | if (r) { 783 | return [ 784 | ["M", +x + +r, y], 785 | ["l", w - r * 2, 0], 786 | ["a", r, r, 0, 0, 1, r, r], 787 | ["l", 0, h - r * 2], 788 | ["a", r, r, 0, 0, 1, -r, r], 789 | ["l", r * 2 - w, 0], 790 | ["a", r, r, 0, 0, 1, -r, -r], 791 | ["l", 0, r * 2 - h], 792 | ["a", r, r, 0, 0, 1, r, -r], 793 | ["z"] 794 | ]; 795 | } 796 | var res = [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]]; 797 | res.toString = toString; 798 | return res; 799 | } 800 | function ellipsePath(x, y, rx, ry, a) { 801 | if (a == null && ry == null) { 802 | ry = rx; 803 | } 804 | x = +x; 805 | y = +y; 806 | rx = +rx; 807 | ry = +ry; 808 | if (a != null) { 809 | var rad = Math.PI / 180, 810 | x1 = x + rx * Math.cos(-ry * rad), 811 | x2 = x + rx * Math.cos(-a * rad), 812 | y1 = y + rx * Math.sin(-ry * rad), 813 | y2 = y + rx * Math.sin(-a * rad), 814 | res = [["M", x1, y1], ["A", rx, rx, 0, +(a - ry > 180), 0, x2, y2]]; 815 | } else { 816 | res = [ 817 | ["M", x, y], 818 | ["m", 0, -ry], 819 | ["a", rx, ry, 0, 1, 1, 0, 2 * ry], 820 | ["a", rx, ry, 0, 1, 1, 0, -2 * ry], 821 | ["z"] 822 | ]; 823 | } 824 | res.toString = toString; 825 | return res; 826 | } 827 | 828 | function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { 829 | // for more information of where this math came from visit: 830 | // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes 831 | var _120 = PI * 120 / 180, 832 | rad = PI / 180 * (+angle || 0), 833 | res = [], 834 | xy, 835 | rotate = cacher(function (x, y, rad) { 836 | var X = x * math.cos(rad) - y * math.sin(rad), 837 | Y = x * math.sin(rad) + y * math.cos(rad); 838 | return {x: X, y: Y}; 839 | }); 840 | if (!rx || !ry) { 841 | return [x1, y1, x2, y2, x2, y2]; 842 | } 843 | if (!recursive) { 844 | xy = rotate(x1, y1, -rad); 845 | x1 = xy.x; 846 | y1 = xy.y; 847 | xy = rotate(x2, y2, -rad); 848 | x2 = xy.x; 849 | y2 = xy.y; 850 | var cos = math.cos(PI / 180 * angle), 851 | sin = math.sin(PI / 180 * angle), 852 | x = (x1 - x2) / 2, 853 | y = (y1 - y2) / 2; 854 | var h = x * x / (rx * rx) + y * y / (ry * ry); 855 | if (h > 1) { 856 | h = math.sqrt(h); 857 | rx = h * rx; 858 | ry = h * ry; 859 | } 860 | var rx2 = rx * rx, 861 | ry2 = ry * ry, 862 | k = (large_arc_flag == sweep_flag ? -1 : 1) * 863 | math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))), 864 | cx = k * rx * y / ry + (x1 + x2) / 2, 865 | cy = k * -ry * x / rx + (y1 + y2) / 2, 866 | f1 = math.asin(((y1 - cy) / ry).toFixed(9)), 867 | f2 = math.asin(((y2 - cy) / ry).toFixed(9)); 868 | 869 | f1 = x1 < cx ? PI - f1 : f1; 870 | f2 = x2 < cx ? PI - f2 : f2; 871 | f1 < 0 && (f1 = PI * 2 + f1); 872 | f2 < 0 && (f2 = PI * 2 + f2); 873 | if (sweep_flag && f1 > f2) { 874 | f1 = f1 - PI * 2; 875 | } 876 | if (!sweep_flag && f2 > f1) { 877 | f2 = f2 - PI * 2; 878 | } 879 | } else { 880 | f1 = recursive[0]; 881 | f2 = recursive[1]; 882 | cx = recursive[2]; 883 | cy = recursive[3]; 884 | } 885 | var df = f2 - f1; 886 | if (abs(df) > _120) { 887 | var f2old = f2, 888 | x2old = x2, 889 | y2old = y2; 890 | f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); 891 | x2 = cx + rx * math.cos(f2); 892 | y2 = cy + ry * math.sin(f2); 893 | res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); 894 | } 895 | df = f2 - f1; 896 | var c1 = math.cos(f1), 897 | s1 = math.sin(f1), 898 | c2 = math.cos(f2), 899 | s2 = math.sin(f2), 900 | t = math.tan(df / 4), 901 | hx = 4 / 3 * rx * t, 902 | hy = 4 / 3 * ry * t, 903 | m1 = [x1, y1], 904 | m2 = [x1 + hx * s1, y1 - hy * c1], 905 | m3 = [x2 + hx * s2, y2 - hy * c2], 906 | m4 = [x2, y2]; 907 | m2[0] = 2 * m1[0] - m2[0]; 908 | m2[1] = 2 * m1[1] - m2[1]; 909 | if (recursive) { 910 | return [m2, m3, m4].concat(res); 911 | } else { 912 | res = [m2, m3, m4].concat(res).join().split(","); 913 | var newres = []; 914 | for (var i = 0, ii = res.length; i < ii; i++) { 915 | newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; 916 | } 917 | return newres; 918 | } 919 | } 920 | 921 | function cacher(f, scope, postprocessor) { 922 | function newf() { 923 | var arg = Array.prototype.slice.call(arguments, 0), 924 | args = arg.join("\u2400"), 925 | cache = newf.cache = newf.cache || {}, 926 | count = newf.count = newf.count || []; 927 | if (cache[has](args)) { 928 | repush(count, args); 929 | return postprocessor ? postprocessor(cache[args]) : cache[args]; 930 | } 931 | count.length >= 1e3 && delete cache[count.shift()]; 932 | count.push(args); 933 | cache[args] = f.apply(scope, arg); 934 | return postprocessor ? postprocessor(cache[args]) : cache[args]; 935 | } 936 | return newf; 937 | } 938 | 939 | function repush(array, item) { 940 | for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) { 941 | return array.push(array.splice(i, 1)[0]); 942 | } 943 | } 944 | 945 | module.exports = { 946 | path2curve: path2curve, 947 | pathBBox: pathBBox 948 | } 949 | --------------------------------------------------------------------------------