├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json └── src ├── svg.js ├── svggroup.js ├── svgnode.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PIXIv5 SVG support 2 | 3 | This is hybrid of [pixi-svg](https://github.com/bigtimebuddy/pixi-svg), [pixi-svg-graphics](https://github.com/saschagehlich/pixi-svg-graphics) and more fixes and adaptations for pixi v5 4 | ### Version support 5 | 6 | Pixi v5.1.1 or greater 7 | 8 | ## Preview 9 | Test it in [LIVE](https://exponenta.github.io/pixi5-svg-examples/live.html) 10 | 11 | ### Supported Features 12 | 13 | Only supports a subset of SVG's feature. Current this includes: 14 | 15 | SVG Elements: 16 | * `path` 17 | * M, L, H, V, S, C, T, Q, Z - fully 18 | * A - partial, large sweep flag is ignored. ARC transforms to multiple bezier curves 19 | * `circle` 20 | * `ellipse` 21 | * `rect` 22 | * `line` 23 | * `polygon` 24 | * `polyline` 25 | * `g` 26 | 27 | Style attributes with the following properties (inlcude inline styles): 28 | * `stroke` 29 | * `stroke-opacity` 30 | * `stroke-width` 31 | * `fill` 32 | * `fill-opacity` 33 | * `opacity` 34 | 35 | Transforms: 36 | * `matrix` 37 | * `translate` 38 | * `rotate` 39 | * `scale` 40 | 41 | ## Features 42 | * Support all exists path commands with maximum similarity of SVG, exclude arc large sweep (you can send PR for it) 43 | * Style inheritance from `g` 44 | * Support single graphics 45 | * Support SVG tree unpacking (with options `unpackTree`) 46 | * Support node picking in single mode 47 | * Support `SVGElement` or raw text of svg 48 | 49 | ## Problems 50 | * Selfcrossed shapes not supports yet in PIXI.Graphics 51 | * Large sweep flags not supported yet (i don't know why it isn't works) 52 | * Because text node cannot support on single graphics mode, it's ignores always 53 | * Mitter limit can't supported in PIXI yet 54 | * Bounds calculation is incorect https://github.com/pixijs/pixi.js/pull/5991 55 | ## Usage 56 | 57 | Install `npm install pixi5-svg` 58 | 59 | Use: 60 | ``` 61 | import Svg from "pixi5-svg" 62 | 63 | const svgText = '.....'; 64 | const options = {/* see DefaultOptions */} 65 | const svg = new Svg(svgText, options); 66 | 67 | 68 | ``` 69 | 70 | Examples : https://github.com/eXponenta/pixi5-svg-examples 71 | 72 | Demo: https://exponenta.github.io/pixi5-svg-examples/ 73 | Live: https://exponenta.github.io/pixi5-svg-examples/live.html 74 | 75 | 76 | ### Alternatives 77 | 78 | https://github.com/bigtimebuddy/pixi-svg 79 | 80 | https://github.com/saschagehlich/pixi-svg-graphics -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { SVG } from "./src/svg"; 2 | import { SVGNode } from "./src/svgnode" 3 | import { SVGGroup } from "./src/svggroup" 4 | 5 | //faster, better, longer!! 6 | SVGGroup.prototype.parseChildren = SVGNode.prototype.parseChildren; 7 | 8 | export { 9 | SVGNode, SVGGroup 10 | } 11 | 12 | export default SVG; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi5-svg", 3 | "version": "0.1.10", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@pixi/accessibility": { 8 | "version": "5.1.1", 9 | "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-5.1.1.tgz", 10 | "integrity": "sha512-2/tVkcnocauIWg3eCBLKp7O8sXjkq2MbAtYSo1NhtdDsjPELVSEjqSyEkfXvp95iEQEM/k2Gomg3kDemkvlksQ==", 11 | "requires": { 12 | "@pixi/core": "^5.1.1", 13 | "@pixi/display": "^5.1.1", 14 | "@pixi/utils": "^5.1.1" 15 | } 16 | }, 17 | "@pixi/app": { 18 | "version": "5.1.1", 19 | "resolved": "https://registry.npmjs.org/@pixi/app/-/app-5.1.1.tgz", 20 | "integrity": "sha512-LayaFW+nd00rDhemP6JlbNk9AZBILuxRNxf/RRm87hKWbpToKtNr/Z9rP5WdKaYWnD9aGjEzP/HA12TiDjviBA==", 21 | "requires": { 22 | "@pixi/core": "^5.1.1", 23 | "@pixi/display": "^5.1.1" 24 | } 25 | }, 26 | "@pixi/constants": { 27 | "version": "5.1.0", 28 | "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-5.1.0.tgz", 29 | "integrity": "sha512-86cogDvjF9yNvmxeizwkIhA0Kl2z3gUSWMf2daYx903dzyje7fwkzRrKLnqDUn6vSAxRXiska0DMJhwYsIC29w==" 30 | }, 31 | "@pixi/core": { 32 | "version": "5.1.1", 33 | "resolved": "https://registry.npmjs.org/@pixi/core/-/core-5.1.1.tgz", 34 | "integrity": "sha512-VSWMUhK4ZM66/i+0IjEOIKm+4jDDvunFsaOF99LTq3vk1MqsuMxzibyowylu/Roy5mdtscK6tV9cbt/Pyvzs0w==", 35 | "requires": { 36 | "@pixi/constants": "^5.1.0", 37 | "@pixi/display": "^5.1.1", 38 | "@pixi/math": "^5.1.0", 39 | "@pixi/runner": "^5.1.1", 40 | "@pixi/settings": "^5.1.1", 41 | "@pixi/ticker": "^5.1.1", 42 | "@pixi/utils": "^5.1.1" 43 | } 44 | }, 45 | "@pixi/display": { 46 | "version": "5.1.1", 47 | "resolved": "https://registry.npmjs.org/@pixi/display/-/display-5.1.1.tgz", 48 | "integrity": "sha512-ncHqtV5Cx+kf4191sR74w248F+Y/O4CPH/kBt0GdNIotoLrZYETxEBfN94NWTV02182HKoe3jznv/VChbQO+Kw==", 49 | "requires": { 50 | "@pixi/math": "^5.1.0", 51 | "@pixi/settings": "^5.1.1", 52 | "@pixi/utils": "^5.1.1" 53 | } 54 | }, 55 | "@pixi/extract": { 56 | "version": "5.1.1", 57 | "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-5.1.1.tgz", 58 | "integrity": "sha512-9uQ+Ht2xX/xtbtO6YtKW5bmK078JfKb6atO0hH4I9p5/Sdh2qGJr44nk8R7fCvaKeCPh/xAImegwpYDVa8+BGA==", 59 | "requires": { 60 | "@pixi/core": "^5.1.1", 61 | "@pixi/math": "^5.1.0", 62 | "@pixi/utils": "^5.1.1" 63 | } 64 | }, 65 | "@pixi/filter-alpha": { 66 | "version": "5.1.1", 67 | "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-5.1.1.tgz", 68 | "integrity": "sha512-xcafkLNCMnmKLoC9+zMlLgqQoyAmYu+QoBbLx8oMWlAhvd7ajv/gIYm8C/xyAS+Lg2kSf+SZp5u5MCfNQL8YjA==", 69 | "requires": { 70 | "@pixi/core": "^5.1.1" 71 | } 72 | }, 73 | "@pixi/filter-blur": { 74 | "version": "5.1.1", 75 | "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-5.1.1.tgz", 76 | "integrity": "sha512-zVDJS7diN+ZbJtMzWD9mqfjo29XP4oo/EIqvFUVLExTCXaaLq/eDoRt77/VPaU0icrfVUOPPDHvCM5au20sgEA==", 77 | "requires": { 78 | "@pixi/core": "^5.1.1", 79 | "@pixi/settings": "^5.1.1" 80 | } 81 | }, 82 | "@pixi/filter-color-matrix": { 83 | "version": "5.1.1", 84 | "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-5.1.1.tgz", 85 | "integrity": "sha512-Rb+QSMvbs/TjrydSCwkQohXP14ej8Nl+WLcSnXGzDjnKPZc51xXEfkMqRNK1HJ/SWTTx/Py4MKRyCAW1P2bZeA==", 86 | "requires": { 87 | "@pixi/core": "^5.1.1" 88 | } 89 | }, 90 | "@pixi/filter-displacement": { 91 | "version": "5.1.1", 92 | "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-5.1.1.tgz", 93 | "integrity": "sha512-ymDV6WM544T3Fknihfu3c18qZ/d/PAumcdoZYpt5ry3x7N54g2ondSnTY4CE/VLXYUPmgCH77GEIs6d8KlyTRw==", 94 | "requires": { 95 | "@pixi/core": "^5.1.1", 96 | "@pixi/math": "^5.1.0" 97 | } 98 | }, 99 | "@pixi/filter-fxaa": { 100 | "version": "5.1.1", 101 | "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-5.1.1.tgz", 102 | "integrity": "sha512-r31BgusOlWER72kFWg0dWRQd2LlKMpnE5kBAd5NISkYf5f/dow2ZmekEnh8KEOE6oFut6Dh8BhTjZqcUCHVX+A==", 103 | "requires": { 104 | "@pixi/core": "^5.1.1" 105 | } 106 | }, 107 | "@pixi/filter-noise": { 108 | "version": "5.1.1", 109 | "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-5.1.1.tgz", 110 | "integrity": "sha512-zhondEfXgNHJJCANRNvB6Nne73jejsL/bU2wAasR/MOo0o7gtsuvig6ivRcRpsqneVkXxrJRYX4hORdtCDP22g==", 111 | "requires": { 112 | "@pixi/core": "^5.1.1" 113 | } 114 | }, 115 | "@pixi/graphics": { 116 | "version": "5.1.1", 117 | "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-5.1.1.tgz", 118 | "integrity": "sha512-iU/MJ7MLn4NRF/0QsGymdziolGeVxO8VfyPi69uC2YeSjsgY2SVrIRRNjdsjPlDceLGkUx2gNVa8IGU+Mosjpg==", 119 | "requires": { 120 | "@pixi/constants": "^5.1.0", 121 | "@pixi/core": "^5.1.1", 122 | "@pixi/display": "^5.1.1", 123 | "@pixi/math": "^5.1.0", 124 | "@pixi/sprite": "^5.1.1", 125 | "@pixi/utils": "^5.1.1" 126 | } 127 | }, 128 | "@pixi/interaction": { 129 | "version": "5.1.1", 130 | "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-5.1.1.tgz", 131 | "integrity": "sha512-7VvnXt/7dUlkNxqGkkh8lEgBPuJRz5h80sTMpz7Sq8os2hE6jDEwfNdlQdiOzUORc5gQyJju0wkwRg4yXpNQuQ==", 132 | "requires": { 133 | "@pixi/core": "^5.1.1", 134 | "@pixi/display": "^5.1.1", 135 | "@pixi/math": "^5.1.0", 136 | "@pixi/ticker": "^5.1.1", 137 | "@pixi/utils": "^5.1.1" 138 | } 139 | }, 140 | "@pixi/loaders": { 141 | "version": "5.1.1", 142 | "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-5.1.1.tgz", 143 | "integrity": "sha512-5kGv7wRBJRZwV6voRpOUo1fQs2ZqFrWHa3e12cMaetWENzZdYsosLuo03MHHvBFwoWg+htBLOEnvuUiF/4hKxg==", 144 | "requires": { 145 | "@pixi/core": "^5.1.1", 146 | "@pixi/utils": "^5.1.1", 147 | "resource-loader": "^3.0.1" 148 | } 149 | }, 150 | "@pixi/math": { 151 | "version": "5.1.0", 152 | "resolved": "https://registry.npmjs.org/@pixi/math/-/math-5.1.0.tgz", 153 | "integrity": "sha512-Vf9W4SgYRRQMdSq8tFViKKKGCU3iklf0RDzd+wzp4gezOxe3m0PLB7XKwvVrP1hRjUh49zIAL9JBpYREPS1EMw==" 154 | }, 155 | "@pixi/mesh": { 156 | "version": "5.1.1", 157 | "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-5.1.1.tgz", 158 | "integrity": "sha512-mCPZI184rSgatI44yGPK0X6oDJTqCGiZlwH4EOwmcbRd3osmzbV0PRZFKVzPZmS8QTlPfhyz1uWoBsZy+pJxmA==", 159 | "requires": { 160 | "@pixi/constants": "^5.1.0", 161 | "@pixi/core": "^5.1.1", 162 | "@pixi/display": "^5.1.1", 163 | "@pixi/math": "^5.1.0", 164 | "@pixi/settings": "^5.1.1", 165 | "@pixi/utils": "^5.1.1" 166 | } 167 | }, 168 | "@pixi/mesh-extras": { 169 | "version": "5.1.1", 170 | "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-5.1.1.tgz", 171 | "integrity": "sha512-q4vA/P1oD6MxL8+zSLlt0r1Id+RHP4+dDlv3BjPxURWX1ts1fDgvZSJ0L/ASD2dmmNTmo55uWEldOmABruM/4w==", 172 | "requires": { 173 | "@pixi/constants": "^5.1.0", 174 | "@pixi/core": "^5.1.1", 175 | "@pixi/math": "^5.1.0", 176 | "@pixi/mesh": "^5.1.1", 177 | "@pixi/utils": "^5.1.1" 178 | } 179 | }, 180 | "@pixi/mixin-cache-as-bitmap": { 181 | "version": "5.1.1", 182 | "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-5.1.1.tgz", 183 | "integrity": "sha512-GutRDQWE2Otrck77A2w+Ye+PswRSDnLJjQq/qiEoVeGAQKo9dU1gJSMK1G1FJHseWlWJ7v7Q3+S2IgX0dd3LEQ==", 184 | "requires": { 185 | "@pixi/core": "^5.1.1", 186 | "@pixi/display": "^5.1.1", 187 | "@pixi/math": "^5.1.0", 188 | "@pixi/settings": "^5.1.1", 189 | "@pixi/sprite": "^5.1.1", 190 | "@pixi/utils": "^5.1.1" 191 | } 192 | }, 193 | "@pixi/mixin-get-child-by-name": { 194 | "version": "5.1.1", 195 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-5.1.1.tgz", 196 | "integrity": "sha512-qngni3HosiAN7rjxZVQk01SqHOLsu6qZgvPi6JQDpolD8LqedjkL9c+vMVZWLJ8QkQ/fJJHJEx7zGNF4EONBIw==", 197 | "requires": { 198 | "@pixi/display": "^5.1.1" 199 | } 200 | }, 201 | "@pixi/mixin-get-global-position": { 202 | "version": "5.1.1", 203 | "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-5.1.1.tgz", 204 | "integrity": "sha512-qvGhCDdiq4apDNDr9E9el7d15JPD/pPSIWwI90sF9ljU/NRTQPH80GmFwF8+Z4XJry+B/6lm9tRMN0hlMGQGZg==", 205 | "requires": { 206 | "@pixi/display": "^5.1.1", 207 | "@pixi/math": "^5.1.0" 208 | } 209 | }, 210 | "@pixi/particles": { 211 | "version": "5.1.1", 212 | "resolved": "https://registry.npmjs.org/@pixi/particles/-/particles-5.1.1.tgz", 213 | "integrity": "sha512-VcllFyxeAKkpzLqhSs/WSSKgxdBVdvwX5Lk99KtUhL7LN2u6pThSbMHLe7iqGIdzj4wSwhzi9PrkmKrkFdJMaw==", 214 | "requires": { 215 | "@pixi/constants": "^5.1.0", 216 | "@pixi/core": "^5.1.1", 217 | "@pixi/display": "^5.1.1", 218 | "@pixi/math": "^5.1.0", 219 | "@pixi/utils": "^5.1.1" 220 | } 221 | }, 222 | "@pixi/polyfill": { 223 | "version": "5.1.0", 224 | "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-5.1.0.tgz", 225 | "integrity": "sha512-8M3nYCO0a599fsdLW7wv9SBYriMqS1QckKAkRuN2JualRuK/GjxZjm5Vcbcwc1gGONRUKZroH12CuPyTcU2HnQ==", 226 | "requires": { 227 | "es6-promise-polyfill": "^1.2.0", 228 | "object-assign": "^4.1.1" 229 | } 230 | }, 231 | "@pixi/prepare": { 232 | "version": "5.1.1", 233 | "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-5.1.1.tgz", 234 | "integrity": "sha512-QPtxtTtw9GGf6OSX25yYkAADq3mC8E8C9R0TrNIu/76kyV2DVxOMKACgy7TJfndkiQnMglME1RjPxT31TVqI/Q==", 235 | "requires": { 236 | "@pixi/core": "^5.1.1", 237 | "@pixi/display": "^5.1.1", 238 | "@pixi/graphics": "^5.1.1", 239 | "@pixi/settings": "^5.1.1", 240 | "@pixi/text": "^5.1.1", 241 | "@pixi/ticker": "^5.1.1" 242 | } 243 | }, 244 | "@pixi/runner": { 245 | "version": "5.1.1", 246 | "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-5.1.1.tgz", 247 | "integrity": "sha512-cOkWsRZlEgOB4IuiUW0PvU0JDMNpNTtyLeECg4DwIDYW4uQ0033zaZFSsN0EOeX0TFkpBmaJsgEIwpmw32VU0w==" 248 | }, 249 | "@pixi/settings": { 250 | "version": "5.1.1", 251 | "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-5.1.1.tgz", 252 | "integrity": "sha512-PyK/kofKwaCXFfi0mNi4FNwH/jQjBnfRi3kyoR3K7rTiWK4gyn/tuCbAGWxzYyutUos6JrYd4gPDcHHYboJO6g==", 253 | "requires": { 254 | "ismobilejs": "^0.5.1" 255 | } 256 | }, 257 | "@pixi/sprite": { 258 | "version": "5.1.1", 259 | "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-5.1.1.tgz", 260 | "integrity": "sha512-LEPNODkvMU/zSl8o1mU0KIRsMU6yaMNEEBPAEj7uQ6RGqcUNYQQ1pzOHEbOIjRdfUqJQVD1iC68PAUZ0V2fpSw==", 261 | "requires": { 262 | "@pixi/constants": "^5.1.0", 263 | "@pixi/core": "^5.1.1", 264 | "@pixi/display": "^5.1.1", 265 | "@pixi/math": "^5.1.0", 266 | "@pixi/settings": "^5.1.1", 267 | "@pixi/utils": "^5.1.1" 268 | } 269 | }, 270 | "@pixi/sprite-animated": { 271 | "version": "5.1.1", 272 | "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-5.1.1.tgz", 273 | "integrity": "sha512-X7XeywqokF7QYzvYA3lwi7Et+zAoZqMFnP4rcerdBRvhcS+Z8fdvx/ApXXPfTcObgoSV8urZ95Gu7btPkmed2w==", 274 | "requires": { 275 | "@pixi/core": "^5.1.1", 276 | "@pixi/sprite": "^5.1.1", 277 | "@pixi/ticker": "^5.1.1" 278 | } 279 | }, 280 | "@pixi/sprite-tiling": { 281 | "version": "5.1.1", 282 | "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-5.1.1.tgz", 283 | "integrity": "sha512-8M1NBFMalHtvhUvvFxb8Lzqf3pjLzCREI0pNlqgV7SfK18zyVsc20kN8tCeo6s9YZUfWpnWwxhArmeWkmaOntw==", 284 | "requires": { 285 | "@pixi/constants": "^5.1.0", 286 | "@pixi/core": "^5.1.1", 287 | "@pixi/display": "^5.1.1", 288 | "@pixi/math": "^5.1.0", 289 | "@pixi/sprite": "^5.1.1", 290 | "@pixi/utils": "^5.1.1" 291 | } 292 | }, 293 | "@pixi/spritesheet": { 294 | "version": "5.1.1", 295 | "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-5.1.1.tgz", 296 | "integrity": "sha512-mgq+fXUJJbO3DyqVumoUKefiWxPIGKTvQBmv+h2GRWeaXPUi/73glctA1qvHQXieAwkRTBUKvlABIq/o5qGSyg==", 297 | "requires": { 298 | "@pixi/core": "^5.1.1", 299 | "@pixi/loaders": "^5.1.1", 300 | "@pixi/math": "^5.1.0", 301 | "@pixi/utils": "^5.1.1" 302 | } 303 | }, 304 | "@pixi/text": { 305 | "version": "5.1.1", 306 | "resolved": "https://registry.npmjs.org/@pixi/text/-/text-5.1.1.tgz", 307 | "integrity": "sha512-llElEEpgYMvgqfchN5oT3M4vSYn77mXNHyCZena9M6mUpZ8VaYUxSSfMt6K5LLElnoQdW+f/t6XvbVh8ZMwk3A==", 308 | "requires": { 309 | "@pixi/core": "^5.1.1", 310 | "@pixi/math": "^5.1.0", 311 | "@pixi/settings": "^5.1.1", 312 | "@pixi/sprite": "^5.1.1", 313 | "@pixi/utils": "^5.1.1" 314 | } 315 | }, 316 | "@pixi/text-bitmap": { 317 | "version": "5.1.1", 318 | "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-5.1.1.tgz", 319 | "integrity": "sha512-UuKzL7fxh2Z2s+Iop2Qq9rg6zfqElRSjF+JB/cGpvoQCxTGMAmlqy54YZnm+lHdSjQ2qOQO0ulA5dMlFVEEPHg==", 320 | "requires": { 321 | "@pixi/core": "^5.1.1", 322 | "@pixi/display": "^5.1.1", 323 | "@pixi/loaders": "^5.1.1", 324 | "@pixi/math": "^5.1.0", 325 | "@pixi/settings": "^5.1.1", 326 | "@pixi/sprite": "^5.1.1", 327 | "@pixi/utils": "^5.1.1" 328 | } 329 | }, 330 | "@pixi/ticker": { 331 | "version": "5.1.1", 332 | "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-5.1.1.tgz", 333 | "integrity": "sha512-eUitqmwTZAialK7yXLwva/L+VHVix2mi2vvsOj5LxUo2wS32ftnjv4Etd+Gh2QCatX6P3fFowW7v2+jxjqIkPA==", 334 | "requires": { 335 | "@pixi/settings": "^5.1.1" 336 | } 337 | }, 338 | "@pixi/utils": { 339 | "version": "5.1.1", 340 | "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-5.1.1.tgz", 341 | "integrity": "sha512-43loetwAhyzhpqrIdekKLR/rAkNJqvs22jHZVUdPVCU/wYeD7Tkvbg/D36/x8zQ3f0DgF2vtKaO3QAcaVyHOlQ==", 342 | "requires": { 343 | "@pixi/constants": "^5.1.0", 344 | "@pixi/settings": "^5.1.1", 345 | "earcut": "^2.1.5", 346 | "eventemitter3": "^3.1.0", 347 | "url": "^0.11.0" 348 | } 349 | }, 350 | "d-path-parser": { 351 | "version": "1.0.0", 352 | "resolved": "https://registry.npmjs.org/d-path-parser/-/d-path-parser-1.0.0.tgz", 353 | "integrity": "sha1-h7+0G0TFWWK0775doxUCte6LCrc=" 354 | }, 355 | "earcut": { 356 | "version": "2.1.5", 357 | "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.5.tgz", 358 | "integrity": "sha512-QFWC7ywTVLtvRAJTVp8ugsuuGQ5mVqNmJ1cRYeLrSHgP3nycr2RHTJob9OtM0v8ujuoKN0NY1a93J/omeTL1PA==" 359 | }, 360 | "es6-promise-polyfill": { 361 | "version": "1.2.0", 362 | "resolved": "https://registry.npmjs.org/es6-promise-polyfill/-/es6-promise-polyfill-1.2.0.tgz", 363 | "integrity": "sha1-84kl8jyz4+jObNqP93T867sJDN4=" 364 | }, 365 | "eventemitter3": { 366 | "version": "3.1.2", 367 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", 368 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" 369 | }, 370 | "ismobilejs": { 371 | "version": "0.5.2", 372 | "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-0.5.2.tgz", 373 | "integrity": "sha512-ta9UdV60xVZk/ZafFtSFslQaE76SvNkcs1r73d2PVR21zVzx9xuYv9tNe4MxA1NN7WoeCc2RjGot3Bz1eHDx3Q==" 374 | }, 375 | "mini-signals": { 376 | "version": "1.2.0", 377 | "resolved": "https://registry.npmjs.org/mini-signals/-/mini-signals-1.2.0.tgz", 378 | "integrity": "sha1-RbCAE8X65RokqhqTXNMXye1yHXQ=" 379 | }, 380 | "object-assign": { 381 | "version": "4.1.1", 382 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 383 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 384 | }, 385 | "parse-uri": { 386 | "version": "1.0.0", 387 | "resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.0.tgz", 388 | "integrity": "sha1-KHLcwi8aeXrN4Vg9igrClVLdrCA=" 389 | }, 390 | "pixi.js": { 391 | "version": "5.1.1", 392 | "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-5.1.1.tgz", 393 | "integrity": "sha512-KfRTPxzTkCP19+quK68vPxjv0qcrgaHVKXjwKiA7uKVCZtfVcodfIlRtw048AsDRAKxQYrcipcg7RLXq2KGAzw==", 394 | "requires": { 395 | "@pixi/accessibility": "^5.1.1", 396 | "@pixi/app": "^5.1.1", 397 | "@pixi/constants": "^5.1.0", 398 | "@pixi/core": "^5.1.1", 399 | "@pixi/display": "^5.1.1", 400 | "@pixi/extract": "^5.1.1", 401 | "@pixi/filter-alpha": "^5.1.1", 402 | "@pixi/filter-blur": "^5.1.1", 403 | "@pixi/filter-color-matrix": "^5.1.1", 404 | "@pixi/filter-displacement": "^5.1.1", 405 | "@pixi/filter-fxaa": "^5.1.1", 406 | "@pixi/filter-noise": "^5.1.1", 407 | "@pixi/graphics": "^5.1.1", 408 | "@pixi/interaction": "^5.1.1", 409 | "@pixi/loaders": "^5.1.1", 410 | "@pixi/math": "^5.1.0", 411 | "@pixi/mesh": "^5.1.1", 412 | "@pixi/mesh-extras": "^5.1.1", 413 | "@pixi/mixin-cache-as-bitmap": "^5.1.1", 414 | "@pixi/mixin-get-child-by-name": "^5.1.1", 415 | "@pixi/mixin-get-global-position": "^5.1.1", 416 | "@pixi/particles": "^5.1.1", 417 | "@pixi/polyfill": "^5.1.0", 418 | "@pixi/prepare": "^5.1.1", 419 | "@pixi/runner": "^5.1.1", 420 | "@pixi/settings": "^5.1.1", 421 | "@pixi/sprite": "^5.1.1", 422 | "@pixi/sprite-animated": "^5.1.1", 423 | "@pixi/sprite-tiling": "^5.1.1", 424 | "@pixi/spritesheet": "^5.1.1", 425 | "@pixi/text": "^5.1.1", 426 | "@pixi/text-bitmap": "^5.1.1", 427 | "@pixi/ticker": "^5.1.1", 428 | "@pixi/utils": "^5.1.1" 429 | } 430 | }, 431 | "punycode": { 432 | "version": "1.3.2", 433 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 434 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 435 | }, 436 | "querystring": { 437 | "version": "0.2.0", 438 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 439 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 440 | }, 441 | "resource-loader": { 442 | "version": "3.0.1", 443 | "resolved": "https://registry.npmjs.org/resource-loader/-/resource-loader-3.0.1.tgz", 444 | "integrity": "sha512-fBuCRbEHdLCI1eglzQhUv9Rrdcmqkydr1r6uHE2cYHvRBrcLXeSmbE/qI/urFt8rPr/IGxir3BUwM5kUK8XoyA==", 445 | "requires": { 446 | "mini-signals": "^1.2.0", 447 | "parse-uri": "^1.0.0" 448 | } 449 | }, 450 | "tinycolor2": { 451 | "version": "1.4.1", 452 | "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", 453 | "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" 454 | }, 455 | "url": { 456 | "version": "0.11.0", 457 | "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", 458 | "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", 459 | "requires": { 460 | "punycode": "1.3.2", 461 | "querystring": "0.2.0" 462 | } 463 | } 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi5-svg", 3 | "version": "0.1.10", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/eXponenta/pixi5-svg.git" 7 | }, 8 | "description": "SVG support for PixiV5. Generate Graphics from SVG. Supported version => 5.1.1.", 9 | "module": "index.js", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "pixi", 15 | "svg", 16 | "pixiv5" 17 | ], 18 | "author": "eXponenta", 19 | "license": "MIT", 20 | "dependencies": { 21 | "d-path-parser": "^1.0.0", 22 | "pixi.js": "^5.1.1", 23 | "tinycolor2": "^1.4.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/svg.js: -------------------------------------------------------------------------------- 1 | import { SVGNode } from "./svgnode"; 2 | 3 | /** 4 | * @typedef {Object} DefaultOptions 5 | * @property {number} [lineWidth] default stroke thickness (must be greater or equal of 1) 6 | * @property {number} [lineColor] default stroke color 7 | * @property {number} [lineOpacity] default stroke opacity 8 | * @property {number} [fillColor] default fill color 9 | * @property {number} [fillOpacity] default fill opacity 10 | * @property {boolean} [unpackTree] unpack node tree, otherwise build single Graphics 11 | */ 12 | 13 | const DEFAULT = { 14 | unpackTree: false, 15 | lineColor: 0, 16 | lineOpacity: 1, 17 | fillColor: 0, 18 | fillOpacity: 1, 19 | lineWidth: 1 20 | }; 21 | 22 | export class SVG extends SVGNode { 23 | /** 24 | * Create Graphics from svg 25 | * @class 26 | * @public 27 | * @param {SVGElement | string} svg 28 | * @param {DefaultOptions} options 29 | */ 30 | constructor(svg, options = DEFAULT) { 31 | if (!(svg instanceof SVGElement)) { 32 | const container = document.createElement("div"); 33 | container.innerHTML = svg; 34 | 35 | //@ts-ignore 36 | svg = container.children[0]; 37 | if (!(svg instanceof SVGElement)) { 38 | throw new Error("invalid SVG!"); 39 | } 40 | } 41 | 42 | super(svg, Object.assign({}, DEFAULT, options || {})); 43 | 44 | //@ts-ignore 45 | this.parseChildren(svg.children); 46 | this.type = "svg"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/svggroup.js: -------------------------------------------------------------------------------- 1 | import { SVGNode } from "./svgnode"; 2 | import * as PIXI from "pixi.js"; 3 | 4 | export class SVGGroup extends PIXI.Container { 5 | /** 6 | * Create Container from svg subnode of 'g' 7 | * @class 8 | * @public 9 | * @param {SVGElement} svg 10 | */ 11 | constructor(svg, options) { 12 | super(); 13 | this.options = options; 14 | this.dataNode = svg; 15 | this.type = svg.nodeName.toLowerCase(); 16 | this.name = svg.getAttribute("id") || ""; 17 | } 18 | 19 | fillShapes(style, matrix) {} 20 | 21 | parseChildren(...args) { 22 | SVGNode.prototype.parseChildren.call(this, ...args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/svgnode.js: -------------------------------------------------------------------------------- 1 | import tcolor from "tinycolor2"; 2 | import dPathParse from "d-path-parser"; 3 | 4 | import * as PIXI from "pixi.js"; 5 | 6 | import { SVG } from "./svg"; 7 | import { SVGGroup } from "./svggroup"; 8 | import { parseSvgStyle, parseSvgTransform, arcToBezier } from "./utils"; 9 | 10 | const EPS = 0.0001; 11 | const tmpPoint = new PIXI.Point(); 12 | 13 | /** 14 | * @typedef {Object} DefaultOptions 15 | * @property {number} [lineWidth] default stroke thickness (must be greater or equal of 1) 16 | * @property {number} [lineColor] default stroke color 17 | * @property {number} [lineOpacity] default stroke opacity 18 | * @property {number} [fillColor] default fill color 19 | * @property {number} [fillOpacity] default fill opacity 20 | * @property {boolean} [unpackTree] unpack node tree, otherwise build single Graphics 21 | */ 22 | 23 | export class SVGNode extends PIXI.Graphics { 24 | /** 25 | * Create Graphics from svg subnode 26 | * @class 27 | * @public 28 | * @param {SVGElement} svg 29 | * @param {DefaultOptions} options 30 | */ 31 | constructor(svg, options) { 32 | super(); 33 | this.options = options; 34 | this.dataNode = svg; 35 | this.type = svg.nodeName.toLowerCase(); 36 | } 37 | 38 | /** 39 | * Get `GraphicsData` under cursor if available. Similar as `containsPoint`, but return internal `GraphicsData` 40 | * @public 41 | * @method SVG#pickGraphicsData 42 | * @param {PIXI.Point} point - global point for intersection checking 43 | * @param {boolean} all - Include all intersected, otherwise first selected if exist 44 | * @return {Array} list of selected GraphicsData, can be empty or grater that 1 45 | */ 46 | pickGraphicsData(point, all) { 47 | let picked = []; 48 | 49 | point = this.worldTransform.applyInverse(point); 50 | 51 | //@ts-ignore 52 | const graphicsData = this.geometry.graphicsData; 53 | 54 | for (let i = 0; i < graphicsData.length; ++i) { 55 | const data = graphicsData[i]; 56 | 57 | if (!data.fillStyle.visible || !data.shape) { 58 | continue; 59 | } 60 | if (data.matrix) { 61 | data.matrix.applyInverse(point, tmpPoint); 62 | } else { 63 | tmpPoint.copyFrom(point); 64 | } 65 | 66 | if (data.shape.contains(tmpPoint.x, tmpPoint.y)) { 67 | let skip = false; 68 | if (data.holes) { 69 | for (let i = 0; i < data.holes.length; i++) { 70 | const hole = data.holes[i]; 71 | if (hole.shape.contains(tmpPoint.x, tmpPoint.y)) { 72 | skip = true; 73 | break; 74 | } 75 | } 76 | } 77 | 78 | if (!skip) { 79 | if (!all) { 80 | return [data]; 81 | } else { 82 | picked.push(data); 83 | } 84 | } 85 | } 86 | } 87 | 88 | return picked; 89 | } 90 | 91 | /** 92 | * Create a PIXI Graphic from SVG elements 93 | * @private 94 | * @method SVG#svgChildren 95 | * @param {Array<*>} children - Collection of SVG nodes 96 | * @param {*} [parentStyle=undefined] Whether to inherit fill settings. 97 | * @param {PIXI.Matrix} [parentMatrix=undefined] Matrix fro transformations 98 | */ 99 | parseChildren(children, parentStyle, parentMatrix) { 100 | for (let i = 0; i < children.length; i++) { 101 | const child = children[i]; 102 | 103 | const nodeStyle = parseSvgStyle(child); 104 | const matrix = parseSvgTransform(child); 105 | const nodeType = child.nodeName.toLowerCase(); 106 | 107 | /** 108 | * @type {SVG | SVGNode} 109 | */ 110 | let shape = this; 111 | 112 | if (this.options.unpackTree) { 113 | //@ts-ignore 114 | shape = nodeType === "g" ? new SVGGroup(child, this.options) : new SVGNode(child, this.options); 115 | } 116 | 117 | //compile full style inherited from all parents 118 | const fullStyle = Object.assign({}, parentStyle || {}, nodeStyle); 119 | 120 | shape.fillShapes(fullStyle, matrix); 121 | 122 | switch (nodeType) { 123 | case "path": { 124 | shape.svgPath(child); 125 | break; 126 | } 127 | case "line": { 128 | this.svgLine(child); 129 | break; 130 | } 131 | case "circle": 132 | case "ellipse": { 133 | shape.svgCircle(child); 134 | break; 135 | } 136 | case "rect": { 137 | shape.svgRect(child); 138 | break; 139 | } 140 | case "polygon": { 141 | shape.svgPoly(child, true); 142 | break; 143 | } 144 | case "polyline": { 145 | shape.svgPoly(child, false); 146 | break; 147 | } 148 | case "g": { 149 | break; 150 | } 151 | default: { 152 | // @if DEBUG 153 | console.info("[SVGUtils] <%s> elements unsupported", child.nodeName); 154 | // @endif 155 | break; 156 | } 157 | } 158 | 159 | shape.parseChildren(child.children, fullStyle, matrix); 160 | if (this.options.unpackTree) { 161 | shape.name = child.getAttribute("id") || "child_" + i; 162 | this.addChild(shape); 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Convert the Hexidecimal string (e.g., "#fff") to uint 169 | * @private 170 | * @method SVG#hexToUint 171 | */ 172 | hexToUint(hex) { 173 | if (hex === undefined || hex === null) return; 174 | 175 | if (hex[0] === "#") { 176 | // Remove the hash 177 | hex = hex.substr(1); 178 | 179 | // Convert shortcolors fc9 to ffcc99 180 | if (hex.length === 3) { 181 | hex = hex.replace(/([a-f0-9])/gi, "$1$1"); 182 | } 183 | return parseInt(hex, 16); 184 | } else { 185 | const rgb = tcolor(hex).toRgb(); 186 | 187 | return (rgb.r << 16) + (rgb.g << 8) + rgb.b; 188 | } 189 | } 190 | 191 | /** 192 | * Render a element 193 | * @private 194 | * @method SVG#svgLine 195 | * @param {SVGCircleElement} node 196 | */ 197 | svgLine(node) { 198 | const x1 = parseFloat(node.getAttribute("x1")); 199 | const y1 = parseFloat(node.getAttribute("y1")); 200 | const x2 = parseFloat(node.getAttribute("x2")); 201 | const y2 = parseFloat(node.getAttribute("y2")); 202 | 203 | //idiot chek 204 | if (Math.abs(x1 - x2) + Math.abs(y1 - y2) <= EPS) return; 205 | 206 | this.moveTo(x1, y1); 207 | this.lineTo(x2, y2); 208 | } 209 | 210 | /** 211 | * Render a element or element 212 | * @private 213 | * @method SVG#svgCircle 214 | * @param {SVGCircleElement} node 215 | */ 216 | svgCircle(node) { 217 | let heightProp = "r"; 218 | let widthProp = "r"; 219 | const isEllipse = node.nodeName === "ellipse"; 220 | if (isEllipse) { 221 | heightProp += "y"; 222 | widthProp += "x"; 223 | } 224 | const width = parseFloat(node.getAttribute(widthProp)); 225 | const height = parseFloat(node.getAttribute(heightProp)); 226 | const cx = node.getAttribute("cx") || "0"; 227 | const cy = node.getAttribute("cy") || "0"; 228 | let x = 0; 229 | let y = 0; 230 | if (cx !== null) { 231 | x = parseFloat(cx); 232 | } 233 | if (cy !== null) { 234 | y = parseFloat(cy); 235 | } 236 | if (!isEllipse) { 237 | this.drawCircle(x, y, width); 238 | } else { 239 | this.drawEllipse(x, y, width, height); 240 | } 241 | } 242 | 243 | /** 244 | * Render a element 245 | * @private 246 | * @method SVG#svgRect 247 | * @param {SVGRectElement} node 248 | */ 249 | svgRect(node) { 250 | const x = parseFloat(node.getAttribute("x")) || 0; 251 | const y = parseFloat(node.getAttribute("y")) || 0; 252 | const width = parseFloat(node.getAttribute("width")); 253 | const height = parseFloat(node.getAttribute("height")); 254 | const rx = parseFloat(node.getAttribute("rx")); 255 | if (rx) { 256 | this.drawRoundedRect(x, y, width, height, rx); 257 | } else { 258 | this.drawRect(x, y, width, height); 259 | } 260 | } 261 | 262 | /** 263 | * Render a polyline element. 264 | * @private 265 | * @method SVG#svgPoly 266 | * @param {SVGPolylineElement} node 267 | */ 268 | svgPoly(node, close) { 269 | const pointsAttr = node.getAttribute("points"); 270 | const pointsRaw = pointsAttr.split(/[ ,]/g); 271 | const points = pointsRaw.reduce((acc, p) => (p && acc.push(parseFloat(p)), acc), []); 272 | this.drawPolygon(points); 273 | if (!close) { 274 | //@ts-ignore 275 | const gd = this.geometry.graphicsData; 276 | //@ts-ignore 277 | gd[gd.length - 1].shape.closeStroke = false; 278 | } 279 | } 280 | 281 | /** 282 | * Set the fill and stroke style. 283 | * @private 284 | * @method SVG#fillShapes 285 | * @param {*} style 286 | * @param {PIXI.Matrix} matrix 287 | */ 288 | fillShapes(style, matrix) { 289 | const { fill, opacity, stroke, strokeWidth, strokeOpacity, fillOpacity } = style; 290 | 291 | const isStrokable = stroke !== undefined && stroke !== "none" && stroke !== "transparent"; 292 | const isFillable = fill !== undefined && fill !== "none" && fill !== "transparent"; 293 | 294 | const defaultLineWidth = isStrokable ? this.options.lineWidth || 1 : 0; 295 | const lineWidth = strokeWidth !== undefined ? Math.max(0.5, parseFloat(strokeWidth)) : defaultLineWidth; 296 | const lineColor = isStrokable ? this.hexToUint(stroke) : this.options.lineColor; 297 | 298 | let strokeOpacityValue = 0; 299 | let fillOpacityValue = 0; 300 | 301 | if (isStrokable) { 302 | strokeOpacityValue = 303 | opacity || strokeOpacity ? parseFloat(opacity || strokeOpacity) : this.options.lineOpacity; 304 | } 305 | if (isFillable) { 306 | fillOpacityValue = opacity || fillOpacity ? parseFloat(opacity || fillOpacity) : this.options.fillOpacity; 307 | } 308 | 309 | if (fill) { 310 | if (!isFillable) { 311 | this.beginFill(0, 0); 312 | } else { 313 | this.beginFill(this.hexToUint(fill), fillOpacityValue); 314 | } 315 | } else { 316 | this.beginFill(this.options.fillColor, 1); 317 | } 318 | 319 | this.lineStyle(lineWidth, lineColor, strokeOpacityValue); 320 | this.setMatrix(matrix); 321 | } 322 | 323 | /** 324 | * Render a d element 325 | * @method SVG#svgPath 326 | * @param {SVGPathElement} node 327 | */ 328 | svgPath(node) { 329 | const d = node.getAttribute("d"); 330 | let x = 0, 331 | y = 0; 332 | let iX = 0, 333 | iY = 0; 334 | const commands = dPathParse(d); 335 | let prevCommand = undefined; 336 | 337 | for (var i = 0; i < commands.length; i++) { 338 | const command = commands[i]; 339 | 340 | switch (command.code) { 341 | case "m": { 342 | this.moveTo((x += command.end.x), (y += command.end.y)); 343 | (iX = x), (iY = y); 344 | break; 345 | } 346 | case "M": { 347 | this.moveTo((x = command.end.x), (y = command.end.y)); 348 | (iX = x), (iY = y); 349 | break; 350 | } 351 | case "H": { 352 | this.lineTo((x = command.value), y); 353 | break; 354 | } 355 | case "h": { 356 | this.lineTo((x += command.value), y); 357 | break; 358 | } 359 | case "V": { 360 | this.lineTo(x, (y = command.value)); 361 | break; 362 | } 363 | case "v": { 364 | this.lineTo(x, (y += command.value)); 365 | break; 366 | } 367 | case "Z": 368 | case "z": { 369 | //jump corete to first point 370 | (x = iX), (y = iY); 371 | this.closePath(); 372 | break; 373 | } 374 | case "L": { 375 | const { x: nx, y: ny } = command.end; 376 | 377 | if (Math.abs(x - nx) + Math.abs(y - ny) <= EPS) { 378 | (x = nx), (y = ny); 379 | break; 380 | } 381 | 382 | this.lineTo((x = nx), (y = ny)); 383 | break; 384 | } 385 | case "l": { 386 | const { x: dx, y: dy } = command.end; 387 | 388 | if (Math.abs(dx) + Math.abs(dy) <= EPS) { 389 | (x += dx), (y += dy); 390 | break; 391 | } 392 | 393 | this.lineTo((x += dx), (y += dy)); 394 | break; 395 | } 396 | //short C, selet cp1 from last command 397 | case "S": { 398 | let cp1 = { x, y }; 399 | let cp2 = command.cp; 400 | 401 | //S is compute points from old points 402 | if (prevCommand.code == "S" || prevCommand.code == "C") { 403 | const lc = prevCommand.cp2 || prevCommand.cp; 404 | cp1.x = 2 * prevCommand.end.x - lc.x; 405 | cp1.y = 2 * prevCommand.end.y - lc.y; 406 | } else { 407 | cp1 = cp2; 408 | } 409 | 410 | this.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, (x = command.end.x), (y = command.end.y)); 411 | break; 412 | } 413 | case "C": { 414 | const cp1 = command.cp1; 415 | const cp2 = command.cp2; 416 | 417 | this.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, (x = command.end.x), (y = command.end.y)); 418 | break; 419 | } 420 | //diff!! 421 | //short C, select cp1 from last command 422 | case "s": { 423 | const currX = x; 424 | const currY = y; 425 | 426 | let cp1 = { x, y }; 427 | let cp2 = command.cp; 428 | 429 | //S is compute points from old points 430 | if (prevCommand.code == "s" || prevCommand.code == "c") { 431 | const lc = prevCommand.cp2 || prevCommand.cp; 432 | cp1.x = prevCommand.end.x - lc.x; 433 | cp1.y = prevCommand.end.y - lc.y; 434 | } else { 435 | this.quadraticCurveTo(currX + cp2.x, currY + cp2.y, (x += command.end.x), (y += command.end.y)); 436 | break; 437 | } 438 | 439 | this.bezierCurveTo( 440 | currX + cp1.x, 441 | currY + cp1.y, 442 | currX + cp2.x, 443 | currY + cp2.y, 444 | (x += command.end.x), 445 | (y += command.end.y) 446 | ); 447 | break; 448 | } 449 | case "c": { 450 | const currX = x; 451 | const currY = y; 452 | const cp1 = command.cp1; 453 | const cp2 = command.cp2; 454 | 455 | this.bezierCurveTo( 456 | currX + cp1.x, 457 | currY + cp1.y, 458 | currX + cp2.x, 459 | currY + cp2.y, 460 | (x += command.end.x), 461 | (y += command.end.y) 462 | ); 463 | break; 464 | } 465 | case "t": { 466 | let cp = command.cp || { x, y }; 467 | let prevCp = { x, y }; 468 | 469 | if (prevCommand.code != "t" || prevCommand.code != "q") { 470 | prevCp = prevCommand.cp || prevCommand.cp2 || prevCommand.end; 471 | cp.x = prevCommand.end.x - prevCp.x; 472 | cp.y = prevCommand.end.y - prevCp.y; 473 | } else { 474 | this.lineTo((x += command.end.x), (y += command.end.y)); 475 | break; 476 | } 477 | 478 | const currX = x; 479 | const currY = y; 480 | 481 | this.quadraticCurveTo(currX + cp.x, currY + cp.y, (x += command.end.x), (y += command.end.y)); 482 | break; 483 | } 484 | case "q": { 485 | const currX = x; 486 | const currY = y; 487 | 488 | this.quadraticCurveTo( 489 | currX + command.cp.x, 490 | currY + command.cp.y, 491 | (x += command.end.x), 492 | (y += command.end.y) 493 | ); 494 | break; 495 | } 496 | 497 | case "T": { 498 | let cp = command.cp || { x, y }; 499 | let prevCp = { x, y }; 500 | 501 | if (prevCommand.code != "T" || prevCommand.code != "Q") { 502 | prevCp = prevCommand.cp || prevCommand.cp2 || prevCommand.end; 503 | cp.x = 2 * prevCommand.end.x - prevCp.x; 504 | cp.y = 2 * prevCommand.end.y - prevCp.y; 505 | } else { 506 | this.lineTo((x = command.end.x), (y = command.end.y)); 507 | break; 508 | } 509 | 510 | this.quadraticCurveTo(cp.x, cp.y, (x = command.end.x), (y = command.end.y)); 511 | break; 512 | } 513 | 514 | case "Q": { 515 | let cp = command.cp; 516 | this.quadraticCurveTo(cp.x, cp.y, (x = command.end.x), (y = command.end.y)); 517 | break; 518 | } 519 | 520 | //arc as bezier 521 | case "a": 522 | case "A": { 523 | const currX = x; 524 | const currY = y; 525 | 526 | if (command.relative) { 527 | x += command.end.x; 528 | y += command.end.y; 529 | } else { 530 | x = command.end.x; 531 | y = command.end.y; 532 | } 533 | const beziers = arcToBezier({ 534 | x1: currX, 535 | y1: currY, 536 | rx: command.radii.x, 537 | ry: command.radii.y, 538 | x2: x, 539 | y2: y, 540 | phi: command.rotation, 541 | fa: command.large, 542 | fs: command.clockwise 543 | }); 544 | for (let b of beziers) { 545 | this.bezierCurveTo(b[2], b[3], b[4], b[5], b[6], b[7]); 546 | } 547 | break; 548 | } 549 | default: { 550 | console.info("[SVGUtils] Draw command not supported:", command.code, command); 551 | } 552 | } 553 | 554 | //save previous command fro C S and Q 555 | prevCommand = command; 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Get the style property and parse options. 4 | * @param {SVGElement} node 5 | * @return {Object} Style attributes 6 | */ 7 | export function parseSvgStyle(node) { 8 | const style = node.getAttribute("style"); 9 | const result = { 10 | fill: node.getAttribute("fill"), 11 | opacity: node.getAttribute("opacity"), 12 | fillOpacity: node.getAttribute("fill-opacity"), 13 | stroke: node.getAttribute("stroke"), 14 | strokeOpacity: node.getAttribute("stroke-opacity"), 15 | strokeWidth: node.getAttribute("stroke-width") 16 | }; 17 | if (style !== null) { 18 | style.split(";").forEach(prop => { 19 | if (prop) { 20 | const [name, value] = prop.split(":"); 21 | if (name && value) { 22 | result[name.trim()] = value.trim(); 23 | } 24 | } 25 | }); 26 | if (result["stroke-width"]) { 27 | result.strokeWidth = result["stroke-width"]; 28 | delete result["stroke-width"]; 29 | } 30 | } 31 | 32 | for (let key in result) { 33 | if (result[key] === null) { 34 | delete result[key]; 35 | } 36 | } 37 | return result; 38 | } 39 | 40 | /** 41 | * Parse transform attribute 42 | * @param {SVGElement} node 43 | */ 44 | export function parseSvgTransform(node) { 45 | if (!node.getAttribute("transform")) { 46 | return undefined; 47 | } 48 | 49 | const matrix = new PIXI.Matrix(); 50 | const transformAttr = node.getAttribute("transform"); 51 | const commands = parseTransform(transformAttr); 52 | 53 | //apply transform matrix right to left 54 | for (let key = commands.length - 1; key >= 0; --key) { 55 | let command = commands[key].command; 56 | let values = commands[key].params; 57 | 58 | switch (command) { 59 | case "matrix": { 60 | matrix.a = parseScientific(values[0]); 61 | matrix.b = parseScientific(values[1]); 62 | matrix.c = parseScientific(values[2]); 63 | matrix.d = parseScientific(values[3]); 64 | matrix.tx = parseScientific(values[4]); 65 | matrix.ty = parseScientific(values[5]); 66 | 67 | return matrix; 68 | } 69 | case "translate": { 70 | const dx = parseScientific(values[0]); 71 | const dy = parseScientific(values[1]) || 0; 72 | matrix.translate(dx, dy); 73 | break; 74 | } 75 | case "scale": { 76 | const sx = parseScientific(values[0]); 77 | const sy = values.length > 1 ? parseScientific(values[1]) : sx; 78 | matrix.scale(sx, sy); 79 | break; 80 | } 81 | case "rotate": { 82 | let dx = 0; 83 | let dy = 0; 84 | 85 | if (values.length > 1) { 86 | dx = parseScientific(values[1]); 87 | dy = parseScientific(values[2]); 88 | } 89 | 90 | matrix 91 | .translate(-dx, -dy) 92 | .rotate((parseScientific(values[0]) * Math.PI) / 180) 93 | .translate(dx, dy); 94 | 95 | break; 96 | } 97 | default: { 98 | console.log(`Command ${command} can't implement yet`); 99 | } 100 | } 101 | } 102 | 103 | return matrix; 104 | } 105 | 106 | 107 | export function parseScientific(numberString) { 108 | var info = /([\d\\.]+)e-(\d+)/i.exec(numberString); 109 | if (!info) { 110 | return parseFloat(numberString); 111 | } 112 | 113 | var num = info[1].replace(".", ""), 114 | numDecs = info[2] - 1; 115 | var output = "0."; 116 | for (var i = 0; i < numDecs; i++) { 117 | output += "0"; 118 | } 119 | output += num; 120 | return parseFloat(output); 121 | } 122 | 123 | export function splitAttributeParams(attr) { 124 | if (attr.indexOf(",") >= 0) { 125 | return attr.split(","); 126 | } else { 127 | //Especially in IE Edge, the parameters do not have to be split by commas, IE even replaces commas with spaces! 128 | return attr.split(" "); 129 | } 130 | } 131 | 132 | /** 133 | * Parse transform attribute 134 | * @param {string} transform 135 | */ 136 | export function parseTransform(transform) 137 | { 138 | transform = transform.replace(/\ /gi, ","); 139 | let token = transform; 140 | let math = token.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g); 141 | let result = []; 142 | for (let i in math ) 143 | { 144 | let c = math[i].match(/[\w\.\-]+/g); 145 | result.push({ 146 | command : c.shift(), 147 | params : c 148 | }); 149 | } 150 | return result; 151 | } 152 | 153 | var TAU = Math.PI * 2; 154 | 155 | /* eslint-disable space-infix-ops */ 156 | 157 | // Calculate an angle between two unit vectors 158 | // 159 | // Since we measure angle between radii of circular arcs, 160 | // we can use simplified math (without length normalization) 161 | // 162 | function unit_vector_angle(ux, uy, vx, vy) { 163 | var sign = ux * vy - uy * vx < 0 ? -1 : 1; 164 | var dot = ux * vx + uy * vy; 165 | 166 | // Add this to work with arbitrary vectors: 167 | // dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy); 168 | 169 | // rounding errors, e.g. -1.0000000000000002 can screw up this 170 | if (dot > 1.0) { 171 | dot = 1.0; 172 | } 173 | if (dot < -1.0) { 174 | dot = -1.0; 175 | } 176 | 177 | return sign * Math.acos(dot); 178 | } 179 | 180 | // Convert from endpoint to center parameterization, 181 | // see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes 182 | // 183 | // Return [cx, cy, theta1, delta_theta] 184 | // 185 | function get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi) { 186 | // Step 1. 187 | // 188 | // Moving an ellipse so origin will be the middlepoint between our two 189 | // points. After that, rotate it to line up ellipse axes with coordinate 190 | // axes. 191 | // 192 | var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2; 193 | var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2; 194 | 195 | var rx_sq = rx * rx; 196 | var ry_sq = ry * ry; 197 | var x1p_sq = x1p * x1p; 198 | var y1p_sq = y1p * y1p; 199 | 200 | // Step 2. 201 | // 202 | // Compute coordinates of the centre of this ellipse (cx', cy') 203 | // in the new coordinate system. 204 | // 205 | var radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq; 206 | 207 | if (radicant < 0) { 208 | // due to rounding errors it might be e.g. -1.3877787807814457e-17 209 | radicant = 0; 210 | } 211 | 212 | radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq; 213 | radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1); 214 | 215 | var cxp = ((radicant * rx) / ry) * y1p; 216 | var cyp = ((radicant * -ry) / rx) * x1p; 217 | 218 | // Step 3. 219 | // 220 | // Transform back to get centre coordinates (cx, cy) in the original 221 | // coordinate system. 222 | // 223 | var cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2; 224 | var cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2; 225 | 226 | // Step 4. 227 | // 228 | // Compute angles (theta1, delta_theta). 229 | // 230 | var v1x = (x1p - cxp) / rx; 231 | var v1y = (y1p - cyp) / ry; 232 | var v2x = (-x1p - cxp) / rx; 233 | var v2y = (-y1p - cyp) / ry; 234 | 235 | var theta1 = unit_vector_angle(1, 0, v1x, v1y); 236 | var delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y); 237 | 238 | if (fs === 0 && delta_theta > 0) { 239 | delta_theta -= TAU; 240 | } 241 | if (fs === 1 && delta_theta < 0) { 242 | delta_theta += TAU; 243 | } 244 | 245 | return [cx, cy, theta1, delta_theta]; 246 | } 247 | 248 | // 249 | // Approximate one unit arc segment with bézier curves, 250 | // see http://math.stackexchange.com/questions/873224 251 | // 252 | function approximate_unit_arc(theta1, delta_theta) { 253 | var alpha = (4 / 3) * Math.tan(delta_theta / 4); 254 | 255 | var x1 = Math.cos(theta1); 256 | var y1 = Math.sin(theta1); 257 | var x2 = Math.cos(theta1 + delta_theta); 258 | var y2 = Math.sin(theta1 + delta_theta); 259 | 260 | return [x1, y1, x1 - y1 * alpha, y1 + x1 * alpha, x2 + y2 * alpha, y2 - x2 * alpha, x2, y2]; 261 | } 262 | 263 | export function arcToBezier({ x1, y1, x2, y2, fa, fs, rx, ry, phi }) { 264 | var sin_phi = Math.sin((phi * TAU) / 360); 265 | var cos_phi = Math.cos((phi * TAU) / 360); 266 | 267 | // Make sure radii are valid 268 | // 269 | var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2; 270 | var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2; 271 | 272 | if (x1p === 0 && y1p === 0) { 273 | // we're asked to draw line to itself 274 | return []; 275 | } 276 | 277 | if (rx === 0 || ry === 0) { 278 | // one of the radii is zero 279 | return []; 280 | } 281 | 282 | // Compensate out-of-range radii 283 | // 284 | rx = Math.abs(rx); 285 | ry = Math.abs(ry); 286 | 287 | var lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); 288 | if (lambda > 1) { 289 | rx *= Math.sqrt(lambda); 290 | ry *= Math.sqrt(lambda); 291 | } 292 | 293 | // Get center parameters (cx, cy, theta1, delta_theta) 294 | // 295 | var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi); 296 | 297 | var result = []; 298 | var theta1 = cc[2]; 299 | var delta_theta = cc[3]; 300 | 301 | // Split an arc to multiple segments, so each segment 302 | // will be less than τ/4 (= 90°) 303 | // 304 | var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1); 305 | delta_theta /= segments; 306 | 307 | for (var i = 0; i < segments; i++) { 308 | result.push(approximate_unit_arc(theta1, delta_theta)); 309 | theta1 += delta_theta; 310 | } 311 | 312 | // We have a bezier approximation of a unit circle, 313 | // now need to transform back to the original ellipse 314 | // 315 | return result.map(function(curve) { 316 | for (var i = 0; i < curve.length; i += 2) { 317 | var x = curve[i + 0]; 318 | var y = curve[i + 1]; 319 | 320 | // scale 321 | x *= rx; 322 | y *= ry; 323 | 324 | // rotate 325 | var xp = cos_phi * x - sin_phi * y; 326 | var yp = sin_phi * x + cos_phi * y; 327 | 328 | // translate 329 | curve[i + 0] = xp + cc[0]; 330 | curve[i + 1] = yp + cc[1]; 331 | } 332 | 333 | return curve; 334 | }); 335 | } 336 | --------------------------------------------------------------------------------