├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── assets ├── delayed.png ├── oneByOne.png ├── script_custom.png ├── script_default.png ├── sync.png └── timelines.svg ├── bower.json ├── dist ├── vivus.js └── vivus.min.js ├── hacks.md ├── index.html ├── package-lock.json ├── package.json ├── readme.md ├── src ├── _build.js ├── pathformer.js └── vivus.js └── test ├── karma.conf.js ├── manual ├── hi-there.svg ├── index.html ├── obturateur.svg ├── polaroid.svg └── synth.svg ├── unit.setup.js └── unit ├── pathformer.spec.js └── vivus.spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": false, 4 | "amd": false, 5 | "browser": true, 6 | "jasmine": true, 7 | "node": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "globals": { 14 | "Pathformer": true, 15 | "Vivus": true, 16 | "window": true, 17 | "document": true, 18 | "define": true, 19 | "jasmine": true, 20 | "it": true, 21 | "expect": true, 22 | "describe": true, 23 | "beforeEach": true, 24 | "afterEach": true, 25 | "spyOn": true 26 | }, 27 | "rules": { 28 | "no-cond-assign": 2, 29 | "no-console": 0, 30 | "no-const-assign": 2, 31 | "no-class-assign": 2, 32 | "no-this-before-super": 2, 33 | "no-unused-vars": 1, 34 | "no-empty": 0, 35 | "object-shorthand": [2, "always"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Please define your problem or issue as clear as possible. 2 | 3 | For a problem, please fill the following: 4 | 5 | **Vivus version**: 6 | 7 | **Browser**: 8 | 9 | **Steps to reproduce it**: 10 | 11 | **JSFiddle link (or similar platform)**: 12 | *No personal website will be allowed, only sandboxed platform where the code is isolated, clear and can be hacked. I don't want to debug uglified code between 42 libraries.* 13 | 14 | *[note]* 15 | Please have a minimum of politeness. There's unfortunately only me as contributor/maintainor which help on my free time, I'm not the Amazon customer service or your Mom. I don't ask to send me flowers and tell me how amazing Vivus is (that won't make me help you quicker). But please try to do as much as you can before opening an issue: check that no closed issue mention a similar problem, that your script is executed correctly (conflicts, race conditions...).. Thanks :) 16 | *[/note]* 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .DS_Store 4 | .idea 5 | yarn.lock 6 | dist/vivus.min.js.map 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | assets 3 | node_modules 4 | coverage 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | script: npm run lint && npm run test 5 | before_install: 6 | - export CHROME_BIN=chromium-browser 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thanks for contributing, thinking about contributing, or even arriving here by mistake. 4 | 5 | Issues are reserved to mention a problem, a bug or discuss about implementing a feature. If you have any question or support request, please use [Gitter](https://gitter.im/maxwellito/vivus). For every issue, please try to give as much information as you can : version, steps to recreate it, examples (via jsFiddle or something like that). 6 | 7 | About pull requests, please try to contact the maintainer beforehand. He's a kinda human Grumpy Cat trying to avoid features which can be useful for only 1% of users. The warning is only because it would be sad to see contributors spending time a feature that wouldn't be merged. Otherwise, I would recommend you to add a section in the `hacks.md` file. But if it's a bug fix, the chances to be merged are higher. If necessary please think about updating the tests. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) maxwellito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /assets/delayed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellito/vivus/06b7adb5d543b6b1d0d93fa83dea137b1f8644a5/assets/delayed.png -------------------------------------------------------------------------------- /assets/oneByOne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellito/vivus/06b7adb5d543b6b1d0d93fa83dea137b1f8644a5/assets/oneByOne.png -------------------------------------------------------------------------------- /assets/script_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellito/vivus/06b7adb5d543b6b1d0d93fa83dea137b1f8644a5/assets/script_custom.png -------------------------------------------------------------------------------- /assets/script_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellito/vivus/06b7adb5d543b6b1d0d93fa83dea137b1f8644a5/assets/script_default.png -------------------------------------------------------------------------------- /assets/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellito/vivus/06b7adb5d543b6b1d0d93fa83dea137b1f8644a5/assets/sync.png -------------------------------------------------------------------------------- /assets/timelines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | time 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 48 | 50 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vivus", 3 | "description": "JavaScript library to make drawing animation on SVG", 4 | "main": "dist/vivus.js", 5 | "licence": "MIT", 6 | "ignore": [ 7 | "assets/", 8 | "src/", 9 | "test/", 10 | ".gitignore", 11 | ".jshintrc", 12 | "gulpfile.js", 13 | "index.html", 14 | "package.json" 15 | ] 16 | } -------------------------------------------------------------------------------- /dist/vivus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vivus - JavaScript library to make drawing animation on SVG 3 | * @version v0.4.6 4 | * @link https://github.com/maxwellito/vivus 5 | * @license MIT 6 | */ 7 | 8 | (function () { 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Pathformer 14 | * Beta version 15 | * 16 | * Take any SVG version 1.1 and transform 17 | * child elements to 'path' elements 18 | * 19 | * This code is purely forked from 20 | * https://github.com/Waest/SVGPathConverter 21 | */ 22 | 23 | /** 24 | * Class constructor 25 | * 26 | * @param {DOM|String} element Dom element of the SVG or id of it 27 | */ 28 | function Pathformer(element) { 29 | // Test params 30 | if (typeof element === 'undefined') { 31 | throw new Error('Pathformer [constructor]: "element" parameter is required'); 32 | } 33 | 34 | // Set the element 35 | if (element.constructor === String) { 36 | element = document.getElementById(element); 37 | if (!element) { 38 | throw new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID'); 39 | } 40 | } 41 | if (element instanceof window.SVGElement || 42 | element instanceof window.SVGGElement || 43 | /^svg$/i.test(element.nodeName)) { 44 | this.el = element; 45 | } else { 46 | throw new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'); 47 | } 48 | 49 | // Start 50 | this.scan(element); 51 | } 52 | 53 | /** 54 | * List of tags which can be transformed 55 | * to path elements 56 | * 57 | * @type {Array} 58 | */ 59 | Pathformer.prototype.TYPES = ['line', 'ellipse', 'circle', 'polygon', 'polyline', 'rect']; 60 | 61 | /** 62 | * List of attribute names which contain 63 | * data. This array list them to check if 64 | * they contain bad values, like percentage. 65 | * 66 | * @type {Array} 67 | */ 68 | Pathformer.prototype.ATTR_WATCH = ['cx', 'cy', 'points', 'r', 'rx', 'ry', 'x', 'x1', 'x2', 'y', 'y1', 'y2']; 69 | 70 | /** 71 | * Finds the elements compatible for transform 72 | * and apply the liked method 73 | * 74 | * @param {object} options Object from the constructor 75 | */ 76 | Pathformer.prototype.scan = function (svg) { 77 | var fn, element, pathData, pathDom, 78 | elements = svg.querySelectorAll(this.TYPES.join(',')); 79 | 80 | for (var i = 0; i < elements.length; i++) { 81 | element = elements[i]; 82 | fn = this[element.tagName.toLowerCase() + 'ToPath']; 83 | pathData = fn(this.parseAttr(element.attributes)); 84 | pathDom = this.pathMaker(element, pathData); 85 | element.parentNode.replaceChild(pathDom, element); 86 | } 87 | }; 88 | 89 | 90 | /** 91 | * Read `line` element to extract and transform 92 | * data, to make it ready for a `path` object. 93 | * 94 | * @param {DOMelement} element Line element to transform 95 | * @return {object} Data for a `path` element 96 | */ 97 | Pathformer.prototype.lineToPath = function (element) { 98 | var newElement = {}, 99 | x1 = element.x1 || 0, 100 | y1 = element.y1 || 0, 101 | x2 = element.x2 || 0, 102 | y2 = element.y2 || 0; 103 | 104 | newElement.d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2; 105 | return newElement; 106 | }; 107 | 108 | /** 109 | * Read `rect` element to extract and transform 110 | * data, to make it ready for a `path` object. 111 | * The radius-border is not taken in charge yet. 112 | * (your help is more than welcomed) 113 | * 114 | * @param {DOMelement} element Rect element to transform 115 | * @return {object} Data for a `path` element 116 | */ 117 | Pathformer.prototype.rectToPath = function (element) { 118 | var newElement = {}, 119 | x = parseFloat(element.x) || 0, 120 | y = parseFloat(element.y) || 0, 121 | width = parseFloat(element.width) || 0, 122 | height = parseFloat(element.height) || 0; 123 | 124 | if (element.rx || element.ry) { 125 | var rx = parseInt(element.rx, 10) || -1, 126 | ry = parseInt(element.ry, 10) || -1; 127 | rx = Math.min(Math.max(rx < 0 ? ry : rx, 0), width/2); 128 | ry = Math.min(Math.max(ry < 0 ? rx : ry, 0), height/2); 129 | 130 | newElement.d = 'M ' + (x + rx) + ',' + y + ' ' + 131 | 'L ' + (x + width - rx) + ',' + y + ' ' + 132 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + width) + ',' + (y + ry) + ' ' + 133 | 'L ' + (x + width) + ',' + (y + height - ry) + ' ' + 134 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + width - rx) + ',' + (y + height) + ' ' + 135 | 'L ' + (x + rx) + ',' + (y + height) + ' ' + 136 | 'A ' + rx + ',' + ry + ',0,0,1,' + x + ',' + (y + height - ry) + ' ' + 137 | 'L ' + x + ',' + (y + ry) + ' ' + 138 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + rx) + ',' + y; 139 | } 140 | else { 141 | newElement.d = 'M' + x + ' ' + y + ' ' + 142 | 'L' + (x + width) + ' ' + y + ' ' + 143 | 'L' + (x + width) + ' ' + (y + height) + ' ' + 144 | 'L' + x + ' ' + (y + height) + ' Z'; 145 | } 146 | return newElement; 147 | }; 148 | 149 | /** 150 | * Read `polyline` element to extract and transform 151 | * data, to make it ready for a `path` object. 152 | * 153 | * @param {DOMelement} element Polyline element to transform 154 | * @return {object} Data for a `path` element 155 | */ 156 | Pathformer.prototype.polylineToPath = function (element) { 157 | var newElement = {}, 158 | points = element.points.trim().split(' '), 159 | i, path; 160 | 161 | // Reformatting if points are defined without commas 162 | if (element.points.indexOf(',') === -1) { 163 | var formattedPoints = []; 164 | for (i = 0; i < points.length; i+=2) { 165 | formattedPoints.push(points[i] + ',' + points[i+1]); 166 | } 167 | points = formattedPoints; 168 | } 169 | 170 | // Generate the path.d value 171 | path = 'M' + points[0]; 172 | for(i = 1; i < points.length; i++) { 173 | if (points[i].indexOf(',') !== -1) { 174 | path += 'L' + points[i]; 175 | } 176 | } 177 | newElement.d = path; 178 | return newElement; 179 | }; 180 | 181 | /** 182 | * Read `polygon` element to extract and transform 183 | * data, to make it ready for a `path` object. 184 | * This method rely on polylineToPath, because the 185 | * logic is similar. The path created is just closed, 186 | * so it needs an 'Z' at the end. 187 | * 188 | * @param {DOMelement} element Polygon element to transform 189 | * @return {object} Data for a `path` element 190 | */ 191 | Pathformer.prototype.polygonToPath = function (element) { 192 | var newElement = Pathformer.prototype.polylineToPath(element); 193 | 194 | newElement.d += 'Z'; 195 | return newElement; 196 | }; 197 | 198 | /** 199 | * Read `ellipse` element to extract and transform 200 | * data, to make it ready for a `path` object. 201 | * 202 | * @param {DOMelement} element ellipse element to transform 203 | * @return {object} Data for a `path` element 204 | */ 205 | Pathformer.prototype.ellipseToPath = function (element) { 206 | var newElement = {}, 207 | rx = parseFloat(element.rx) || 0, 208 | ry = parseFloat(element.ry) || 0, 209 | cx = parseFloat(element.cx) || 0, 210 | cy = parseFloat(element.cy) || 0, 211 | startX = cx - rx, 212 | startY = cy, 213 | endX = parseFloat(cx) + parseFloat(rx), 214 | endY = cy; 215 | 216 | newElement.d = 'M' + startX + ',' + startY + 217 | 'A' + rx + ',' + ry + ' 0,1,1 ' + endX + ',' + endY + 218 | 'A' + rx + ',' + ry + ' 0,1,1 ' + startX + ',' + endY; 219 | return newElement; 220 | }; 221 | 222 | /** 223 | * Read `circle` element to extract and transform 224 | * data, to make it ready for a `path` object. 225 | * 226 | * @param {DOMelement} element Circle element to transform 227 | * @return {object} Data for a `path` element 228 | */ 229 | Pathformer.prototype.circleToPath = function (element) { 230 | var newElement = {}, 231 | r = parseFloat(element.r) || 0, 232 | cx = parseFloat(element.cx) || 0, 233 | cy = parseFloat(element.cy) || 0, 234 | startX = cx - r, 235 | startY = cy, 236 | endX = parseFloat(cx) + parseFloat(r), 237 | endY = cy; 238 | 239 | newElement.d = 'M' + startX + ',' + startY + 240 | 'A' + r + ',' + r + ' 0,1,1 ' + endX + ',' + endY + 241 | 'A' + r + ',' + r + ' 0,1,1 ' + startX + ',' + endY; 242 | return newElement; 243 | }; 244 | 245 | /** 246 | * Create `path` elements form original element 247 | * and prepared objects 248 | * 249 | * @param {DOMelement} element Original element to transform 250 | * @param {object} pathData Path data (from `toPath` methods) 251 | * @return {DOMelement} Path element 252 | */ 253 | Pathformer.prototype.pathMaker = function (element, pathData) { 254 | var i, attr, pathTag = document.createElementNS('http://www.w3.org/2000/svg','path'); 255 | for(i = 0; i < element.attributes.length; i++) { 256 | attr = element.attributes[i]; 257 | if (this.ATTR_WATCH.indexOf(attr.name) === -1) { 258 | pathTag.setAttribute(attr.name, attr.value); 259 | } 260 | } 261 | for(i in pathData) { 262 | pathTag.setAttribute(i, pathData[i]); 263 | } 264 | return pathTag; 265 | }; 266 | 267 | /** 268 | * Parse attributes of a DOM element to 269 | * get an object of attribute => value 270 | * 271 | * @param {NamedNodeMap} attributes Attributes object from DOM element to parse 272 | * @return {object} Object of attributes 273 | */ 274 | Pathformer.prototype.parseAttr = function (element) { 275 | var attr, output = {}; 276 | for (var i = 0; i < element.length; i++) { 277 | attr = element[i]; 278 | // Check if no data attribute contains '%', or the transformation is impossible 279 | if (this.ATTR_WATCH.indexOf(attr.name) !== -1 && attr.value.indexOf('%') !== -1) { 280 | throw new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.'); 281 | } 282 | output[attr.name] = attr.value; 283 | } 284 | return output; 285 | }; 286 | 287 | 'use strict'; 288 | 289 | var setupEnv, requestAnimFrame, cancelAnimFrame, parsePositiveInt; 290 | 291 | /** 292 | * Vivus 293 | * Beta version 294 | * 295 | * Take any SVG and make the animation 296 | * to give give the impression of live drawing 297 | * 298 | * This in more than just inspired from codrops 299 | * At that point, it's a pure fork. 300 | */ 301 | 302 | /** 303 | * Class constructor 304 | * option structure 305 | * type: 'delayed'|'sync'|'oneByOne'|'script' (to know if the items must be drawn synchronously or not, default: delayed) 306 | * duration: (in frames) 307 | * start: 'inViewport'|'manual'|'autostart' (start automatically the animation, default: inViewport) 308 | * delay: (delay between the drawing of first and last path) 309 | * dashGap whitespace extra margin between dashes 310 | * pathTimingFunction timing animation function for each path element of the SVG 311 | * animTimingFunction timing animation function for the complete SVG 312 | * forceRender force the browser to re-render all updated path items 313 | * selfDestroy removes all extra styling on the SVG, and leaves it as original 314 | * 315 | * The attribute 'type' is by default on 'delayed'. 316 | * - 'delayed' 317 | * all paths are draw at the same time but with a 318 | * little delay between them before start 319 | * - 'sync' 320 | * all path are start and finish at the same time 321 | * - 'oneByOne' 322 | * only one path is draw at the time 323 | * the end of the first one will trigger the draw 324 | * of the next one 325 | * 326 | * All these values can be overwritten individually 327 | * for each path item in the SVG 328 | * The value of frames will always take the advantage of 329 | * the duration value. 330 | * If you fail somewhere, an error will be thrown. 331 | * Good luck. 332 | * 333 | * @constructor 334 | * @this {Vivus} 335 | * @param {DOM|String} element Dom element of the SVG or id of it 336 | * @param {Object} options Options about the animation 337 | * @param {Function} callback Callback for the end of the animation 338 | */ 339 | function Vivus(element, options, callback) { 340 | setupEnv(); 341 | 342 | // Setup 343 | this.isReady = false; 344 | this.setElement(element, options); 345 | this.setOptions(options); 346 | this.setCallback(callback); 347 | 348 | if (this.isReady) { 349 | this.init(); 350 | } 351 | } 352 | 353 | /** 354 | * Timing functions 355 | ************************************** 356 | * 357 | * Default functions to help developers. 358 | * It always take a number as parameter (between 0 to 1) then 359 | * return a number (between 0 and 1) 360 | */ 361 | Vivus.LINEAR = function(x) { 362 | return x; 363 | }; 364 | Vivus.EASE = function(x) { 365 | return -Math.cos(x * Math.PI) / 2 + 0.5; 366 | }; 367 | Vivus.EASE_OUT = function(x) { 368 | return 1 - Math.pow(1 - x, 3); 369 | }; 370 | Vivus.EASE_IN = function(x) { 371 | return Math.pow(x, 3); 372 | }; 373 | Vivus.EASE_OUT_BOUNCE = function(x) { 374 | var base = -Math.cos(x * (0.5 * Math.PI)) + 1, 375 | rate = Math.pow(base, 1.5), 376 | rateR = Math.pow(1 - x, 2), 377 | progress = -Math.abs(Math.cos(rate * (2.5 * Math.PI))) + 1; 378 | return 1 - rateR + progress * rateR; 379 | }; 380 | 381 | /** 382 | * Setters 383 | ************************************** 384 | */ 385 | 386 | /** 387 | * Check and set the element in the instance 388 | * The method will not return anything, but will throw an 389 | * error if the parameter is invalid 390 | * 391 | * @param {DOM|String} element SVG Dom element or id of it 392 | */ 393 | Vivus.prototype.setElement = function(element, options) { 394 | var onLoad, self; 395 | 396 | // Basic check 397 | if (typeof element === 'undefined') { 398 | throw new Error('Vivus [constructor]: "element" parameter is required'); 399 | } 400 | 401 | // Set the element 402 | if (element.constructor === String) { 403 | element = document.getElementById(element); 404 | if (!element) { 405 | throw new Error( 406 | 'Vivus [constructor]: "element" parameter is not related to an existing ID' 407 | ); 408 | } 409 | } 410 | this.parentEl = element; 411 | 412 | // Load the SVG with XMLHttpRequest and extract the SVG 413 | if (options && options.file) { 414 | self = this; 415 | onLoad = function() { 416 | var domSandbox = document.createElement('div'); 417 | domSandbox.innerHTML = this.responseText; 418 | 419 | var svgTag = domSandbox.querySelector('svg'); 420 | if (!svgTag) { 421 | throw new Error( 422 | 'Vivus [load]: Cannot find the SVG in the loaded file : ' + 423 | options.file 424 | ); 425 | } 426 | 427 | self.el = svgTag; 428 | self.el.setAttribute('width', '100%'); 429 | self.el.setAttribute('height', '100%'); 430 | self.parentEl.appendChild(self.el); 431 | self.isReady = true; 432 | self.init(); 433 | self = null; 434 | }; 435 | 436 | var oReq = new window.XMLHttpRequest(); 437 | oReq.addEventListener('load', onLoad); 438 | oReq.open('GET', options.file); 439 | oReq.send(); 440 | return; 441 | } 442 | 443 | switch (element.constructor) { 444 | case window.SVGSVGElement: 445 | case window.SVGElement: 446 | case window.SVGGElement: 447 | this.el = element; 448 | this.isReady = true; 449 | break; 450 | 451 | case window.HTMLObjectElement: 452 | self = this; 453 | onLoad = function(e) { 454 | if (self.isReady) { 455 | return; 456 | } 457 | self.el = 458 | element.contentDocument && 459 | element.contentDocument.querySelector('svg'); 460 | if (!self.el && e) { 461 | throw new Error( 462 | 'Vivus [constructor]: object loaded does not contain any SVG' 463 | ); 464 | } else if (self.el) { 465 | if (element.getAttribute('built-by-vivus')) { 466 | self.parentEl.insertBefore(self.el, element); 467 | self.parentEl.removeChild(element); 468 | self.el.setAttribute('width', '100%'); 469 | self.el.setAttribute('height', '100%'); 470 | } 471 | self.isReady = true; 472 | self.init(); 473 | self = null; 474 | } 475 | }; 476 | 477 | if (!onLoad()) { 478 | element.addEventListener('load', onLoad); 479 | } 480 | break; 481 | 482 | default: 483 | throw new Error( 484 | 'Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)' 485 | ); 486 | } 487 | }; 488 | 489 | /** 490 | * Set up user option to the instance 491 | * The method will not return anything, but will throw an 492 | * error if the parameter is invalid 493 | * 494 | * @param {object} options Object from the constructor 495 | */ 496 | Vivus.prototype.setOptions = function(options) { 497 | var allowedTypes = [ 498 | 'delayed', 499 | 'sync', 500 | 'async', 501 | 'nsync', 502 | 'oneByOne', 503 | 'scenario', 504 | 'scenario-sync' 505 | ]; 506 | var allowedStarts = ['inViewport', 'manual', 'autostart']; 507 | 508 | // Basic check 509 | if (options !== undefined && options.constructor !== Object) { 510 | throw new Error( 511 | 'Vivus [constructor]: "options" parameter must be an object' 512 | ); 513 | } else { 514 | options = options || {}; 515 | } 516 | 517 | // Set the animation type 518 | if (options.type && allowedTypes.indexOf(options.type) === -1) { 519 | throw new Error( 520 | 'Vivus [constructor]: ' + 521 | options.type + 522 | ' is not an existing animation `type`' 523 | ); 524 | } else { 525 | this.type = options.type || allowedTypes[0]; 526 | } 527 | 528 | // Set the start type 529 | if (options.start && allowedStarts.indexOf(options.start) === -1) { 530 | throw new Error( 531 | 'Vivus [constructor]: ' + 532 | options.start + 533 | ' is not an existing `start` option' 534 | ); 535 | } else { 536 | this.start = options.start || allowedStarts[0]; 537 | } 538 | 539 | this.isIE = 540 | window.navigator.userAgent.indexOf('MSIE') !== -1 || 541 | window.navigator.userAgent.indexOf('Trident/') !== -1 || 542 | window.navigator.userAgent.indexOf('Edge/') !== -1; 543 | this.duration = parsePositiveInt(options.duration, 120); 544 | this.delay = parsePositiveInt(options.delay, null); 545 | this.dashGap = parsePositiveInt(options.dashGap, 1); 546 | this.forceRender = options.hasOwnProperty('forceRender') 547 | ? !!options.forceRender 548 | : this.isIE; 549 | this.reverseStack = !!options.reverseStack; 550 | this.selfDestroy = !!options.selfDestroy; 551 | this.onReady = options.onReady; 552 | this.map = []; 553 | this.frameLength = this.currentFrame = this.delayUnit = this.speed = this.handle = null; 554 | 555 | this.ignoreInvisible = options.hasOwnProperty('ignoreInvisible') 556 | ? !!options.ignoreInvisible 557 | : false; 558 | 559 | this.animTimingFunction = options.animTimingFunction || Vivus.LINEAR; 560 | this.pathTimingFunction = options.pathTimingFunction || Vivus.LINEAR; 561 | 562 | if (this.delay >= this.duration) { 563 | throw new Error('Vivus [constructor]: delay must be shorter than duration'); 564 | } 565 | }; 566 | 567 | /** 568 | * Set up callback to the instance 569 | * The method will not return enything, but will throw an 570 | * error if the parameter is invalid 571 | * 572 | * @param {Function} callback Callback for the animation end 573 | */ 574 | Vivus.prototype.setCallback = function(callback) { 575 | // Basic check 576 | if (!!callback && callback.constructor !== Function) { 577 | throw new Error( 578 | 'Vivus [constructor]: "callback" parameter must be a function' 579 | ); 580 | } 581 | this.callback = callback || function() {}; 582 | }; 583 | 584 | /** 585 | * Core 586 | ************************************** 587 | */ 588 | 589 | /** 590 | * Map the svg, path by path. 591 | * The method return nothing, it just fill the 592 | * `map` array. Each item in this array represent 593 | * a path element from the SVG, with informations for 594 | * the animation. 595 | * 596 | * ``` 597 | * [ 598 | * { 599 | * el: the path element 600 | * length: length of the path line 601 | * startAt: time start of the path animation (in frames) 602 | * duration: path animation duration (in frames) 603 | * }, 604 | * ... 605 | * ] 606 | * ``` 607 | * 608 | */ 609 | Vivus.prototype.mapping = function() { 610 | var i, paths, path, pAttrs, pathObj, totalLength, lengthMeter, timePoint, scale, hasNonScale; 611 | timePoint = totalLength = lengthMeter = 0; 612 | paths = this.el.querySelectorAll('path'); 613 | hasNonScale = false; 614 | 615 | for (i = 0; i < paths.length; i++) { 616 | path = paths[i]; 617 | if (this.isInvisible(path)) { 618 | continue; 619 | } 620 | 621 | pathObj = { 622 | el: path, 623 | length: 0, 624 | startAt: 0, 625 | duration: 0, 626 | isResizeSensitive: false 627 | }; 628 | 629 | // If vector effect is non-scaling-stroke, the total length won't match the rendered length 630 | // so we need to calculate the scale and apply it 631 | if (path.getAttribute('vector-effect') === 'non-scaling-stroke') { 632 | var rect = path.getBoundingClientRect(); 633 | var box = path.getBBox(); 634 | scale = Math.max(rect.width / box.width, rect.height / box.height); 635 | pathObj.isResizeSensitive = true; 636 | hasNonScale = true; 637 | } else { 638 | scale = 1; 639 | } 640 | pathObj.length = Math.ceil(path.getTotalLength() * scale); 641 | 642 | // Test if the path length is correct 643 | if (isNaN(pathObj.length)) { 644 | if (window.console && console.warn) { 645 | console.warn( 646 | 'Vivus [mapping]: cannot retrieve a path element length', 647 | path 648 | ); 649 | } 650 | continue; 651 | } 652 | this.map.push(pathObj); 653 | path.style.strokeDasharray = 654 | pathObj.length + ' ' + (pathObj.length + this.dashGap * 2); 655 | path.style.strokeDashoffset = pathObj.length + this.dashGap; 656 | pathObj.length += this.dashGap; 657 | totalLength += pathObj.length; 658 | 659 | this.renderPath(i); 660 | } 661 | 662 | // Show a warning for non-scaling elements 663 | if (hasNonScale) { 664 | console.warn('Vivus: this SVG contains non-scaling-strokes. You should call instance.recalc() when the SVG is resized or you will encounter unwanted behaviour. See https://github.com/maxwellito/vivus#non-scaling for more info.'); 665 | } 666 | 667 | totalLength = totalLength === 0 ? 1 : totalLength; 668 | this.delay = this.delay === null ? this.duration / 3 : this.delay; 669 | this.delayUnit = this.delay / (paths.length > 1 ? paths.length - 1 : 1); 670 | 671 | // Reverse stack if asked 672 | if (this.reverseStack) { 673 | this.map.reverse(); 674 | } 675 | 676 | for (i = 0; i < this.map.length; i++) { 677 | pathObj = this.map[i]; 678 | 679 | switch (this.type) { 680 | case 'delayed': 681 | pathObj.startAt = this.delayUnit * i; 682 | pathObj.duration = this.duration - this.delay; 683 | break; 684 | 685 | case 'oneByOne': 686 | pathObj.startAt = (lengthMeter / totalLength) * this.duration; 687 | pathObj.duration = (pathObj.length / totalLength) * this.duration; 688 | break; 689 | 690 | case 'sync': 691 | case 'async': 692 | case 'nsync': 693 | pathObj.startAt = 0; 694 | pathObj.duration = this.duration; 695 | break; 696 | 697 | case 'scenario-sync': 698 | path = pathObj.el; 699 | pAttrs = this.parseAttr(path); 700 | pathObj.startAt = 701 | timePoint + 702 | (parsePositiveInt(pAttrs['data-delay'], this.delayUnit) || 0); 703 | pathObj.duration = parsePositiveInt( 704 | pAttrs['data-duration'], 705 | this.duration 706 | ); 707 | timePoint = 708 | pAttrs['data-async'] !== undefined 709 | ? pathObj.startAt 710 | : pathObj.startAt + pathObj.duration; 711 | this.frameLength = Math.max( 712 | this.frameLength, 713 | pathObj.startAt + pathObj.duration 714 | ); 715 | break; 716 | 717 | case 'scenario': 718 | path = pathObj.el; 719 | pAttrs = this.parseAttr(path); 720 | pathObj.startAt = 721 | parsePositiveInt(pAttrs['data-start'], this.delayUnit) || 0; 722 | pathObj.duration = parsePositiveInt( 723 | pAttrs['data-duration'], 724 | this.duration 725 | ); 726 | this.frameLength = Math.max( 727 | this.frameLength, 728 | pathObj.startAt + pathObj.duration 729 | ); 730 | break; 731 | } 732 | lengthMeter += pathObj.length; 733 | this.frameLength = this.frameLength || this.duration; 734 | } 735 | }; 736 | 737 | /** 738 | * Public method to re-evaluate line length for non-scaling lines 739 | * path elements. 740 | */ 741 | Vivus.prototype.recalc = function () { 742 | if (this.mustRecalcScale) { 743 | return; 744 | } 745 | this.mustRecalcScale = requestAnimFrame(function () { 746 | this.performLineRecalc(); 747 | }.bind(this)); 748 | } 749 | 750 | /** 751 | * Private method to re-evaluate line length on non-scaling 752 | * path elements. Then call for a trace to update the SVG. 753 | */ 754 | Vivus.prototype.performLineRecalc = function () { 755 | var pathObj, path, rect, box, scale; 756 | for (var i = 0; i < this.map.length; i++) { 757 | pathObj = this.map[i]; 758 | if (pathObj.isResizeSensitive) { 759 | path = pathObj.el; 760 | rect = path.getBoundingClientRect(); 761 | box = path.getBBox(); 762 | scale = Math.max(rect.width / box.width, rect.height / box.height); 763 | pathObj.length = Math.ceil(path.getTotalLength() * scale); 764 | path.style.strokeDasharray = pathObj.length + ' ' + (pathObj.length + this.dashGap * 2); 765 | } 766 | } 767 | this.trace(); 768 | this.mustRecalcScale = null; 769 | } 770 | 771 | /** 772 | * Interval method to draw the SVG from current 773 | * position of the animation. It update the value of 774 | * `currentFrame` and re-trace the SVG. 775 | * 776 | * It use this.handle to store the requestAnimationFrame 777 | * and clear it one the animation is stopped. So this 778 | * attribute can be used to know if the animation is 779 | * playing. 780 | * 781 | * Once the animation at the end, this method will 782 | * trigger the Vivus callback. 783 | * 784 | */ 785 | Vivus.prototype.draw = function() { 786 | var self = this; 787 | this.currentFrame += this.speed; 788 | 789 | if (this.currentFrame <= 0) { 790 | this.stop(); 791 | this.reset(); 792 | } else if (this.currentFrame >= this.frameLength) { 793 | this.stop(); 794 | this.currentFrame = this.frameLength; 795 | this.trace(); 796 | if (this.selfDestroy) { 797 | this.destroy(); 798 | } 799 | } else { 800 | this.trace(); 801 | this.handle = requestAnimFrame(function() { 802 | self.draw(); 803 | }); 804 | return; 805 | } 806 | 807 | this.callback(this); 808 | if (this.instanceCallback) { 809 | this.instanceCallback(this); 810 | this.instanceCallback = null; 811 | } 812 | }; 813 | 814 | /** 815 | * Draw the SVG at the current instant from the 816 | * `currentFrame` value. Here is where most of the magic is. 817 | * The trick is to use the `strokeDashoffset` style property. 818 | * 819 | * For optimisation reasons, a new property called `progress` 820 | * is added in each item of `map`. This one contain the current 821 | * progress of the path element. Only if the new value is different 822 | * the new value will be applied to the DOM element. This 823 | * method save a lot of resources to re-render the SVG. And could 824 | * be improved if the animation couldn't be played forward. 825 | * 826 | */ 827 | Vivus.prototype.trace = function() { 828 | var i, progress, path, currentFrame; 829 | currentFrame = 830 | this.animTimingFunction(this.currentFrame / this.frameLength) * 831 | this.frameLength; 832 | for (i = 0; i < this.map.length; i++) { 833 | path = this.map[i]; 834 | progress = (currentFrame - path.startAt) / path.duration; 835 | progress = this.pathTimingFunction(Math.max(0, Math.min(1, progress))); 836 | if (path.progress !== progress) { 837 | path.progress = progress; 838 | path.el.style.strokeDashoffset = Math.floor(path.length * (1 - progress)); 839 | this.renderPath(i); 840 | } 841 | } 842 | }; 843 | 844 | /** 845 | * Method forcing the browser to re-render a path element 846 | * from it's index in the map. Depending on the `forceRender` 847 | * value. 848 | * The trick is to replace the path element by it's clone. 849 | * This practice is not recommended because it's asking more 850 | * ressources, too much DOM manupulation.. 851 | * but it's the only way to let the magic happen on IE. 852 | * By default, this fallback is only applied on IE. 853 | * 854 | * @param {Number} index Path index 855 | */ 856 | Vivus.prototype.renderPath = function(index) { 857 | if (this.forceRender && this.map && this.map[index]) { 858 | var pathObj = this.map[index], 859 | newPath = pathObj.el.cloneNode(true); 860 | pathObj.el.parentNode.replaceChild(newPath, pathObj.el); 861 | pathObj.el = newPath; 862 | } 863 | }; 864 | 865 | /** 866 | * When the SVG object is loaded and ready, 867 | * this method will continue the initialisation. 868 | * 869 | * This this mainly due to the case of passing an 870 | * object tag in the constructor. It will wait 871 | * the end of the loading to initialise. 872 | * 873 | */ 874 | Vivus.prototype.init = function() { 875 | // Set object variables 876 | this.frameLength = 0; 877 | this.currentFrame = 0; 878 | this.map = []; 879 | 880 | // Start 881 | new Pathformer(this.el); 882 | this.mapping(); 883 | this.starter(); 884 | 885 | if (this.onReady) { 886 | this.onReady(this); 887 | } 888 | }; 889 | 890 | /** 891 | * Trigger to start of the animation. 892 | * Depending on the `start` value, a different script 893 | * will be applied. 894 | * 895 | * If the `start` value is not valid, an error will be thrown. 896 | * Even if technically, this is impossible. 897 | * 898 | */ 899 | Vivus.prototype.starter = function() { 900 | switch (this.start) { 901 | case 'manual': 902 | return; 903 | 904 | case 'autostart': 905 | this.play(); 906 | break; 907 | 908 | case 'inViewport': 909 | var self = this, 910 | listener = function() { 911 | if (self.isInViewport(self.parentEl, 1)) { 912 | self.play(); 913 | window.removeEventListener('scroll', listener); 914 | } 915 | }; 916 | window.addEventListener('scroll', listener); 917 | listener(); 918 | break; 919 | } 920 | }; 921 | 922 | /** 923 | * Controls 924 | ************************************** 925 | */ 926 | 927 | /** 928 | * Get the current status of the animation between 929 | * three different states: 'start', 'progress', 'end'. 930 | * @return {string} Instance status 931 | */ 932 | Vivus.prototype.getStatus = function() { 933 | return this.currentFrame === 0 934 | ? 'start' 935 | : this.currentFrame === this.frameLength 936 | ? 'end' 937 | : 'progress'; 938 | }; 939 | 940 | /** 941 | * Reset the instance to the initial state : undraw 942 | * Be careful, it just reset the animation, if you're 943 | * playing the animation, this won't stop it. But just 944 | * make it start from start. 945 | * 946 | */ 947 | Vivus.prototype.reset = function() { 948 | return this.setFrameProgress(0); 949 | }; 950 | 951 | /** 952 | * Set the instance to the final state : drawn 953 | * Be careful, it just set the animation, if you're 954 | * playing the animation on rewind, this won't stop it. 955 | * But just make it start from the end. 956 | * 957 | */ 958 | Vivus.prototype.finish = function() { 959 | return this.setFrameProgress(1); 960 | }; 961 | 962 | /** 963 | * Set the level of progress of the drawing. 964 | * 965 | * @param {number} progress Level of progress to set 966 | */ 967 | Vivus.prototype.setFrameProgress = function(progress) { 968 | progress = Math.min(1, Math.max(0, progress)); 969 | this.currentFrame = Math.round(this.frameLength * progress); 970 | this.trace(); 971 | return this; 972 | }; 973 | 974 | /** 975 | * Play the animation at the desired speed. 976 | * Speed must be a valid number (no zero). 977 | * By default, the speed value is 1. 978 | * But a negative value is accepted to go forward. 979 | * 980 | * And works with float too. 981 | * But don't forget we are in JavaScript, se be nice 982 | * with him and give him a 1/2^x value. 983 | * 984 | * @param {number} speed Animation speed [optional] 985 | */ 986 | Vivus.prototype.play = function(speed, callback) { 987 | this.instanceCallback = null; 988 | 989 | if (speed && typeof speed === 'function') { 990 | this.instanceCallback = speed; // first parameter is actually the callback function 991 | speed = null; 992 | } else if (speed && typeof speed !== 'number') { 993 | throw new Error('Vivus [play]: invalid speed'); 994 | } 995 | // if the first parameter wasn't the callback, check if the seconds was 996 | if (callback && typeof callback === 'function' && !this.instanceCallback) { 997 | this.instanceCallback = callback; 998 | } 999 | 1000 | this.speed = speed || 1; 1001 | if (!this.handle) { 1002 | this.draw(); 1003 | } 1004 | return this; 1005 | }; 1006 | 1007 | /** 1008 | * Stop the current animation, if on progress. 1009 | * Should not trigger any error. 1010 | * 1011 | */ 1012 | Vivus.prototype.stop = function() { 1013 | if (this.handle) { 1014 | cancelAnimFrame(this.handle); 1015 | this.handle = null; 1016 | } 1017 | return this; 1018 | }; 1019 | 1020 | /** 1021 | * Destroy the instance. 1022 | * Remove all bad styling attributes on all 1023 | * path tags 1024 | * 1025 | */ 1026 | Vivus.prototype.destroy = function() { 1027 | this.stop(); 1028 | var i, path; 1029 | for (i = 0; i < this.map.length; i++) { 1030 | path = this.map[i]; 1031 | path.el.style.strokeDashoffset = null; 1032 | path.el.style.strokeDasharray = null; 1033 | this.renderPath(i); 1034 | } 1035 | }; 1036 | 1037 | /** 1038 | * Utils methods 1039 | * include methods from Codrops 1040 | ************************************** 1041 | */ 1042 | 1043 | /** 1044 | * Method to best guess if a path should added into 1045 | * the animation or not. 1046 | * 1047 | * 1. Use the `data-vivus-ignore` attribute if set 1048 | * 2. Check if the instance must ignore invisible paths 1049 | * 3. Check if the path is visible 1050 | * 1051 | * For now the visibility checking is unstable. 1052 | * It will be used for a beta phase. 1053 | * 1054 | * Other improvments are planned. Like detecting 1055 | * is the path got a stroke or a valid opacity. 1056 | */ 1057 | Vivus.prototype.isInvisible = function(el) { 1058 | var rect, 1059 | ignoreAttr = el.getAttribute('data-ignore'); 1060 | 1061 | if (ignoreAttr !== null) { 1062 | return ignoreAttr !== 'false'; 1063 | } 1064 | 1065 | if (this.ignoreInvisible) { 1066 | rect = el.getBoundingClientRect(); 1067 | return !rect.width && !rect.height; 1068 | } else { 1069 | return false; 1070 | } 1071 | }; 1072 | 1073 | /** 1074 | * Parse attributes of a DOM element to 1075 | * get an object of {attributeName => attributeValue} 1076 | * 1077 | * @param {object} element DOM element to parse 1078 | * @return {object} Object of attributes 1079 | */ 1080 | Vivus.prototype.parseAttr = function(element) { 1081 | var attr, 1082 | output = {}; 1083 | if (element && element.attributes) { 1084 | for (var i = 0; i < element.attributes.length; i++) { 1085 | attr = element.attributes[i]; 1086 | output[attr.name] = attr.value; 1087 | } 1088 | } 1089 | return output; 1090 | }; 1091 | 1092 | /** 1093 | * Reply if an element is in the page viewport 1094 | * 1095 | * @param {object} el Element to observe 1096 | * @param {number} h Percentage of height 1097 | * @return {boolean} 1098 | */ 1099 | Vivus.prototype.isInViewport = function(el, h) { 1100 | var scrolled = this.scrollY(), 1101 | viewed = scrolled + this.getViewportH(), 1102 | elBCR = el.getBoundingClientRect(), 1103 | elHeight = elBCR.height, 1104 | elTop = scrolled + elBCR.top, 1105 | elBottom = elTop + elHeight; 1106 | 1107 | // if 0, the element is considered in the viewport as soon as it enters. 1108 | // if 1, the element is considered in the viewport only when it's fully inside 1109 | // value in percentage (1 >= h >= 0) 1110 | h = h || 0; 1111 | 1112 | return elTop + elHeight * h <= viewed && elBottom >= scrolled; 1113 | }; 1114 | 1115 | /** 1116 | * Get the viewport height in pixels 1117 | * 1118 | * @return {integer} Viewport height 1119 | */ 1120 | Vivus.prototype.getViewportH = function() { 1121 | var client = this.docElem.clientHeight, 1122 | inner = window.innerHeight; 1123 | 1124 | if (client < inner) { 1125 | return inner; 1126 | } else { 1127 | return client; 1128 | } 1129 | }; 1130 | 1131 | /** 1132 | * Get the page Y offset 1133 | * 1134 | * @return {integer} Page Y offset 1135 | */ 1136 | Vivus.prototype.scrollY = function() { 1137 | return window.pageYOffset || this.docElem.scrollTop; 1138 | }; 1139 | 1140 | setupEnv = function() { 1141 | if (Vivus.prototype.docElem) { 1142 | return; 1143 | } 1144 | 1145 | /** 1146 | * Alias for document element 1147 | * 1148 | * @type {DOMelement} 1149 | */ 1150 | Vivus.prototype.docElem = window.document.documentElement; 1151 | 1152 | /** 1153 | * Alias for `requestAnimationFrame` or 1154 | * `setTimeout` function for deprecated browsers. 1155 | * 1156 | */ 1157 | requestAnimFrame = (function() { 1158 | return ( 1159 | window.requestAnimationFrame || 1160 | window.webkitRequestAnimationFrame || 1161 | window.mozRequestAnimationFrame || 1162 | window.oRequestAnimationFrame || 1163 | window.msRequestAnimationFrame || 1164 | function(/* function */ callback) { 1165 | return window.setTimeout(callback, 1000 / 60); 1166 | } 1167 | ); 1168 | })(); 1169 | 1170 | /** 1171 | * Alias for `cancelAnimationFrame` or 1172 | * `cancelTimeout` function for deprecated browsers. 1173 | * 1174 | */ 1175 | cancelAnimFrame = (function() { 1176 | return ( 1177 | window.cancelAnimationFrame || 1178 | window.webkitCancelAnimationFrame || 1179 | window.mozCancelAnimationFrame || 1180 | window.oCancelAnimationFrame || 1181 | window.msCancelAnimationFrame || 1182 | function(id) { 1183 | return window.clearTimeout(id); 1184 | } 1185 | ); 1186 | })(); 1187 | }; 1188 | 1189 | /** 1190 | * Parse string to integer. 1191 | * If the number is not positive or null 1192 | * the method will return the default value 1193 | * or 0 if undefined 1194 | * 1195 | * @param {string} value String to parse 1196 | * @param {*} defaultValue Value to return if the result parsed is invalid 1197 | * @return {number} 1198 | * 1199 | */ 1200 | parsePositiveInt = function(value, defaultValue) { 1201 | var output = parseInt(value, 10); 1202 | return output >= 0 ? output : defaultValue; 1203 | }; 1204 | 1205 | 1206 | if (typeof define === 'function' && define.amd) { 1207 | // AMD. Register as an anonymous module. 1208 | define([], function() { 1209 | return Vivus; 1210 | }); 1211 | } else if (typeof exports === 'object') { 1212 | // Node. Does not work with strict CommonJS, but 1213 | // only CommonJS-like environments that support module.exports, 1214 | // like Node. 1215 | module.exports = Vivus; 1216 | } else { 1217 | // Browser globals 1218 | window.Vivus = Vivus; 1219 | } 1220 | 1221 | }()); 1222 | 1223 | -------------------------------------------------------------------------------- /dist/vivus.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function n(t){if(void 0===t)throw new Error('Pathformer [constructor]: "element" parameter is required');if(t.constructor===String&&!(t=document.getElementById(t)))throw new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID');if(!(t instanceof window.SVGElement||t instanceof window.SVGGElement||/^svg$/i.test(t.nodeName)))throw new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement');this.el=t,this.scan(t)}var r,e,t,p;function i(t,e,n){r(),this.isReady=!1,this.setElement(t,e),this.setOptions(e),this.setCallback(n),this.isReady&&this.init()}n.prototype.TYPES=["line","ellipse","circle","polygon","polyline","rect"],n.prototype.ATTR_WATCH=["cx","cy","points","r","rx","ry","x","x1","x2","y","y1","y2"],n.prototype.scan=function(t){for(var e,n,r,i=t.querySelectorAll(this.TYPES.join(",")),a=0;a=this.duration)throw new Error("Vivus [constructor]: delay must be shorter than duration")},i.prototype.setCallback=function(t){if(t&&t.constructor!==Function)throw new Error('Vivus [constructor]: "callback" parameter must be a function');this.callback=t||function(){}},i.prototype.mapping=function(){var t,e,n,r,i,a,o,s,h,l;for(s=a=o=0,e=this.el.querySelectorAll("path"),l=!1,t=0;t=this.frameLength))return this.trace(),void(this.handle=e(function(){t.draw()}));this.stop(),this.currentFrame=this.frameLength,this.trace(),this.selfDestroy&&this.destroy()}this.callback(this),this.instanceCallback&&(this.instanceCallback(this),this.instanceCallback=null)},i.prototype.trace=function(){var t,e,n,r;for(r=this.animTimingFunction(this.currentFrame/this.frameLength)*this.frameLength,t=0;t 2 | 3 | 4 | 5 | 6 | 7 | vivus.js - svg animation 8 | 9 | 10 | 205 | 206 | 207 | 208 | 209 |
210 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
240 | 241 | 242 |
243 |

vivus, bringing your SVGs to life

244 |

Vivus is a lightweight JavaScript class (with no dependencies) that allows you to animate SVGs, giving them the appearance of being drawn. There are a variety of different animations available, as well as the option to create a custom script to draw your SVG in whatever way you like.

245 | 248 | 249 |
250 | 251 | 252 |
253 |
254 | 255 |

Animation types

256 |
257 |
258 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 |
274 |

Delayed

275 |

Every path element is drawn at the same time with a small delay at the start. This is currently the default animation.

276 | 277 |
278 |
279 | 280 |
281 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 |
297 |

Sync

298 |

Each line is drawn synchronously. They all start and finish at the same time, hence the name `sync`.

299 | 300 |
301 |
302 | 303 |
304 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 |
320 |

OneByOne

321 |

Each path element is drawn one after the other. This animation gives the best impression of live drawing.

322 | 323 |
324 |
325 |
326 |
327 |
328 |
329 | 330 | 331 |
332 |
333 | 334 |

Timing function

335 |

To give more freedom, it's possible to override the animation of each path and/or the entire SVG. It works a bit like the CSS animation timing function. But instead of using a cubic-bezier function, it use a simple JavaScript function. It must accept a number as parameter (between 0 to 1), then return a number (also between 0 and 1). It's a hook.

336 |

Here an example test to play around with the different properties available.

337 | 338 |
339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 |
350 |

Type 351 | 352 | 353 | 354 | 355 | 356 |

357 |

Path timing function 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 |

366 |

Anim timing function 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 |

375 |
376 |
377 |
378 |
379 | 380 | 381 |
382 |
383 | 384 |
385 | 387 | 388 | 389 | 390 | 393 | 396 | 397 | 398 | 401 | 404 | 406 | 407 | 408 | 409 | 412 | 415 | 418 | 420 | 421 | 422 | 423 | 426 | 427 | 428 | 429 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 |
446 |

Scenarize

447 |

This feature allows you to script the animation of your SVG. To do this, the custom values will be set directly in the DOM of the SVG.

448 |

Here is an example using scenario-sync.
I would recommend you look at the source code and the readme file for more information.

449 | 450 | 451 |
452 |
453 | 454 |
455 |
456 |
457 | 458 | 459 |
460 |
461 |

Play with it on Vivus instant.

462 |

More information and documentation on GitHub.

463 |
464 |
465 | 466 | 467 |
468 |

Thanks for watching.

469 |

Made with love a keyboard

470 |
471 | 472 | 473 | 474 | 516 | 517 | 518 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vivus", 3 | "version": "0.4.6", 4 | "description": "JavaScript library to make drawing animation on SVG", 5 | "main": "dist/vivus.js", 6 | "scripts": { 7 | "test": "karma start test/karma.conf.js", 8 | "serve": "python -m SimpleHTTPServer 8844", 9 | "lint": "./node_modules/eslint/bin/eslint.js src test", 10 | "build": "npm run build-raw && npm run build-min", 11 | "build-raw": "node src/_build.js > dist/vivus.js", 12 | "build-min": "uglifyjs dist/vivus.js -o dist/vivus.min.js -c -m --source-map" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/maxwellito/vivus.git" 17 | }, 18 | "author": "maxwellito", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/maxwellito/vivus/issues" 22 | }, 23 | "homepage": "https://github.com/maxwellito/vivus", 24 | "engine": { 25 | "node": ">=0.10.22" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^5.15.3", 29 | "karma": "^4.0.1", 30 | "karma-chrome-launcher": "^2.2.0", 31 | "karma-coverage": "^1.1.2", 32 | "karma-jasmine": "^2.0.1", 33 | "uglify-js": "^3.5.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # vivus.js 2 | 3 | Demo available on http://maxwellito.github.io/vivus 4 | 5 | Play with it on [Vivus Instant](https://maxwellito.github.io/vivus-instant/) 6 | 7 | Vivus is a lightweight JavaScript class (with no dependencies) that allows you to animate SVGs, giving them the appearance of being drawn. There are a variety of different animations available, as well as the option to create a custom script to draw your SVG in whatever way you like. 8 | 9 | Available via: 10 | 11 | - [NPM](https://www.npmjs.com/package/vivus): `npm install vivus` 12 | - [Bower](http://bower.io/): `bower install vivus` 13 | - [jsDelivr CDN](http://www.jsdelivr.com/#!vivus): `//cdn.jsdelivr.net/npm/vivus@latest/dist/vivus.min.js` 14 | - [CDNJS CDN](https://cdnjs.com/libraries/vivus) 15 | - [WebJars](http://www.webjars.org/) 16 | 17 | Join the conversation on [Gitter](https://gitter.im/maxwellito/vivus) 18 | 19 | Try Vivus with your SVG on [Vivus Instant](https://maxwellito.github.io/vivus-instant/). If you plan to use the library to animate a single SVG without callback or controls, this will allow you to download your animated SVG, powered by CSS, JavaScript free. 20 | 21 | ## Animations 22 | 23 | On the following images, the pink color represents the `duration` value, and the blue one is for `delay` value. 24 | 25 | ### Delayed 26 | 27 | ![Timeline for delayed animation](https://raw.github.com/maxwellito/vivus/master/assets/delayed.png) 28 | 29 | Every path element is drawn at the same time with a small delay at the start. This is currently the default animation. 30 | 31 | ### Sync 32 | 33 | ![Timeline for sync animation](https://raw.github.com/maxwellito/vivus/master/assets/sync.png) 34 | 35 | Each line is drawn synchronously. They all start and finish at the same time, hence the name `sync`. 36 | 37 | ### OneByOne 38 | 39 | ![Timeline for oneByOne animation](https://raw.github.com/maxwellito/vivus/master/assets/oneByOne.png) 40 | 41 | Each path element is drawn one after the other. This animation gives the best impression of live drawing. The duration for each line depends on their length to make a constant drawing speed. 42 | 43 | ## Principles 44 | 45 | To get this effect, the script uses the CSS property `strokeDashoffset`. This property manages the stroke offset on every line of the SVG. Now, all we have to do is add some JavaScript to update this value progressively and the magic begins. 46 | 47 | However, there's a problem with this. The `strokeDashoffset` property is only available on the path elements. This is an issue because in an SVG there are a lot of elements such as `circle`, `rect`, `line` and `polyline` which will break the animation. So to fix this, there is another class available in the repo called `pathformer`. It's made for transforming all objects of your SVG into `path` elements to be able to use `strokeDashoffset` and animate your SVGs. 48 | 49 | _The animation always draws elements in the same order as they are defined in the SVG tag._ 50 | 51 | There are few conditions that your SVG must meet: 52 | 53 | - All elements must have a stroke property and cannot be filled. This is because the animation only looks to progressively draw strokes and will not check for filled colours. For example: fill: "none"; stroke: "#FFF"; 54 | 55 | - You should avoid creating any hidden path elements in your SVG. Vivus considers them all eligible to be animated, so it is advised to remove them before playing with it. If they are not removed the animation might not achieve the desired effect, with blank areas and gaps appearing. 56 | 57 | - `text` elements aren't allowed, they cannot be transformed into `path` elements. See [#22](https://github.com/maxwellito/vivus/issues/22) for more details. 58 | 59 | The code is inspired from other repositories. The drawer is inspired from the excellent [Codrops](http://tympanus.net/codrops/) about the post [SVG Drawing Animation](http://tympanus.net/codrops/2013/12/30/svg-drawing-animation/) (if you don't know this website, get ready to have your mind blown). Then for the pathformer, there is a lot of work from [SVGPathConverter](https://github.com/Waest/SVGPathConverter) by [Waest](https://github.com/Waest). 60 | 61 | ## Usage 62 | 63 | As I said, no dependencies here. All you need to do is include the scripts. 64 | 65 | **Inline SVG** 66 | 67 | ```html 68 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | ``` 78 | 79 | **Dynamic load** 80 | 81 | ```html 82 | 83 | 84 | 87 | ``` 88 | 89 | or 90 | 91 | ```html 92 |
93 | 94 | 97 | ``` 98 | 99 | By default the `object` created will take the size of the parent element, this one must have a height and width or your SVG might not appear. 100 | 101 | If you need to edit this object, it is accessible in the `onReady` callback: 102 | 103 | ```js 104 | new Vivus('my-div-id', { 105 | file: 'link/to/my.svg', 106 | onReady: function (myVivus) { 107 | // `el` property is the SVG element 108 | myVivus.el.setAttribute('height', 'auto'); 109 | } 110 | }); 111 | ``` 112 | 113 | Check out the [hacks page](https://github.com/maxwellito/vivus/blob/master/hacks.md) for more tricks. 114 | 115 | ### Constructor 116 | 117 | The Vivus constructor asks for 3 parameters: 118 | 119 | - ID (or object) of DOM element to interact with.
It can be an inline SVG or a wrapper element to append an object tag from the option `file` 120 | - Option object (described in the following | 121 | - Callback to call at the end of the animation (optional) 122 | 123 | ### Option list 124 | 125 | | Name | Type | Description | 126 | | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 127 | | `type` | string | Defines what kind of animation will be used: `delayed`, `sync`, `oneByOne`, `script`, `scenario` or `scenario-sync`. [Default: `delayed`] | 128 | | `file` | string | Link to the SVG to animate. If set, Vivus will create an object tag and append it to the DOM element given to the constructor. Be careful, use the `onReady` callback before playing with the Vivus instance. | 129 | | `start` | string | Defines how to trigger the animation (`inViewport` once the SVG is in the viewport, `manual` gives you the freedom to call draw method to start, `autostart` makes it start right now). [Default: `inViewport`] | 130 | | `duration` | integer | Animation duration, in frames. [Default: `200`] | 131 | | `delay` | integer | Time between the drawing of first and last path, in frames (only for `delayed` animations). | 132 | | `onReady` | function | Function called when the instance is ready to play. | 133 | | `pathTimingFunction` | function | Timing animation function for each path element of the SVG. Check the [timing function part](#timing-function). | 134 | | `animTimingFunction` | function | Timing animation function for the complete SVG. Check the [timing function part](#timing-function). | 135 | | `dashGap` | integer | Whitespace extra margin between dashes. Increase it in case of glitches at the initial state of the animation. [Default: `2`] | 136 | | `forceRender` | boolean | Force the browser to re-render all updated path items. By default, the value is `true` on IE only. (check the 'troubleshoot' section for more details). | 137 | | `reverseStack` | boolean | Reverse the order of execution. The default behaviour is to render from the first 'path' in the SVG to the last one. This option allow you to reverse the order. [Default: `false`] | 138 | | `selfDestroy` | boolean | Removes all extra styling on the SVG, and leaves it as original. | 139 | 140 | ### Methods 141 | 142 | | Name | Description | 143 | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 144 | | `play(speed, callback)` | Plays the animation with the speed given in parameter. This value can be negative to go backward, between 0 and 1 to go slowly, >1 to go faster, or <0 to go in reverse from current state. [Default: `1`]. Callback executed after the animation is finished (optional) | 145 | | `stop()` | Stops the animation. | 146 | | `reset()` | Reinitialises the SVG to the original state: undrawn. | 147 | | `finish()` | Set the SVG to the final state: drawn. | 148 | | `setFrameProgress(progress)` | Set the progress of the animation. Progress must be a number between 0 and 1. | 149 | | `getStatus()` | Get the status of the animation between `start`, `progress`, `end` | 150 | | `destroy()` | Reset the SVG but make the instance out of order. | 151 | 152 | These methods return the object so you can chain the actions. 153 | 154 | ```js 155 | const myVivus = new Vivus('my-svg-id'); 156 | myVivus.stop().reset().play(2); 157 | ``` 158 | 159 | #### Play method callback 160 | 161 | Instead of using the global constructor callback when you create the Vivus object, you can add callbacks to be 162 | executed for specific `play` method calls. 163 | 164 | ```js 165 | const myVivus = new Vivus('my-svg-id'); 166 | myVivus.play(1, function () { 167 | // called after the animation completes 168 | }); 169 | 170 | // alternativly if you leave the speed param blank and use the default, you 171 | // can pass the callback as the first parameter like so. 172 | myVivus.play(function () { 173 | // called after the animation completes 174 | }); 175 | ``` 176 | 177 | ## Timing function 178 | 179 | To give more freedom, it's possible to override the animation of each path and/or the entire SVG. It works a bit like the CSS animation timing function. But instead of using a cubic-bezier function, it use a simple JavaScript function. It must accept a number as parameter (between 0 to 1), then return a number (also between 0 and 1). It's a hook. 180 | 181 | If you don't want to create your own, timing methods are available via the constructor object: `EASE`, `EASE_IN`, `EASE_OUT` and `EASE_OUT_BOUNCE`. Then set it in the option object to enjoy them. 182 | 183 | ```js 184 | // Here, the ease animation will be use for the global drawing. 185 | new Vivus( 186 | 'my-svg-id', 187 | { 188 | type: 'delayed', 189 | duration: 200, 190 | animTimingFunction: Vivus.EASE 191 | }, 192 | myCallback 193 | ); 194 | ``` 195 | 196 | **WARNING**: `animTimingFunction` is called at every frame of the animation, and `pathTimingFunction` is also called at every frame for each path of your SVG. So be careful about them. Keep it simple, or it can affect the performance. 197 | 198 | ## Extra attributes 199 | 200 | The attribute `data-ignore` allows you to ignore path tags from the vivus animation. 201 | 202 | ```html 203 | 204 | 205 | 206 | 207 | 208 | ``` 209 | 210 | In this case, the second path won't be part of the animation. 211 | 212 | ## Scenarize 213 | 214 | This feature allows you to script the animation of your SVG. For this, the custom values will be set directly in the DOM of the SVG. 215 | 216 | ### `scenario` 217 | 218 | This type is easier to understand, but longer to implement. You just have to define the start and duration of each element with `data-start` and `data-duration` attributes. If it is missing, it will use the default value given to the constructor. 219 | The best part of this type is the flexibility it provides. You don't have to respect the order/stack of the SVG and you can start with the last element, then continue with the first to finish with all the rest at the same time. 220 | 221 | You will then have to define custom rules for each element in your SVG via extra attributes in your SVG DOM : 222 | 223 | - `data-start` (integer) 224 | time when the animation must start, in frames 225 | - `data-duration` (integer) 226 | animation duration of this path, in frames 227 | 228 | ```html 229 | 230 | 231 | 232 | 233 | 234 | 235 | ``` 236 | 237 | ### `scenario-sync` 238 | 239 | It's not the sexiest code ever, but it's quite flexible. In addition to this, the behaviour is fairly different. 240 | By using this animation type, the default behaviour is the same as `oneByOne`. However, you can define some properties on a specific path item such as the duration, the delay to start (from the end of the previous path) and if it should be played synchronously. 241 | 242 | - `data-delay` (integer) 243 | time between the end of the animation of the previous path and the start of the current path, in frames 244 | - `data-duration` (integer) 245 | duration of this path animation, in frames 246 | - `data-async` (no value required) 247 | make the drawing of this path asynchronous. It means the next path will start at the same time. 248 | If a path does not have an attribute for duration or delay then the default values, set in the options, will be used. 249 | 250 | Example: here is a simple SVG containing 5 elements. With the following options `{duration: 20, delay: 0}`, we should get this timeline 251 | 252 | ![Timeline for script animation by default](https://raw.github.com/maxwellito/vivus/master/assets/script_default.png) 253 | 254 | This looks like 'oneByOne' animation, synchronous mode. But to make it a bit custom, here is what I can do: 255 | 256 | ```html 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | ``` 265 | 266 | This scenario should give us 267 | 268 | ![Timeline for this custom script animation](https://raw.github.com/maxwellito/vivus/master/assets/script_custom.png) 269 | 270 | I'm sorry if it does not look very sexy, and it's not really easy to use. I'm happy to make any changes, as long as the idea sounds interesting. Post an issue and I'll be very happy to talk about it! 271 | 272 | ## Non Scaling 273 | 274 | Some SVG elements might use non scaling properties such as `vector-effect="non-scaling-stroke"`, which requires some additional custom logic. On instance construction Vivus will map all the child elements in the SVG and calculate their line length. If the element is resized during the animation, the calculated stroke style properties become invalid and the SVG will display incorrectly. 275 | 276 | To keep animation consistency, the method `recalc` should be called when the SVG is resized. It will re-calculate the line length on affected child elements on the next frame calculation. 277 | 278 | Code example: 279 | 280 | ```js 281 | // Create your Vivus instance 282 | const vivusObject = new Vivus('my-div', { 283 | duration: 200, 284 | file: 'link/to/my.svg', 285 | }); 286 | 287 | // Create your observer and set up a callback on resize 288 | const resizeObserver = new ResizeObserver((entries) => { 289 | // Recalculate the line lengths 290 | vivusObject.recalc(); 291 | }); 292 | 293 | resizeObserver.observe(vivusObject.el); 294 | ``` 295 | 296 | Vivus will provide a warning in the console when it detects stroke scaling. 297 | 298 | ## Development 299 | 300 | To make it simpler a gulp file is set up to automise minifying, JShint and tests. 301 | If you have never used Gulp before this is a good opportunity. To use it, you need to install NodeJS first then run `sudo npm install -g gulp`. 302 | 303 | To start, you will need to install the repo dependencies: 304 | 305 | ```bash 306 | $ npm install 307 | ``` 308 | 309 | Then you can use NPM scripts to run the following tasks: 310 | 311 | - `build` make the build (generate `dist/vivus.js` and `dist/vivus.min.js`) 312 | - `lint` run ESlint on the source files 313 | - `test` run Karma 314 | 315 | ## Troubleshoot 316 | 317 | ### Internet Explorer 318 | 319 | Some SVG weren't working at all. The only solution found was to clone and replace each updated path element. Of course this solution requires more resources and a lot of DOM manipulation, but it will give a smooth animation like other browsers. This fallback is only applied on Internet Explorer (all versions), and can be disabled via the option `forceRender`. 320 | 321 | Replacing each updated path by a clone was the only way to force IE to re-render the SVG. On some SVGs this trick is not necessary, but IE can be a bit tricky with this. If you're worried about performance, I would recommend checking if your SVG works correctly by disabling the `forceRender` option. If it works correctly on IE, then keep it like this. 322 | 323 | By default, `forceRender` is `true` on Internet Explorer only. 324 | 325 | ### Firefox 326 | 327 | For Firefox users, you might encounter some glitches depending on your SVG and browser version. On versions before 36, there is a problem retrieving path length via `getTotalLength` method. Returning 174321516544 instead of 209 (I'm not exaggerating, this comes from a real case), messing up the entire animation treatment. Unfortunately, there's nothing that this library can do, this is due to Firefox. I hope to find a workaround, but at the moment I can only recommend that you test your animation on previous versions of Firefox. 328 | 329 | ## Debug 330 | 331 | For an easier debug have a look to the attribute `map` of your Vivus object. This contains the mapping of your animation. If you're using a modern browser, I recommend `console.table` to get a nice output of the array which will make your debug easier. 332 | 333 | ```javascript 334 | const logo = new Vivus('myLogo', { type: 'scenario-sync' }); 335 | 336 | // The property 'map' contain all the SVG mapping 337 | console.table(logo.map); 338 | ``` 339 | 340 | ## Special thanks! 341 | 342 | Thanks to all contributors! Also users who pushed me to improve the library by publishing it on NPM, or browser compatibility or features. Also thanks for fixing my awful english :) 343 | 344 | - [@jolic](https://github.com/jolic) for dynamic SVG loading, ignore invisible paths, infinite and beyond... 345 | - [@BenMcGeachy](https://github.com/BenMcGeachy) for making the documentation understandable 346 | - [@TranscendOfSypherus](https://github.com/TranscendOfSypherus) for fixing the PathFormer 347 | - [@flyingfisch](https://github.com/flyingfisch) for general helping with issues 348 | - [@morgangiraud](https://github.com/morgangiraud) on the ignore invisible paths 349 | - [@Nerdissimo](https://github.com/Nerdissimo) for inserting SVG without `object` wrapper 350 | - [@jsimnz](https://github.com/jsimnz) for adding callbacks to play method 351 | 352 | and many others... 353 | -------------------------------------------------------------------------------- /src/_build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var pkg = require('../package.json'); 3 | var vivus = fs.readFileSync('src/vivus.js', { encoding: 'utf8' }); 4 | var pathformer = fs.readFileSync('src/pathformer.js', { encoding: 'utf8' }); 5 | 6 | var output = `/** 7 | * ${pkg.name} - ${pkg.description} 8 | * @version v${pkg.version} 9 | * @link ${pkg.homepage} 10 | * @license ${pkg.license} 11 | */ 12 | 13 | (function () { 14 | 15 | ${pathformer} 16 | ${vivus} 17 | 18 | if (typeof define === 'function' && define.amd) { 19 | // AMD. Register as an anonymous module. 20 | define([], function() { 21 | return Vivus; 22 | }); 23 | } else if (typeof exports === 'object') { 24 | // Node. Does not work with strict CommonJS, but 25 | // only CommonJS-like environments that support module.exports, 26 | // like Node. 27 | module.exports = Vivus; 28 | } else { 29 | // Browser globals 30 | window.Vivus = Vivus; 31 | } 32 | 33 | }()); 34 | `; 35 | 36 | console.log(output); 37 | -------------------------------------------------------------------------------- /src/pathformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pathformer 5 | * Beta version 6 | * 7 | * Take any SVG version 1.1 and transform 8 | * child elements to 'path' elements 9 | * 10 | * This code is purely forked from 11 | * https://github.com/Waest/SVGPathConverter 12 | */ 13 | 14 | /** 15 | * Class constructor 16 | * 17 | * @param {DOM|String} element Dom element of the SVG or id of it 18 | */ 19 | function Pathformer(element) { 20 | // Test params 21 | if (typeof element === 'undefined') { 22 | throw new Error('Pathformer [constructor]: "element" parameter is required'); 23 | } 24 | 25 | // Set the element 26 | if (element.constructor === String) { 27 | element = document.getElementById(element); 28 | if (!element) { 29 | throw new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID'); 30 | } 31 | } 32 | if (element instanceof window.SVGElement || 33 | element instanceof window.SVGGElement || 34 | /^svg$/i.test(element.nodeName)) { 35 | this.el = element; 36 | } else { 37 | throw new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'); 38 | } 39 | 40 | // Start 41 | this.scan(element); 42 | } 43 | 44 | /** 45 | * List of tags which can be transformed 46 | * to path elements 47 | * 48 | * @type {Array} 49 | */ 50 | Pathformer.prototype.TYPES = ['line', 'ellipse', 'circle', 'polygon', 'polyline', 'rect']; 51 | 52 | /** 53 | * List of attribute names which contain 54 | * data. This array list them to check if 55 | * they contain bad values, like percentage. 56 | * 57 | * @type {Array} 58 | */ 59 | Pathformer.prototype.ATTR_WATCH = ['cx', 'cy', 'points', 'r', 'rx', 'ry', 'x', 'x1', 'x2', 'y', 'y1', 'y2']; 60 | 61 | /** 62 | * Finds the elements compatible for transform 63 | * and apply the liked method 64 | * 65 | * @param {object} options Object from the constructor 66 | */ 67 | Pathformer.prototype.scan = function (svg) { 68 | var fn, element, pathData, pathDom, 69 | elements = svg.querySelectorAll(this.TYPES.join(',')); 70 | 71 | for (var i = 0; i < elements.length; i++) { 72 | element = elements[i]; 73 | fn = this[element.tagName.toLowerCase() + 'ToPath']; 74 | pathData = fn(this.parseAttr(element.attributes)); 75 | pathDom = this.pathMaker(element, pathData); 76 | element.parentNode.replaceChild(pathDom, element); 77 | } 78 | }; 79 | 80 | 81 | /** 82 | * Read `line` element to extract and transform 83 | * data, to make it ready for a `path` object. 84 | * 85 | * @param {DOMelement} element Line element to transform 86 | * @return {object} Data for a `path` element 87 | */ 88 | Pathformer.prototype.lineToPath = function (element) { 89 | var newElement = {}, 90 | x1 = element.x1 || 0, 91 | y1 = element.y1 || 0, 92 | x2 = element.x2 || 0, 93 | y2 = element.y2 || 0; 94 | 95 | newElement.d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2; 96 | return newElement; 97 | }; 98 | 99 | /** 100 | * Read `rect` element to extract and transform 101 | * data, to make it ready for a `path` object. 102 | * The radius-border is not taken in charge yet. 103 | * (your help is more than welcomed) 104 | * 105 | * @param {DOMelement} element Rect element to transform 106 | * @return {object} Data for a `path` element 107 | */ 108 | Pathformer.prototype.rectToPath = function (element) { 109 | var newElement = {}, 110 | x = parseFloat(element.x) || 0, 111 | y = parseFloat(element.y) || 0, 112 | width = parseFloat(element.width) || 0, 113 | height = parseFloat(element.height) || 0; 114 | 115 | if (element.rx || element.ry) { 116 | var rx = parseInt(element.rx, 10) || -1, 117 | ry = parseInt(element.ry, 10) || -1; 118 | rx = Math.min(Math.max(rx < 0 ? ry : rx, 0), width/2); 119 | ry = Math.min(Math.max(ry < 0 ? rx : ry, 0), height/2); 120 | 121 | newElement.d = 'M ' + (x + rx) + ',' + y + ' ' + 122 | 'L ' + (x + width - rx) + ',' + y + ' ' + 123 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + width) + ',' + (y + ry) + ' ' + 124 | 'L ' + (x + width) + ',' + (y + height - ry) + ' ' + 125 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + width - rx) + ',' + (y + height) + ' ' + 126 | 'L ' + (x + rx) + ',' + (y + height) + ' ' + 127 | 'A ' + rx + ',' + ry + ',0,0,1,' + x + ',' + (y + height - ry) + ' ' + 128 | 'L ' + x + ',' + (y + ry) + ' ' + 129 | 'A ' + rx + ',' + ry + ',0,0,1,' + (x + rx) + ',' + y; 130 | } 131 | else { 132 | newElement.d = 'M' + x + ' ' + y + ' ' + 133 | 'L' + (x + width) + ' ' + y + ' ' + 134 | 'L' + (x + width) + ' ' + (y + height) + ' ' + 135 | 'L' + x + ' ' + (y + height) + ' Z'; 136 | } 137 | return newElement; 138 | }; 139 | 140 | /** 141 | * Read `polyline` element to extract and transform 142 | * data, to make it ready for a `path` object. 143 | * 144 | * @param {DOMelement} element Polyline element to transform 145 | * @return {object} Data for a `path` element 146 | */ 147 | Pathformer.prototype.polylineToPath = function (element) { 148 | var newElement = {}, 149 | points = element.points.trim().split(' '), 150 | i, path; 151 | 152 | // Reformatting if points are defined without commas 153 | if (element.points.indexOf(',') === -1) { 154 | var formattedPoints = []; 155 | for (i = 0; i < points.length; i+=2) { 156 | formattedPoints.push(points[i] + ',' + points[i+1]); 157 | } 158 | points = formattedPoints; 159 | } 160 | 161 | // Generate the path.d value 162 | path = 'M' + points[0]; 163 | for(i = 1; i < points.length; i++) { 164 | if (points[i].indexOf(',') !== -1) { 165 | path += 'L' + points[i]; 166 | } 167 | } 168 | newElement.d = path; 169 | return newElement; 170 | }; 171 | 172 | /** 173 | * Read `polygon` element to extract and transform 174 | * data, to make it ready for a `path` object. 175 | * This method rely on polylineToPath, because the 176 | * logic is similar. The path created is just closed, 177 | * so it needs an 'Z' at the end. 178 | * 179 | * @param {DOMelement} element Polygon element to transform 180 | * @return {object} Data for a `path` element 181 | */ 182 | Pathformer.prototype.polygonToPath = function (element) { 183 | var newElement = Pathformer.prototype.polylineToPath(element); 184 | 185 | newElement.d += 'Z'; 186 | return newElement; 187 | }; 188 | 189 | /** 190 | * Read `ellipse` element to extract and transform 191 | * data, to make it ready for a `path` object. 192 | * 193 | * @param {DOMelement} element ellipse element to transform 194 | * @return {object} Data for a `path` element 195 | */ 196 | Pathformer.prototype.ellipseToPath = function (element) { 197 | var newElement = {}, 198 | rx = parseFloat(element.rx) || 0, 199 | ry = parseFloat(element.ry) || 0, 200 | cx = parseFloat(element.cx) || 0, 201 | cy = parseFloat(element.cy) || 0, 202 | startX = cx - rx, 203 | startY = cy, 204 | endX = parseFloat(cx) + parseFloat(rx), 205 | endY = cy; 206 | 207 | newElement.d = 'M' + startX + ',' + startY + 208 | 'A' + rx + ',' + ry + ' 0,1,1 ' + endX + ',' + endY + 209 | 'A' + rx + ',' + ry + ' 0,1,1 ' + startX + ',' + endY; 210 | return newElement; 211 | }; 212 | 213 | /** 214 | * Read `circle` element to extract and transform 215 | * data, to make it ready for a `path` object. 216 | * 217 | * @param {DOMelement} element Circle element to transform 218 | * @return {object} Data for a `path` element 219 | */ 220 | Pathformer.prototype.circleToPath = function (element) { 221 | var newElement = {}, 222 | r = parseFloat(element.r) || 0, 223 | cx = parseFloat(element.cx) || 0, 224 | cy = parseFloat(element.cy) || 0, 225 | startX = cx - r, 226 | startY = cy, 227 | endX = parseFloat(cx) + parseFloat(r), 228 | endY = cy; 229 | 230 | newElement.d = 'M' + startX + ',' + startY + 231 | 'A' + r + ',' + r + ' 0,1,1 ' + endX + ',' + endY + 232 | 'A' + r + ',' + r + ' 0,1,1 ' + startX + ',' + endY; 233 | return newElement; 234 | }; 235 | 236 | /** 237 | * Create `path` elements form original element 238 | * and prepared objects 239 | * 240 | * @param {DOMelement} element Original element to transform 241 | * @param {object} pathData Path data (from `toPath` methods) 242 | * @return {DOMelement} Path element 243 | */ 244 | Pathformer.prototype.pathMaker = function (element, pathData) { 245 | var i, attr, pathTag = document.createElementNS('http://www.w3.org/2000/svg','path'); 246 | for(i = 0; i < element.attributes.length; i++) { 247 | attr = element.attributes[i]; 248 | if (this.ATTR_WATCH.indexOf(attr.name) === -1) { 249 | pathTag.setAttribute(attr.name, attr.value); 250 | } 251 | } 252 | for(i in pathData) { 253 | pathTag.setAttribute(i, pathData[i]); 254 | } 255 | return pathTag; 256 | }; 257 | 258 | /** 259 | * Parse attributes of a DOM element to 260 | * get an object of attribute => value 261 | * 262 | * @param {NamedNodeMap} attributes Attributes object from DOM element to parse 263 | * @return {object} Object of attributes 264 | */ 265 | Pathformer.prototype.parseAttr = function (element) { 266 | var attr, output = {}; 267 | for (var i = 0; i < element.length; i++) { 268 | attr = element[i]; 269 | // Check if no data attribute contains '%', or the transformation is impossible 270 | if (this.ATTR_WATCH.indexOf(attr.name) !== -1 && attr.value.indexOf('%') !== -1) { 271 | throw new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.'); 272 | } 273 | output[attr.name] = attr.value; 274 | } 275 | return output; 276 | }; 277 | -------------------------------------------------------------------------------- /src/vivus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var setupEnv, requestAnimFrame, cancelAnimFrame, parsePositiveInt; 4 | 5 | /** 6 | * Vivus 7 | * Beta version 8 | * 9 | * Take any SVG and make the animation 10 | * to give give the impression of live drawing 11 | * 12 | * This in more than just inspired from codrops 13 | * At that point, it's a pure fork. 14 | */ 15 | 16 | /** 17 | * Class constructor 18 | * option structure 19 | * type: 'delayed'|'sync'|'oneByOne'|'script' (to know if the items must be drawn synchronously or not, default: delayed) 20 | * duration: (in frames) 21 | * start: 'inViewport'|'manual'|'autostart' (start automatically the animation, default: inViewport) 22 | * delay: (delay between the drawing of first and last path) 23 | * dashGap whitespace extra margin between dashes 24 | * pathTimingFunction timing animation function for each path element of the SVG 25 | * animTimingFunction timing animation function for the complete SVG 26 | * forceRender force the browser to re-render all updated path items 27 | * selfDestroy removes all extra styling on the SVG, and leaves it as original 28 | * 29 | * The attribute 'type' is by default on 'delayed'. 30 | * - 'delayed' 31 | * all paths are draw at the same time but with a 32 | * little delay between them before start 33 | * - 'sync' 34 | * all path are start and finish at the same time 35 | * - 'oneByOne' 36 | * only one path is draw at the time 37 | * the end of the first one will trigger the draw 38 | * of the next one 39 | * 40 | * All these values can be overwritten individually 41 | * for each path item in the SVG 42 | * The value of frames will always take the advantage of 43 | * the duration value. 44 | * If you fail somewhere, an error will be thrown. 45 | * Good luck. 46 | * 47 | * @constructor 48 | * @this {Vivus} 49 | * @param {DOM|String} element Dom element of the SVG or id of it 50 | * @param {Object} options Options about the animation 51 | * @param {Function} callback Callback for the end of the animation 52 | */ 53 | function Vivus(element, options, callback) { 54 | setupEnv(); 55 | 56 | // Setup 57 | this.isReady = false; 58 | this.setElement(element, options); 59 | this.setOptions(options); 60 | this.setCallback(callback); 61 | 62 | if (this.isReady) { 63 | this.init(); 64 | } 65 | } 66 | 67 | /** 68 | * Timing functions 69 | ************************************** 70 | * 71 | * Default functions to help developers. 72 | * It always take a number as parameter (between 0 to 1) then 73 | * return a number (between 0 and 1) 74 | */ 75 | Vivus.LINEAR = function(x) { 76 | return x; 77 | }; 78 | Vivus.EASE = function(x) { 79 | return -Math.cos(x * Math.PI) / 2 + 0.5; 80 | }; 81 | Vivus.EASE_OUT = function(x) { 82 | return 1 - Math.pow(1 - x, 3); 83 | }; 84 | Vivus.EASE_IN = function(x) { 85 | return Math.pow(x, 3); 86 | }; 87 | Vivus.EASE_OUT_BOUNCE = function(x) { 88 | var base = -Math.cos(x * (0.5 * Math.PI)) + 1, 89 | rate = Math.pow(base, 1.5), 90 | rateR = Math.pow(1 - x, 2), 91 | progress = -Math.abs(Math.cos(rate * (2.5 * Math.PI))) + 1; 92 | return 1 - rateR + progress * rateR; 93 | }; 94 | 95 | /** 96 | * Setters 97 | ************************************** 98 | */ 99 | 100 | /** 101 | * Check and set the element in the instance 102 | * The method will not return anything, but will throw an 103 | * error if the parameter is invalid 104 | * 105 | * @param {DOM|String} element SVG Dom element or id of it 106 | */ 107 | Vivus.prototype.setElement = function(element, options) { 108 | var onLoad, self; 109 | 110 | // Basic check 111 | if (typeof element === 'undefined') { 112 | throw new Error('Vivus [constructor]: "element" parameter is required'); 113 | } 114 | 115 | // Set the element 116 | if (element.constructor === String) { 117 | element = document.getElementById(element); 118 | if (!element) { 119 | throw new Error( 120 | 'Vivus [constructor]: "element" parameter is not related to an existing ID' 121 | ); 122 | } 123 | } 124 | this.parentEl = element; 125 | 126 | // Load the SVG with XMLHttpRequest and extract the SVG 127 | if (options && options.file) { 128 | self = this; 129 | onLoad = function() { 130 | var domSandbox = document.createElement('div'); 131 | domSandbox.innerHTML = this.responseText; 132 | 133 | var svgTag = domSandbox.querySelector('svg'); 134 | if (!svgTag) { 135 | throw new Error( 136 | 'Vivus [load]: Cannot find the SVG in the loaded file : ' + 137 | options.file 138 | ); 139 | } 140 | 141 | self.el = svgTag; 142 | self.el.setAttribute('width', '100%'); 143 | self.el.setAttribute('height', '100%'); 144 | self.parentEl.appendChild(self.el); 145 | self.isReady = true; 146 | self.init(); 147 | self = null; 148 | }; 149 | 150 | var oReq = new window.XMLHttpRequest(); 151 | oReq.addEventListener('load', onLoad); 152 | oReq.open('GET', options.file); 153 | oReq.send(); 154 | return; 155 | } 156 | 157 | switch (element.constructor) { 158 | case window.SVGSVGElement: 159 | case window.SVGElement: 160 | case window.SVGGElement: 161 | this.el = element; 162 | this.isReady = true; 163 | break; 164 | 165 | case window.HTMLObjectElement: 166 | self = this; 167 | onLoad = function(e) { 168 | if (self.isReady) { 169 | return; 170 | } 171 | self.el = 172 | element.contentDocument && 173 | element.contentDocument.querySelector('svg'); 174 | if (!self.el && e) { 175 | throw new Error( 176 | 'Vivus [constructor]: object loaded does not contain any SVG' 177 | ); 178 | } else if (self.el) { 179 | if (element.getAttribute('built-by-vivus')) { 180 | self.parentEl.insertBefore(self.el, element); 181 | self.parentEl.removeChild(element); 182 | self.el.setAttribute('width', '100%'); 183 | self.el.setAttribute('height', '100%'); 184 | } 185 | self.isReady = true; 186 | self.init(); 187 | self = null; 188 | } 189 | }; 190 | 191 | if (!onLoad()) { 192 | element.addEventListener('load', onLoad); 193 | } 194 | break; 195 | 196 | default: 197 | throw new Error( 198 | 'Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)' 199 | ); 200 | } 201 | }; 202 | 203 | /** 204 | * Set up user option to the instance 205 | * The method will not return anything, but will throw an 206 | * error if the parameter is invalid 207 | * 208 | * @param {object} options Object from the constructor 209 | */ 210 | Vivus.prototype.setOptions = function(options) { 211 | var allowedTypes = [ 212 | 'delayed', 213 | 'sync', 214 | 'async', 215 | 'nsync', 216 | 'oneByOne', 217 | 'scenario', 218 | 'scenario-sync' 219 | ]; 220 | var allowedStarts = ['inViewport', 'manual', 'autostart']; 221 | 222 | // Basic check 223 | if (options !== undefined && options.constructor !== Object) { 224 | throw new Error( 225 | 'Vivus [constructor]: "options" parameter must be an object' 226 | ); 227 | } else { 228 | options = options || {}; 229 | } 230 | 231 | // Set the animation type 232 | if (options.type && allowedTypes.indexOf(options.type) === -1) { 233 | throw new Error( 234 | 'Vivus [constructor]: ' + 235 | options.type + 236 | ' is not an existing animation `type`' 237 | ); 238 | } else { 239 | this.type = options.type || allowedTypes[0]; 240 | } 241 | 242 | // Set the start type 243 | if (options.start && allowedStarts.indexOf(options.start) === -1) { 244 | throw new Error( 245 | 'Vivus [constructor]: ' + 246 | options.start + 247 | ' is not an existing `start` option' 248 | ); 249 | } else { 250 | this.start = options.start || allowedStarts[0]; 251 | } 252 | 253 | this.isIE = 254 | window.navigator.userAgent.indexOf('MSIE') !== -1 || 255 | window.navigator.userAgent.indexOf('Trident/') !== -1 || 256 | window.navigator.userAgent.indexOf('Edge/') !== -1; 257 | this.duration = parsePositiveInt(options.duration, 120); 258 | this.delay = parsePositiveInt(options.delay, null); 259 | this.dashGap = parsePositiveInt(options.dashGap, 1); 260 | this.forceRender = options.hasOwnProperty('forceRender') 261 | ? !!options.forceRender 262 | : this.isIE; 263 | this.reverseStack = !!options.reverseStack; 264 | this.selfDestroy = !!options.selfDestroy; 265 | this.onReady = options.onReady; 266 | this.map = []; 267 | this.frameLength = this.currentFrame = this.delayUnit = this.speed = this.handle = null; 268 | 269 | this.ignoreInvisible = options.hasOwnProperty('ignoreInvisible') 270 | ? !!options.ignoreInvisible 271 | : false; 272 | 273 | this.animTimingFunction = options.animTimingFunction || Vivus.LINEAR; 274 | this.pathTimingFunction = options.pathTimingFunction || Vivus.LINEAR; 275 | 276 | if (this.delay >= this.duration) { 277 | throw new Error('Vivus [constructor]: delay must be shorter than duration'); 278 | } 279 | }; 280 | 281 | /** 282 | * Set up callback to the instance 283 | * The method will not return enything, but will throw an 284 | * error if the parameter is invalid 285 | * 286 | * @param {Function} callback Callback for the animation end 287 | */ 288 | Vivus.prototype.setCallback = function(callback) { 289 | // Basic check 290 | if (!!callback && callback.constructor !== Function) { 291 | throw new Error( 292 | 'Vivus [constructor]: "callback" parameter must be a function' 293 | ); 294 | } 295 | this.callback = callback || function() {}; 296 | }; 297 | 298 | /** 299 | * Core 300 | ************************************** 301 | */ 302 | 303 | /** 304 | * Map the svg, path by path. 305 | * The method return nothing, it just fill the 306 | * `map` array. Each item in this array represent 307 | * a path element from the SVG, with informations for 308 | * the animation. 309 | * 310 | * ``` 311 | * [ 312 | * { 313 | * el: the path element 314 | * length: length of the path line 315 | * startAt: time start of the path animation (in frames) 316 | * duration: path animation duration (in frames) 317 | * }, 318 | * ... 319 | * ] 320 | * ``` 321 | * 322 | */ 323 | Vivus.prototype.mapping = function() { 324 | var i, paths, path, pAttrs, pathObj, totalLength, lengthMeter, timePoint, scale, hasNonScale; 325 | timePoint = totalLength = lengthMeter = 0; 326 | paths = this.el.querySelectorAll('path'); 327 | hasNonScale = false; 328 | 329 | for (i = 0; i < paths.length; i++) { 330 | path = paths[i]; 331 | if (this.isInvisible(path)) { 332 | continue; 333 | } 334 | 335 | pathObj = { 336 | el: path, 337 | length: 0, 338 | startAt: 0, 339 | duration: 0, 340 | isResizeSensitive: false 341 | }; 342 | 343 | // If vector effect is non-scaling-stroke, the total length won't match the rendered length 344 | // so we need to calculate the scale and apply it 345 | if (path.getAttribute('vector-effect') === 'non-scaling-stroke') { 346 | var rect = path.getBoundingClientRect(); 347 | var box = path.getBBox(); 348 | scale = Math.max(rect.width / box.width, rect.height / box.height); 349 | pathObj.isResizeSensitive = true; 350 | hasNonScale = true; 351 | } else { 352 | scale = 1; 353 | } 354 | pathObj.length = Math.ceil(path.getTotalLength() * scale); 355 | 356 | // Test if the path length is correct 357 | if (isNaN(pathObj.length)) { 358 | if (window.console && console.warn) { 359 | console.warn( 360 | 'Vivus [mapping]: cannot retrieve a path element length', 361 | path 362 | ); 363 | } 364 | continue; 365 | } 366 | this.map.push(pathObj); 367 | path.style.strokeDasharray = 368 | pathObj.length + ' ' + (pathObj.length + this.dashGap * 2); 369 | path.style.strokeDashoffset = pathObj.length + this.dashGap; 370 | pathObj.length += this.dashGap; 371 | totalLength += pathObj.length; 372 | 373 | this.renderPath(i); 374 | } 375 | 376 | // Show a warning for non-scaling elements 377 | if (hasNonScale) { 378 | console.warn('Vivus: this SVG contains non-scaling-strokes. You should call instance.recalc() when the SVG is resized or you will encounter unwanted behaviour. See https://github.com/maxwellito/vivus#non-scaling for more info.'); 379 | } 380 | 381 | totalLength = totalLength === 0 ? 1 : totalLength; 382 | this.delay = this.delay === null ? this.duration / 3 : this.delay; 383 | this.delayUnit = this.delay / (paths.length > 1 ? paths.length - 1 : 1); 384 | 385 | // Reverse stack if asked 386 | if (this.reverseStack) { 387 | this.map.reverse(); 388 | } 389 | 390 | for (i = 0; i < this.map.length; i++) { 391 | pathObj = this.map[i]; 392 | 393 | switch (this.type) { 394 | case 'delayed': 395 | pathObj.startAt = this.delayUnit * i; 396 | pathObj.duration = this.duration - this.delay; 397 | break; 398 | 399 | case 'oneByOne': 400 | pathObj.startAt = (lengthMeter / totalLength) * this.duration; 401 | pathObj.duration = (pathObj.length / totalLength) * this.duration; 402 | break; 403 | 404 | case 'sync': 405 | case 'async': 406 | case 'nsync': 407 | pathObj.startAt = 0; 408 | pathObj.duration = this.duration; 409 | break; 410 | 411 | case 'scenario-sync': 412 | path = pathObj.el; 413 | pAttrs = this.parseAttr(path); 414 | pathObj.startAt = 415 | timePoint + 416 | (parsePositiveInt(pAttrs['data-delay'], this.delayUnit) || 0); 417 | pathObj.duration = parsePositiveInt( 418 | pAttrs['data-duration'], 419 | this.duration 420 | ); 421 | timePoint = 422 | pAttrs['data-async'] !== undefined 423 | ? pathObj.startAt 424 | : pathObj.startAt + pathObj.duration; 425 | this.frameLength = Math.max( 426 | this.frameLength, 427 | pathObj.startAt + pathObj.duration 428 | ); 429 | break; 430 | 431 | case 'scenario': 432 | path = pathObj.el; 433 | pAttrs = this.parseAttr(path); 434 | pathObj.startAt = 435 | parsePositiveInt(pAttrs['data-start'], this.delayUnit) || 0; 436 | pathObj.duration = parsePositiveInt( 437 | pAttrs['data-duration'], 438 | this.duration 439 | ); 440 | this.frameLength = Math.max( 441 | this.frameLength, 442 | pathObj.startAt + pathObj.duration 443 | ); 444 | break; 445 | } 446 | lengthMeter += pathObj.length; 447 | this.frameLength = this.frameLength || this.duration; 448 | } 449 | }; 450 | 451 | /** 452 | * Public method to re-evaluate line length for non-scaling lines 453 | * path elements. 454 | */ 455 | Vivus.prototype.recalc = function () { 456 | if (this.mustRecalcScale) { 457 | return; 458 | } 459 | this.mustRecalcScale = requestAnimFrame(function () { 460 | this.performLineRecalc(); 461 | }.bind(this)); 462 | } 463 | 464 | /** 465 | * Private method to re-evaluate line length on non-scaling 466 | * path elements. Then call for a trace to update the SVG. 467 | */ 468 | Vivus.prototype.performLineRecalc = function () { 469 | var pathObj, path, rect, box, scale; 470 | for (var i = 0; i < this.map.length; i++) { 471 | pathObj = this.map[i]; 472 | if (pathObj.isResizeSensitive) { 473 | path = pathObj.el; 474 | rect = path.getBoundingClientRect(); 475 | box = path.getBBox(); 476 | scale = Math.max(rect.width / box.width, rect.height / box.height); 477 | pathObj.length = Math.ceil(path.getTotalLength() * scale); 478 | path.style.strokeDasharray = pathObj.length + ' ' + (pathObj.length + this.dashGap * 2); 479 | } 480 | } 481 | this.trace(); 482 | this.mustRecalcScale = null; 483 | } 484 | 485 | /** 486 | * Interval method to draw the SVG from current 487 | * position of the animation. It update the value of 488 | * `currentFrame` and re-trace the SVG. 489 | * 490 | * It use this.handle to store the requestAnimationFrame 491 | * and clear it one the animation is stopped. So this 492 | * attribute can be used to know if the animation is 493 | * playing. 494 | * 495 | * Once the animation at the end, this method will 496 | * trigger the Vivus callback. 497 | * 498 | */ 499 | Vivus.prototype.draw = function() { 500 | var self = this; 501 | this.currentFrame += this.speed; 502 | 503 | if (this.currentFrame <= 0) { 504 | this.stop(); 505 | this.reset(); 506 | } else if (this.currentFrame >= this.frameLength) { 507 | this.stop(); 508 | this.currentFrame = this.frameLength; 509 | this.trace(); 510 | if (this.selfDestroy) { 511 | this.destroy(); 512 | } 513 | } else { 514 | this.trace(); 515 | this.handle = requestAnimFrame(function() { 516 | self.draw(); 517 | }); 518 | return; 519 | } 520 | 521 | this.callback(this); 522 | if (this.instanceCallback) { 523 | this.instanceCallback(this); 524 | this.instanceCallback = null; 525 | } 526 | }; 527 | 528 | /** 529 | * Draw the SVG at the current instant from the 530 | * `currentFrame` value. Here is where most of the magic is. 531 | * The trick is to use the `strokeDashoffset` style property. 532 | * 533 | * For optimisation reasons, a new property called `progress` 534 | * is added in each item of `map`. This one contain the current 535 | * progress of the path element. Only if the new value is different 536 | * the new value will be applied to the DOM element. This 537 | * method save a lot of resources to re-render the SVG. And could 538 | * be improved if the animation couldn't be played forward. 539 | * 540 | */ 541 | Vivus.prototype.trace = function() { 542 | var i, progress, path, currentFrame; 543 | currentFrame = 544 | this.animTimingFunction(this.currentFrame / this.frameLength) * 545 | this.frameLength; 546 | for (i = 0; i < this.map.length; i++) { 547 | path = this.map[i]; 548 | progress = (currentFrame - path.startAt) / path.duration; 549 | progress = this.pathTimingFunction(Math.max(0, Math.min(1, progress))); 550 | if (path.progress !== progress) { 551 | path.progress = progress; 552 | path.el.style.strokeDashoffset = Math.floor(path.length * (1 - progress)); 553 | this.renderPath(i); 554 | } 555 | } 556 | }; 557 | 558 | /** 559 | * Method forcing the browser to re-render a path element 560 | * from it's index in the map. Depending on the `forceRender` 561 | * value. 562 | * The trick is to replace the path element by it's clone. 563 | * This practice is not recommended because it's asking more 564 | * ressources, too much DOM manupulation.. 565 | * but it's the only way to let the magic happen on IE. 566 | * By default, this fallback is only applied on IE. 567 | * 568 | * @param {Number} index Path index 569 | */ 570 | Vivus.prototype.renderPath = function(index) { 571 | if (this.forceRender && this.map && this.map[index]) { 572 | var pathObj = this.map[index], 573 | newPath = pathObj.el.cloneNode(true); 574 | pathObj.el.parentNode.replaceChild(newPath, pathObj.el); 575 | pathObj.el = newPath; 576 | } 577 | }; 578 | 579 | /** 580 | * When the SVG object is loaded and ready, 581 | * this method will continue the initialisation. 582 | * 583 | * This this mainly due to the case of passing an 584 | * object tag in the constructor. It will wait 585 | * the end of the loading to initialise. 586 | * 587 | */ 588 | Vivus.prototype.init = function() { 589 | // Set object variables 590 | this.frameLength = 0; 591 | this.currentFrame = 0; 592 | this.map = []; 593 | 594 | // Start 595 | new Pathformer(this.el); 596 | this.mapping(); 597 | this.starter(); 598 | 599 | if (this.onReady) { 600 | this.onReady(this); 601 | } 602 | }; 603 | 604 | /** 605 | * Trigger to start of the animation. 606 | * Depending on the `start` value, a different script 607 | * will be applied. 608 | * 609 | * If the `start` value is not valid, an error will be thrown. 610 | * Even if technically, this is impossible. 611 | * 612 | */ 613 | Vivus.prototype.starter = function() { 614 | switch (this.start) { 615 | case 'manual': 616 | return; 617 | 618 | case 'autostart': 619 | this.play(); 620 | break; 621 | 622 | case 'inViewport': 623 | var self = this, 624 | listener = function() { 625 | if (self.isInViewport(self.parentEl, 1)) { 626 | self.play(); 627 | window.removeEventListener('scroll', listener); 628 | } 629 | }; 630 | window.addEventListener('scroll', listener); 631 | listener(); 632 | break; 633 | } 634 | }; 635 | 636 | /** 637 | * Controls 638 | ************************************** 639 | */ 640 | 641 | /** 642 | * Get the current status of the animation between 643 | * three different states: 'start', 'progress', 'end'. 644 | * @return {string} Instance status 645 | */ 646 | Vivus.prototype.getStatus = function() { 647 | return this.currentFrame === 0 648 | ? 'start' 649 | : this.currentFrame === this.frameLength 650 | ? 'end' 651 | : 'progress'; 652 | }; 653 | 654 | /** 655 | * Reset the instance to the initial state : undraw 656 | * Be careful, it just reset the animation, if you're 657 | * playing the animation, this won't stop it. But just 658 | * make it start from start. 659 | * 660 | */ 661 | Vivus.prototype.reset = function() { 662 | return this.setFrameProgress(0); 663 | }; 664 | 665 | /** 666 | * Set the instance to the final state : drawn 667 | * Be careful, it just set the animation, if you're 668 | * playing the animation on rewind, this won't stop it. 669 | * But just make it start from the end. 670 | * 671 | */ 672 | Vivus.prototype.finish = function() { 673 | return this.setFrameProgress(1); 674 | }; 675 | 676 | /** 677 | * Set the level of progress of the drawing. 678 | * 679 | * @param {number} progress Level of progress to set 680 | */ 681 | Vivus.prototype.setFrameProgress = function(progress) { 682 | progress = Math.min(1, Math.max(0, progress)); 683 | this.currentFrame = Math.round(this.frameLength * progress); 684 | this.trace(); 685 | return this; 686 | }; 687 | 688 | /** 689 | * Play the animation at the desired speed. 690 | * Speed must be a valid number (no zero). 691 | * By default, the speed value is 1. 692 | * But a negative value is accepted to go forward. 693 | * 694 | * And works with float too. 695 | * But don't forget we are in JavaScript, se be nice 696 | * with him and give him a 1/2^x value. 697 | * 698 | * @param {number} speed Animation speed [optional] 699 | */ 700 | Vivus.prototype.play = function(speed, callback) { 701 | this.instanceCallback = null; 702 | 703 | if (speed && typeof speed === 'function') { 704 | this.instanceCallback = speed; // first parameter is actually the callback function 705 | speed = null; 706 | } else if (speed && typeof speed !== 'number') { 707 | throw new Error('Vivus [play]: invalid speed'); 708 | } 709 | // if the first parameter wasn't the callback, check if the seconds was 710 | if (callback && typeof callback === 'function' && !this.instanceCallback) { 711 | this.instanceCallback = callback; 712 | } 713 | 714 | this.speed = speed || 1; 715 | if (!this.handle) { 716 | this.draw(); 717 | } 718 | return this; 719 | }; 720 | 721 | /** 722 | * Stop the current animation, if on progress. 723 | * Should not trigger any error. 724 | * 725 | */ 726 | Vivus.prototype.stop = function() { 727 | if (this.handle) { 728 | cancelAnimFrame(this.handle); 729 | this.handle = null; 730 | } 731 | return this; 732 | }; 733 | 734 | /** 735 | * Destroy the instance. 736 | * Remove all bad styling attributes on all 737 | * path tags 738 | * 739 | */ 740 | Vivus.prototype.destroy = function() { 741 | this.stop(); 742 | var i, path; 743 | for (i = 0; i < this.map.length; i++) { 744 | path = this.map[i]; 745 | path.el.style.strokeDashoffset = null; 746 | path.el.style.strokeDasharray = null; 747 | this.renderPath(i); 748 | } 749 | }; 750 | 751 | /** 752 | * Utils methods 753 | * include methods from Codrops 754 | ************************************** 755 | */ 756 | 757 | /** 758 | * Method to best guess if a path should added into 759 | * the animation or not. 760 | * 761 | * 1. Use the `data-vivus-ignore` attribute if set 762 | * 2. Check if the instance must ignore invisible paths 763 | * 3. Check if the path is visible 764 | * 765 | * For now the visibility checking is unstable. 766 | * It will be used for a beta phase. 767 | * 768 | * Other improvments are planned. Like detecting 769 | * is the path got a stroke or a valid opacity. 770 | */ 771 | Vivus.prototype.isInvisible = function(el) { 772 | var rect, 773 | ignoreAttr = el.getAttribute('data-ignore'); 774 | 775 | if (ignoreAttr !== null) { 776 | return ignoreAttr !== 'false'; 777 | } 778 | 779 | if (this.ignoreInvisible) { 780 | rect = el.getBoundingClientRect(); 781 | return !rect.width && !rect.height; 782 | } else { 783 | return false; 784 | } 785 | }; 786 | 787 | /** 788 | * Parse attributes of a DOM element to 789 | * get an object of {attributeName => attributeValue} 790 | * 791 | * @param {object} element DOM element to parse 792 | * @return {object} Object of attributes 793 | */ 794 | Vivus.prototype.parseAttr = function(element) { 795 | var attr, 796 | output = {}; 797 | if (element && element.attributes) { 798 | for (var i = 0; i < element.attributes.length; i++) { 799 | attr = element.attributes[i]; 800 | output[attr.name] = attr.value; 801 | } 802 | } 803 | return output; 804 | }; 805 | 806 | /** 807 | * Reply if an element is in the page viewport 808 | * 809 | * @param {object} el Element to observe 810 | * @param {number} h Percentage of height 811 | * @return {boolean} 812 | */ 813 | Vivus.prototype.isInViewport = function(el, h) { 814 | var scrolled = this.scrollY(), 815 | viewed = scrolled + this.getViewportH(), 816 | elBCR = el.getBoundingClientRect(), 817 | elHeight = elBCR.height, 818 | elTop = scrolled + elBCR.top, 819 | elBottom = elTop + elHeight; 820 | 821 | // if 0, the element is considered in the viewport as soon as it enters. 822 | // if 1, the element is considered in the viewport only when it's fully inside 823 | // value in percentage (1 >= h >= 0) 824 | h = h || 0; 825 | 826 | return elTop + elHeight * h <= viewed && elBottom >= scrolled; 827 | }; 828 | 829 | /** 830 | * Get the viewport height in pixels 831 | * 832 | * @return {integer} Viewport height 833 | */ 834 | Vivus.prototype.getViewportH = function() { 835 | var client = this.docElem.clientHeight, 836 | inner = window.innerHeight; 837 | 838 | if (client < inner) { 839 | return inner; 840 | } else { 841 | return client; 842 | } 843 | }; 844 | 845 | /** 846 | * Get the page Y offset 847 | * 848 | * @return {integer} Page Y offset 849 | */ 850 | Vivus.prototype.scrollY = function() { 851 | return window.pageYOffset || this.docElem.scrollTop; 852 | }; 853 | 854 | setupEnv = function() { 855 | if (Vivus.prototype.docElem) { 856 | return; 857 | } 858 | 859 | /** 860 | * Alias for document element 861 | * 862 | * @type {DOMelement} 863 | */ 864 | Vivus.prototype.docElem = window.document.documentElement; 865 | 866 | /** 867 | * Alias for `requestAnimationFrame` or 868 | * `setTimeout` function for deprecated browsers. 869 | * 870 | */ 871 | requestAnimFrame = (function() { 872 | return ( 873 | window.requestAnimationFrame || 874 | window.webkitRequestAnimationFrame || 875 | window.mozRequestAnimationFrame || 876 | window.oRequestAnimationFrame || 877 | window.msRequestAnimationFrame || 878 | function(/* function */ callback) { 879 | return window.setTimeout(callback, 1000 / 60); 880 | } 881 | ); 882 | })(); 883 | 884 | /** 885 | * Alias for `cancelAnimationFrame` or 886 | * `cancelTimeout` function for deprecated browsers. 887 | * 888 | */ 889 | cancelAnimFrame = (function() { 890 | return ( 891 | window.cancelAnimationFrame || 892 | window.webkitCancelAnimationFrame || 893 | window.mozCancelAnimationFrame || 894 | window.oCancelAnimationFrame || 895 | window.msCancelAnimationFrame || 896 | function(id) { 897 | return window.clearTimeout(id); 898 | } 899 | ); 900 | })(); 901 | }; 902 | 903 | /** 904 | * Parse string to integer. 905 | * If the number is not positive or null 906 | * the method will return the default value 907 | * or 0 if undefined 908 | * 909 | * @param {string} value String to parse 910 | * @param {*} defaultValue Value to return if the result parsed is invalid 911 | * @return {number} 912 | * 913 | */ 914 | parsePositiveInt = function(value, defaultValue) { 915 | var output = parseInt(value, 10); 916 | return output >= 0 ? output : defaultValue; 917 | }; 918 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Jul 18 2014 10:58:08 GMT+0100 (BST) 3 | 4 | module.exports = function(config) { 5 | var options = { 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '..', 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'test/unit.setup.js', 16 | 'src/pathformer.js', 17 | 'src/vivus.js', 18 | 'test/unit/**.js' 19 | ], 20 | 21 | // list of files to exclude 22 | exclude: [], 23 | 24 | // preprocess matching files before serving them to the browser 25 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors: { 27 | '../src/pathformer.js': ['coverage'], 28 | '../src/vivus.js': ['coverage'] 29 | }, 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress' 33 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 34 | reporters: ['progress', 'coverage'], 35 | 36 | // optionally, configure the reporter 37 | coverageReporter: { 38 | type: 'html', 39 | dir: '../coverage/' 40 | }, 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: true, 54 | 55 | // start these browsers 56 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 57 | browsers: ['Chrome'], 58 | 59 | // Continuous Integration mode 60 | // if true, Karma captures browsers, runs the tests and exits 61 | singleRun: true 62 | }; 63 | 64 | if (process.env.TRAVIS) { 65 | options.customLaunchers = { 66 | Chrome_travis_ci: { 67 | base: 'Chrome', 68 | flags: ['--no-sandbox'] 69 | } 70 | }; 71 | options.browsers = ['Chrome_travis_ci']; 72 | } 73 | 74 | config.set(options); 75 | }; 76 | -------------------------------------------------------------------------------- /test/manual/hi-there.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/manual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vivus.js - manual tests page 8 | 9 | 10 | 151 | 152 | 153 |
154 |

Vivus manual (cheap) tests.

155 |

156 | Just scroll along the page and if a glitch appear or the visual 157 | appearance is not like the description, it's not good. 158 |

159 | 164 |
165 | 166 |
167 |
168 |
169 |

170 | This should display the obturateur SVG like on the demo page. The 171 | strokes must be orange. The element must remain an 172 | object tag. 173 |

174 | 179 |
180 |
181 | 187 |
188 |
189 |
190 | 191 |
192 |
193 |
194 |
195 |
196 |
197 |

198 | This should display the polaroid SVG like on the demo page. The 199 | strokes must have the same color as this text. 200 |

201 |
202 |
203 |
204 | 205 |
206 |
207 |
208 |

209 | This should display the 'Hi there' SVG like ready to start. Be sure 210 | no glitch appear (no small path or dots). Click on the following 211 | button to start. 212 |

213 | 214 |
215 |
216 |
217 |
218 |
219 |
220 | 221 |
222 |
223 |
224 |
225 |
226 |
227 |

228 | This should display a synth ready to start. Be sure no glitch appear 229 | (no small path or dots). The animation should use a custom path 230 | timing function (ease_in: slow at start then finish fast.). Click on 231 | the following button to start. 232 |

233 | 234 |
235 |
236 |
237 | 238 |
239 |
240 | 241 | 242 | 254 | 266 | 278 | 279 | 280 | 292 | 304 | 316 | 317 | 318 | 330 | 342 | 354 | 355 |
243 | 244 | 252 | 253 | 255 | 256 | 264 | 265 | 267 | 268 | 276 | 277 |
281 | 282 | 290 | 291 | 293 | 294 | 302 | 303 | 305 | 306 | 314 | 315 |
319 | 320 | 328 | 329 | 331 | 332 | 340 | 341 | 343 | 344 | 352 | 353 |
356 |

357 | Non scaling path
In any case of a resize, the animation of each 358 | line must be complete. 359 |

360 | 361 | 362 |
363 |
364 | 365 | 366 | 367 | 424 | 425 | 426 | -------------------------------------------------------------------------------- /test/manual/obturateur.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/manual/polaroid.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 14 | 15 | 16 | 19 | 22 | 24 | 25 | 26 | 27 | 30 | 33 | 36 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /test/manual/synth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 102 | 103 | 104 | 105 | 106 | 107 | 109 | 110 | 112 | 113 | 114 | 115 | 116 | 117 | 119 | 120 | 122 | 123 | 124 | 125 | 126 | 127 | 129 | 130 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 142 | 143 | 144 | 145 | 146 | 147 | 149 | 150 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /test/unit.setup.js: -------------------------------------------------------------------------------- 1 | /* Here is a cheap and bad implementation 2 | * of requestAnimationFrame and 3 | * cancelAnimationFrame mock. 4 | * But it's more than enough 5 | * for our tests. 6 | */ 7 | window.requestAnimFrameStack = []; 8 | window.requestAnimationFrame = function (callback) { 9 | window.requestAnimFrameStack.push(callback); 10 | return true; 11 | }; 12 | window.cancelAnimationFrame = function () { 13 | window.requestAnimFrameStack = []; 14 | }; 15 | -------------------------------------------------------------------------------- /test/unit/pathformer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Unit tests for Pathformer 5 | * 6 | */ 7 | describe('Pathformer', function () { 8 | 9 | var svgTag, 10 | svgTagId = 'my-svg', 11 | svgGroupTag, 12 | svgGroupTagId = 'my-svg-group'; 13 | 14 | beforeEach(function () { 15 | // Remove tag if existing 16 | svgTag = document.getElementById(svgTagId); 17 | if (svgTag) { 18 | svgTag.remove(); 19 | } 20 | 21 | // Create the SVG 22 | svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg'); 23 | svgTag.id = svgTagId; 24 | svgTag.innerHTML = '' + 25 | '' + 26 | '' + 27 | '' + 28 | '' + 29 | '' + 30 | '' + 31 | ''; 32 | 33 | svgGroupTag = svgTag.querySelector('#'+svgGroupTagId); 34 | 35 | // Insert it to the body 36 | document.body.appendChild(svgTag); 37 | }); 38 | 39 | describe('[param tests]', function () { 40 | 41 | // Tests about the SVG element 42 | it('should throw an error if the SVG is given in parameter', function () { 43 | expect(function () { 44 | new Pathformer(); 45 | }).toThrow(new Error('Pathformer [constructor]: "element" parameter is required')); 46 | }); 47 | 48 | it('should work with only the SVG id', function () { 49 | expect(function () { 50 | new Pathformer(svgTagId); 51 | }).not.toThrow(); 52 | }); 53 | 54 | it('should work with only the SVG object', function () { 55 | expect(function () { 56 | new Pathformer(svgTag); 57 | }).not.toThrow(); 58 | }); 59 | 60 | it('should work with only the SVG group object', function () { 61 | expect(function () { 62 | new Pathformer(svgGroupTag); 63 | }).not.toThrow(); 64 | }); 65 | 66 | it('should throw an error if the SVG ID given is invalid', function () { 67 | expect(function () { 68 | new Pathformer('my-unexisting-svg'); 69 | }).toThrow(new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID')); 70 | }); 71 | 72 | it('should throw an error if the ID given is not related to a SVG element', function () { 73 | var divTag = document.createElement('div'); 74 | divTag.id = 'my-div'; 75 | document.body.appendChild(divTag); 76 | expect(function () { 77 | new Pathformer('my-div'); 78 | }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 79 | }); 80 | 81 | it('should throw an error if the element is not a correct type (DOM object or string)', function () { 82 | expect(function () { new Pathformer({}); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 83 | expect(function () { new Pathformer(42); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 84 | expect(function () { new Pathformer(false); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 85 | expect(function () { new Pathformer(new Date()); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 86 | expect(function () { new Pathformer(function () {}); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 87 | expect(function () { new Pathformer(document.createElement('div')); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement')); 88 | }); 89 | }); 90 | 91 | describe('[translation]', function () { 92 | 93 | // Line object 94 | describe('line', function () { 95 | it('should return an object with a `d` attribute', function () { 96 | var output = Pathformer.prototype.lineToPath({}); 97 | expect(output.d).toBeDefined(); 98 | }); 99 | 100 | it('should return an object with an unclosed shape', function () { 101 | var output = Pathformer.prototype.lineToPath({}); 102 | expect(output.d.substr(-1)).not.toEqual('Z'); 103 | }); 104 | 105 | it('should set default positino attributes to zero', function () { 106 | var output = Pathformer.prototype.lineToPath({ 107 | x1: '21', x2: '32', y1: '11' 108 | }); 109 | expect(output.d.indexOf('0')).not.toEqual(-1); 110 | expect(output.d.indexOf('undefined')).toEqual(-1); 111 | }); 112 | }); 113 | 114 | // Rect object 115 | describe('rect', function () { 116 | it('should return an object with a `d` attribute', function () { 117 | var output = Pathformer.prototype.rectToPath({}); 118 | expect(output.d).toBeDefined(); 119 | }); 120 | 121 | it('should return an object with a closed shape', function () { 122 | var output = Pathformer.prototype.rectToPath({}); 123 | expect(output.d.substr(-1)).toEqual('Z'); 124 | }); 125 | 126 | it('should set default positino attributes to zero', function () { 127 | var output = Pathformer.prototype.rectToPath({ 128 | x: '21', height: '32', width: '11' 129 | }); 130 | expect(output.d.indexOf('0')).not.toEqual(-1); 131 | expect(output.d.indexOf('undefined')).toEqual(-1); 132 | }); 133 | 134 | it('should apply rounded corners', function () { 135 | var result = 'M 50,10 ' + 136 | 'L 50,10 A 40,20,0,0,1,90,30 ' + 137 | 'L 90,50 A 40,20,0,0,1,50,70 ' + 138 | 'L 50,70 A 40,20,0,0,1,10,50 ' + 139 | 'L 10,30 A 40,20,0,0,1,50,10'; 140 | 141 | var output = Pathformer.prototype.rectToPath({ 142 | x:10, y:10, width:80, height:60, rx:100, ry:20 143 | }); 144 | 145 | expect(output.d).toEqual(result); 146 | }); 147 | 148 | it('should apply rounded corners even when a value is missing', function () { 149 | var result = 'M 30,10 ' + 150 | 'L 70,10 A 20,20,0,0,1,90,30 ' + 151 | 'L 90,50 A 20,20,0,0,1,70,70 ' + 152 | 'L 30,70 A 20,20,0,0,1,10,50 ' + 153 | 'L 10,30 A 20,20,0,0,1,30,10'; 154 | 155 | var output = Pathformer.prototype.rectToPath({ 156 | x:10, y:10, width:80, height:60, ry:20 157 | }); 158 | 159 | expect(output.d).toEqual(result); 160 | }); 161 | }); 162 | 163 | // Polyline object 164 | describe('polyline', function () { 165 | var polyline; 166 | beforeEach(function () { 167 | polyline = { 168 | points: '2,3 4,5 6,7' 169 | }; 170 | }); 171 | 172 | it('should return an object with a `d` attribute', function () { 173 | var output = Pathformer.prototype.polylineToPath(polyline); 174 | expect(output.d).toBeDefined(); 175 | }); 176 | 177 | it('should return an object with an unclosed shape', function () { 178 | var output = Pathformer.prototype.polylineToPath(polyline); 179 | expect(output.d.substr(-1)).not.toEqual('Z'); 180 | }); 181 | 182 | it('should ignore incorrect points', function () { 183 | var output; 184 | polyline.points += ' 43'; 185 | output = Pathformer.prototype.polylineToPath(polyline); 186 | expect(output.d.indexOf('43')).toEqual(-1); 187 | }); 188 | 189 | it('should accept points defined with and without commas', function () { 190 | var outputWithPoint = Pathformer.prototype.polylineToPath(polyline); 191 | var outputWithoutPoint = Pathformer.prototype.polylineToPath({points: '2 3 4 5 6 7'}); 192 | expect(outputWithPoint).toEqual(outputWithoutPoint); 193 | }); 194 | }); 195 | 196 | // Polygon object 197 | describe('polygon', function () { 198 | var polygon; 199 | beforeEach(function () { 200 | polygon = { 201 | points: '2,3 4,5 6,7' 202 | }; 203 | }); 204 | 205 | it('should return an object with a `d` attribute', function () { 206 | var output = Pathformer.prototype.polygonToPath(polygon); 207 | expect(output.d).toBeDefined(); 208 | }); 209 | 210 | it('should return an object with a closed shape', function () { 211 | var output = Pathformer.prototype.polygonToPath(polygon); 212 | expect(output.d.substr(-1)).toEqual('Z'); 213 | }); 214 | }); 215 | 216 | // Ellipse object 217 | describe('ellipse', function () { 218 | 219 | var ellipse; 220 | beforeEach(function () { 221 | ellipse = { 222 | cx: 2, 223 | cy: 3, 224 | rx: 3 225 | }; 226 | }); 227 | 228 | it('should return an object with a `d` attribute', function () { 229 | var output = Pathformer.prototype.ellipseToPath(ellipse); 230 | expect(output.d).toBeDefined(); 231 | }); 232 | 233 | it('should return an object with an unclosed shape', function () { 234 | var output = Pathformer.prototype.ellipseToPath(ellipse); 235 | expect(output.d.substr(-1)).not.toEqual('Z'); 236 | }); 237 | 238 | it('should set default positino attributes to zero', function () { 239 | delete ellipse.cy; 240 | var output = Pathformer.prototype.ellipseToPath(ellipse); 241 | expect(output.d.indexOf('0')).not.toEqual(-1); 242 | expect(output.d.indexOf('undefined')).toEqual(-1); 243 | }); 244 | }); 245 | 246 | // Circle object 247 | describe('circle', function () { 248 | 249 | var circle; 250 | beforeEach(function () { 251 | circle = { 252 | cx: 2, 253 | cy: 3, 254 | rx: 3, 255 | r: 1 256 | }; 257 | }); 258 | 259 | it('should return an object with a `d` attribute', function () { 260 | var output = Pathformer.prototype.circleToPath(circle); 261 | expect(output.d).toBeDefined(); 262 | }); 263 | 264 | it('should return an object with an unclosed shape', function () { 265 | var output = Pathformer.prototype.circleToPath(circle); 266 | expect(output.d.substr(-1)).not.toEqual('Z'); 267 | }); 268 | 269 | it('should set default positino attributes to zero', function () { 270 | delete circle.cy; 271 | var output = Pathformer.prototype.circleToPath(circle); 272 | expect(output.d.indexOf('0')).not.toEqual(-1); 273 | expect(output.d.indexOf('undefined')).toEqual(-1); 274 | }); 275 | }); 276 | }); 277 | 278 | describe('[utils]', function () { 279 | 280 | describe('attribute parser', function () { 281 | it('should return an empty object if attributes length are undefined', function () { 282 | var output = Pathformer.prototype.parseAttr({}); 283 | expect(output).toEqual({}); 284 | }); 285 | }); 286 | 287 | describe('engine', function () { 288 | it('shouldn\'t throw an error if the SVG got a tag not taken in charge', function () { 289 | svgTag.innerHTML = ''; 290 | 291 | expect(function () { 292 | new Pathformer(svgTagId); 293 | }).not.toThrow(); 294 | }); 295 | 296 | it('should remove useless attributes during transformation', function () { 297 | new Pathformer(svgTagId); 298 | expect(svgTag.childNodes[0].getAttribute('cx')).toBe(null); 299 | }); 300 | }); 301 | 302 | describe('validity', function () { 303 | it('should throw error if the SVG contain shape with percentage value', function () { 304 | // Create the SVG 305 | var svgTagPrc = document.createElementNS('http://www.w3.org/2000/svg','svg'); 306 | svgTagPrc.innerHTML = ''; 307 | expect(function () { 308 | new Pathformer(svgTagPrc); 309 | }).toThrow(new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.')); 310 | }); 311 | 312 | it('shouldn\'t throw error if the SVG contain shape with percentage value on a non-data attribute', function () { 313 | // Create the SVG 314 | var svgTagPrc = document.createElementNS('http://www.w3.org/2000/svg','svg'); 315 | svgTagPrc.innerHTML = ''; 316 | expect(function () { 317 | new Pathformer(svgTagPrc); 318 | }).not.toThrow(); 319 | }); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /test/unit/vivus.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Unit tests for Vivus 5 | * 6 | */ 7 | describe('Vivus', function () { 8 | 9 | var ObjectElementMock, 10 | triggerFrames, 11 | myVivus, 12 | objTag, 13 | wrapTag, 14 | svgTag, 15 | svgTagId = 'my-svg', 16 | svgGroupTagId = 'my-svg-group'; 17 | 18 | // Mock ObjectElement and it's constructor via createElement 19 | ObjectElementMock = function () { 20 | this.loadCb = []; 21 | this.attr = {}; 22 | this.addEventListener = function (evtName, cb) { 23 | if (evtName === 'load') { 24 | this.loadCb.push(cb); 25 | } 26 | }; 27 | this.loaded = function () { 28 | for (var i = 0; i < this.loadCb.length; i++) { 29 | this.loadCb[i]({target: this}); 30 | } 31 | }; 32 | this.getBoundingClientRect = function () { 33 | return { 34 | height: 11, 35 | top: 364 36 | }; 37 | }; 38 | this.insertBefore = function () {}; 39 | this.removeChild = function () {}; 40 | this.setAttribute = function (key, val) { 41 | this.attr[key] = val; 42 | }; 43 | this.getAttribute = function (key) { 44 | return this.attr[key]; 45 | }; 46 | }; 47 | window.HTMLObjectElement = ObjectElementMock; 48 | 49 | triggerFrames = function (counter) { 50 | counter = counter || -1; 51 | while (window.requestAnimFrameStack.length && counter !== 0) { 52 | window.requestAnimFrameStack.shift()(); 53 | counter--; 54 | } 55 | }; 56 | 57 | 58 | beforeEach(function () { 59 | // Create the SVG 60 | svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg'); 61 | svgTag.id = svgTagId; 62 | svgTag.innerHTML = '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '' + 68 | '' + 69 | ''; 70 | 71 | wrapTag = document.createElement('div'); 72 | wrapTag.appendChild(svgTag); 73 | 74 | document.body.appendChild(wrapTag); 75 | 76 | // Reset the request anim frame stack 77 | window.requestAnimFrameStack = []; 78 | }); 79 | 80 | afterEach(function () { 81 | // Remove tag 82 | svgTag.remove(); 83 | wrapTag.remove(); 84 | }); 85 | 86 | describe('[basic tests]', function () { 87 | 88 | it('should the class be defined under Vivus name', function () { 89 | expect(Vivus).toBeDefined(); 90 | }); 91 | 92 | it('should have timing functions set', function () { 93 | expect(Vivus.LINEAR).toBeDefined(); 94 | expect(Vivus.EASE).toBeDefined(); 95 | expect(Vivus.EASE_IN).toBeDefined(); 96 | expect(Vivus.EASE_OUT).toBeDefined(); 97 | expect(Vivus.EASE_OUT_BOUNCE).toBeDefined(); 98 | }); 99 | 100 | it('should have timing functions returning correct value on limits', function () { 101 | expect(Vivus.LINEAR(0)).toEqual(0); 102 | expect(Vivus.LINEAR(1)).toEqual(1); 103 | expect(Vivus.EASE(0)).toEqual(0); 104 | expect(Vivus.EASE(1)).toEqual(1); 105 | expect(Vivus.EASE_IN(0)).toEqual(0); 106 | expect(Vivus.EASE_IN(1)).toEqual(1); 107 | expect(Vivus.EASE_OUT(0)).toEqual(0); 108 | expect(Vivus.EASE_OUT(1)).toEqual(1); 109 | expect(Vivus.EASE_OUT_BOUNCE(0)).toEqual(0); 110 | expect(Vivus.EASE_OUT_BOUNCE(1)).toEqual(1); 111 | }); 112 | }); 113 | 114 | describe('[param tests]', function () { 115 | 116 | // Tests about the SVG element 117 | it('should throw an error if the SVG is given in parameter', function () { 118 | expect(function () { 119 | new Vivus(); 120 | }).toThrow(new Error('Vivus [constructor]: "element" parameter is required')); 121 | }); 122 | 123 | it('should work with only the SVG id', function () { 124 | expect(function () { 125 | new Vivus(svgTagId); 126 | }).not.toThrow(); 127 | }); 128 | 129 | it('should work with only the SVG object', function () { 130 | expect(function () { 131 | new Vivus(svgTag); 132 | }).not.toThrow(); 133 | }); 134 | 135 | it('should work with the SVG group object', function () { 136 | expect(function () { 137 | new Vivus(svgGroupTagId); 138 | }).not.toThrow(); 139 | }); 140 | 141 | it('should throw an error if the SVG ID given is invalid', function () { 142 | expect(function () { 143 | new Vivus('my-unexisting-svg'); 144 | }).toThrow(new Error('Vivus [constructor]: "element" parameter is not related to an existing ID')); 145 | }); 146 | 147 | it('should throw an error if the ID given is not related to a SVG element', function () { 148 | var divTag = document.createElement('div'); 149 | divTag.id = 'my-div'; 150 | document.body.appendChild(divTag); 151 | expect(function () { 152 | new Vivus('my-div'); 153 | }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 154 | }); 155 | 156 | it('should accept any DOM element if `file` option is set', function () { 157 | var divTag = document.createElement('div'); 158 | spyOn(window, 'XMLHttpRequest'); 159 | try { 160 | new Vivus(divTag, {file: 'opensource.svg'}); 161 | } 162 | catch(err) {} 163 | 164 | expect(window.XMLHttpRequest).toHaveBeenCalled(); 165 | }); 166 | 167 | it('should throw an error if the element is not a correct type (DOM object or string)', function () { 168 | expect(function () { new Vivus({}); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 169 | expect(function () { new Vivus(42); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 170 | expect(function () { new Vivus(false); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 171 | expect(function () { new Vivus(new Date()); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 172 | expect(function () { new Vivus(function () {}); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 173 | expect(function () { new Vivus(document.createElement('div')); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)')); 174 | }); 175 | 176 | it('should accept object element', function () { 177 | // Create a mock Object getElementById 178 | objTag = new ObjectElementMock(); 179 | objTag.contentDocument = wrapTag; 180 | 181 | expect(function () { 182 | new Vivus(objTag); 183 | }).not.toThrow(); 184 | }); 185 | 186 | it('the vivus state should be ready if the SVG is already loaded', function () { 187 | objTag = new ObjectElementMock(); 188 | objTag.contentDocument = wrapTag; 189 | objTag.loaded(); 190 | var myVivus = new Vivus(objTag); 191 | expect(myVivus.isReady).toEqual(true); 192 | }); 193 | 194 | it('the vivus instance should have `el` and `parentEl` different if the element is an object', function () { 195 | objTag = new ObjectElementMock(); 196 | objTag.contentDocument = wrapTag; 197 | objTag.loaded(); 198 | var myVivus = new Vivus(objTag); 199 | expect(myVivus.parentEl).not.toEqual(myVivus.el); 200 | }); 201 | 202 | it('should call `onReady` callback once the SVG is loaded', function () { 203 | objTag = new ObjectElementMock(); 204 | objTag.contentDocument = document.createElement('div'); 205 | var myVivus = new Vivus(objTag); 206 | objTag.contentDocument = wrapTag; 207 | objTag.loaded(); 208 | expect(myVivus.isReady).toEqual(true); 209 | }); 210 | 211 | it('should throw an error if the SVG file does not exists', function () { 212 | objTag = new ObjectElementMock(); 213 | objTag.contentDocument = document.createElement('div'); 214 | new Vivus(objTag); 215 | expect(function () { 216 | objTag.loaded(); 217 | }).toThrow(); 218 | }); 219 | 220 | // Options 221 | it('should work without options', function () { 222 | expect(function () { 223 | new Vivus(svgTag); 224 | }).not.toThrow(); 225 | }); 226 | 227 | it('should throw an error if options is not an object', function () { 228 | expect(function () { new Vivus(svgTag, []); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 229 | expect(function () { new Vivus(svgTag, 42); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 230 | expect(function () { new Vivus(svgTag, false); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 231 | expect(function () { new Vivus(svgTag, new Date()); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 232 | expect(function () { new Vivus(svgTag, 'manual'); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 233 | expect(function () { new Vivus(svgTag, function () {}); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object')); 234 | }); 235 | 236 | // Options 237 | it('should work with empty option object', function () { 238 | expect(function () { 239 | new Vivus(svgTag, {}); 240 | }).not.toThrow(); 241 | }); 242 | 243 | it('should throw an error if the `type` value given in options does not exists', function () { 244 | expect(function () { 245 | new Vivus(svgTag, {type: 'by-unicorn'}); 246 | }).toThrow(new Error('Vivus [constructor]: by-unicorn is not an existing animation `type`')); 247 | }); 248 | 249 | it('should throw an error if the `start` value given in options is not a string', function () { 250 | expect(function () { 251 | new Vivus(svgTag, {start: 'when-unicorn-ready'}); 252 | }).toThrow(new Error('Vivus [constructor]: when-unicorn-ready is not an existing `start` option')); 253 | }); 254 | 255 | it('should throw an error if the `delay` value is bigger (or equal) than `duration`', function () { 256 | expect(function () { 257 | new Vivus(svgTag, {duration: 200, delay: 199}); 258 | }).not.toThrow(); 259 | expect(function () { 260 | new Vivus(svgTag, {duration: 200, delay: 200}); 261 | }).toThrow(new Error('Vivus [constructor]: delay must be shorter than duration')); 262 | expect(function () { 263 | new Vivus(svgTag, {duration: 200, delay: 201}); 264 | }).toThrow(new Error('Vivus [constructor]: delay must be shorter than duration')); 265 | }); 266 | 267 | it('should override `duration` if invalid', function () { 268 | myVivus = new Vivus(svgTag, {duration: -12}); 269 | expect(myVivus.duration > 0).toBe(true); 270 | }); 271 | 272 | it('should override `delay` if invalid, with a null value', function () { 273 | myVivus = new Vivus(svgTag, {delay: -12}); 274 | expect(!myVivus.delay).toBe(false); 275 | }); 276 | 277 | it('should set up default values', function () { 278 | myVivus = new Vivus(svgTag, {}); 279 | expect(myVivus.type).toBeDefined(); 280 | expect(myVivus.start).toBeDefined(); 281 | expect(myVivus.duration).toBeDefined(); 282 | }); 283 | 284 | it('the vivus instance should have `el` and `parentEl` equal if the element is a SVG object', function () { 285 | myVivus = new Vivus(svgTag, {}); 286 | expect(myVivus.el).toEqual(myVivus.parentEl); 287 | }); 288 | 289 | // Callback 290 | it('should throw an error if callback is non a function', function () { 291 | expect(function () { 292 | new Vivus(svgTag, {}, 42); 293 | }).toThrow(new Error('Vivus [constructor]: "callback" parameter must be a function')); 294 | }); 295 | 296 | it('should use scale to determine path length when vector effect is non-scaling-stroke', function () { 297 | var scalingSvgTag = document.createElementNS('http://www.w3.org/2000/svg','svg'); 298 | var scalingWrapTag = document.createElement('div'); 299 | 300 | scalingSvgTag.setAttribute('viewBox', '0 0 500 200'); 301 | scalingWrapTag.style.width = '1000px'; 302 | 303 | scalingSvgTag.id = 'scaling-stroke-test'; 304 | 305 | scalingSvgTag.innerHTML = '' + 306 | ''; 307 | 308 | scalingWrapTag.appendChild(scalingSvgTag); 309 | 310 | document.body.appendChild(scalingWrapTag); 311 | 312 | myVivus = new Vivus(scalingSvgTag); 313 | 314 | expect(myVivus.map.length).toEqual(2); 315 | expect(myVivus.map[0].length).toEqual(280); 316 | expect(myVivus.map[1].length).toEqual(141); 317 | }); 318 | }); 319 | 320 | describe('[engine]', function () { 321 | 322 | // Mapping 323 | describe('Mapping:', function () { 324 | 325 | it('should not trigger any error if the SVG is empty', function () { 326 | expect(function () { 327 | var svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg'); 328 | myVivus = new Vivus(svgTag, {}); 329 | }).not.toThrow(); 330 | }); 331 | 332 | it('should create a mapping of the SVG', function () { 333 | myVivus = new Vivus(svgTag, {}); 334 | expect(myVivus.map && myVivus.map.length).toEqual(6); 335 | }); 336 | 337 | it('should map with correct values for start and duration', function () { 338 | var i, typeIndex, types = ['delayed', 'sync', 'oneByOne', 'scenario', 'scenario-sync']; 339 | for (typeIndex in types) { 340 | myVivus = new Vivus(svgTag, {type: types[typeIndex], duration: 200}); 341 | for (i in myVivus.map) { 342 | expect(myVivus.map[i].startAt >= 0).toBe(true); 343 | expect(myVivus.map[i].duration >= 0).toBe(true); 344 | } 345 | } 346 | }); 347 | 348 | // Tests for 'getTotalLength' method in case of awkward results 349 | describe('SVG parsing issue', function () { 350 | 351 | var getTotalLengthBkp = SVGPathElement.prototype.getTotalLength, 352 | warnBkp = console.warn; 353 | 354 | beforeEach(function () { 355 | SVGPathElement.prototype.getTotalLength = function () { 356 | return NaN; 357 | }; 358 | }); 359 | 360 | afterEach(function () { 361 | SVGPathElement.prototype.getTotalLength = getTotalLengthBkp; 362 | console.warn = warnBkp; 363 | }); 364 | 365 | it('should call console.warn if a path length is NaN', function () { 366 | var warnSpy = jasmine.createSpy('spy'); 367 | console.warn = warnSpy; 368 | myVivus = new Vivus(svgTag); 369 | expect(warnSpy.calls.count()).toEqual(6); 370 | expect(myVivus.map.length).toEqual(0); 371 | }); 372 | 373 | it('shouldn\'t call console.warn if not defined a path length is NaN', function () { 374 | console.warn = null; 375 | myVivus = new Vivus(svgTag); 376 | expect(myVivus.map.length).toEqual(0); 377 | }); 378 | }); 379 | }); 380 | 381 | describe('Visibility checking:', function () { 382 | 383 | it('should not accept a path which is not displayed', function () { 384 | // Hide a path 385 | svgTag.childNodes[1].style.display = 'none'; 386 | myVivus = new Vivus(svgTag, {ignoreInvisible: true}); 387 | expect(myVivus.map.length).toEqual(5); 388 | }); 389 | 390 | it('should not accept a path which with an ignore tag', function () { 391 | svgTag.childNodes[1].setAttribute('data-ignore', 'true'); 392 | myVivus = new Vivus(svgTag); 393 | expect(myVivus.map.length).toEqual(5); 394 | }); 395 | 396 | it('should not accept a path which is not displayed', function () { 397 | svgTag.childNodes[1].setAttribute('data-ignore', 'false'); 398 | myVivus = new Vivus(svgTag); 399 | expect(myVivus.map.length).toEqual(6); 400 | }); 401 | }); 402 | 403 | // Drawing 404 | describe('Drawing:', function () { 405 | 406 | it('should call the callback once the animation is finished', function () { 407 | var done = false; 408 | myVivus = new Vivus(svgTag, { 409 | duration: 6, 410 | start: 'autostart' 411 | }, function () { 412 | done = true; 413 | }); 414 | 415 | triggerFrames(); 416 | expect(done).toBe(true); 417 | }); 418 | 419 | it('should call the callback once the reverse animation is finished', function () { 420 | var done = false; 421 | myVivus = new Vivus(svgTag, { 422 | type: 'oneByOne', 423 | duration: 6 424 | }, function () { 425 | done = true; 426 | }); 427 | 428 | myVivus.finish().play(-1); 429 | triggerFrames(); 430 | expect(done).toBe(true); 431 | }); 432 | 433 | it('should call the method callback as the second param once the animation is finished', function () { 434 | var done = false; 435 | myVivus = new Vivus(svgTag, { 436 | duration: 6, 437 | start: 'manual', 438 | }); 439 | 440 | myVivus.play(1, function() { 441 | done = true; 442 | }); 443 | triggerFrames(); 444 | expect(done).toBe(true); 445 | }); 446 | 447 | it('should call the method callback as the first param once the animation is finished', function () { 448 | var done = false; 449 | myVivus = new Vivus(svgTag, { 450 | duration: 6, 451 | start: 'manual', 452 | }); 453 | 454 | myVivus.play(function() { 455 | done = true; 456 | }); 457 | triggerFrames(); 458 | expect(done).toBe(true); 459 | }); 460 | 461 | it('should call the method callback once the reverse animation is finished', function () { 462 | var done = false; 463 | myVivus = new Vivus(svgTag, { 464 | duration: 6, 465 | start: 'manual', 466 | }); 467 | 468 | myVivus.finish().play(-1, function() { 469 | done = true; 470 | }); 471 | triggerFrames(); 472 | expect(done).toBe(true); 473 | }); 474 | 475 | it('should call the method callback provided in the last play call', function () { 476 | var done = false; 477 | myVivus = new Vivus(svgTag, { 478 | duration: 6, 479 | start: 'manual', 480 | }); 481 | 482 | myVivus.finish().play(-1, function () {}); 483 | myVivus.play(function() { 484 | done = true; 485 | }); 486 | triggerFrames(); 487 | expect(done).toBe(true); 488 | }); 489 | 490 | it('should call destroy method once the animation is finished', function () { 491 | myVivus = new Vivus(svgTag, { 492 | duration: 6, 493 | start: 'manual', 494 | selfDestroy: true 495 | }); 496 | myVivus.destroy = jasmine.createSpy('spy'); 497 | myVivus.play(); 498 | triggerFrames(); 499 | expect(myVivus.destroy.calls.count()).toEqual(1); 500 | }); 501 | 502 | it('should\' call destroy method if selfDestroy option is not present', function () { 503 | myVivus = new Vivus(svgTag, { 504 | duration: 6, 505 | start: 'manual' 506 | }); 507 | myVivus.destroy = jasmine.createSpy('spy'); 508 | myVivus.play(); 509 | triggerFrames(); 510 | expect(myVivus.destroy.calls.count()).toEqual(0); 511 | }); 512 | 513 | it('should stop animation if destroy has been called', function () { 514 | var callbackSpy = jasmine.createSpy('spy'); 515 | myVivus = new Vivus(svgTag, { 516 | duration: 6, 517 | start: 'autostart' 518 | }, callbackSpy); 519 | 520 | triggerFrames(1); 521 | myVivus.destroy(); 522 | 523 | triggerFrames(); 524 | expect(callbackSpy.calls.count()).toEqual(0); 525 | }); 526 | 527 | it('should stop the animation once it reaches currentFrame == 0', function () { 528 | myVivus = new Vivus(svgTag, { 529 | duration: 6, 530 | start: 'manual' 531 | }); 532 | myVivus.stop = jasmine.createSpy('spy'); 533 | myVivus.play(-1); 534 | triggerFrames(); 535 | expect(myVivus.stop.calls.count()).toEqual(1); 536 | }); 537 | 538 | it('should trace reasonably', function () { 539 | myVivus = new Vivus(svgTag, { 540 | duration: 6, 541 | start: 'manual' 542 | }); 543 | spyOn(myVivus, 'trace').and.callThrough(); 544 | myVivus.play(0.5); 545 | triggerFrames(); 546 | expect(myVivus.trace.calls.count()).toEqual(12); 547 | }); 548 | 549 | it('should start by the last path if reverseStack is enabled', function () { 550 | myVivus = new Vivus(svgTag, { 551 | type: 'oneByOne', 552 | duration: 5, 553 | reverseStack: true 554 | }); 555 | myVivus.setFrameProgress(0.5); 556 | 557 | var paths = svgTag.querySelectorAll('path'); 558 | expect(+paths[0].style.strokeDashoffset).not.toEqual(0); 559 | expect(+paths[paths.length -1].style.strokeDashoffset).toEqual(0); 560 | }); 561 | }); 562 | 563 | describe('Force Render:', function () { 564 | 565 | it('should use renderPath if forceRender option is set to true', function () { 566 | myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: true }); 567 | 568 | var originalFirstPath = myVivus.map[0].el; 569 | myVivus.renderPath(0); 570 | expect(myVivus.map[0].el).not.toBe(originalFirstPath); 571 | }); 572 | 573 | it('should not use renderPath if forceRender option is set to false', function () { 574 | myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: false }); 575 | 576 | var originalFirstPath = myVivus.map[0].el; 577 | myVivus.renderPath(0); 578 | expect(myVivus.map[0].el).toBe(originalFirstPath); 579 | }); 580 | 581 | it('renderPath should not throw an error if the index doesn\'t exists', function () { 582 | myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: true }); 583 | expect(function () { 584 | myVivus.renderPath(42); 585 | }).not.toThrow(); 586 | }); 587 | }); 588 | 589 | }); 590 | 591 | describe('[controls]', function () { 592 | 593 | beforeEach(function () { 594 | myVivus = new Vivus(svgTag, { 595 | type: 'oneByOne', 596 | duration: 2, 597 | start: 'manual' 598 | }); 599 | }); 600 | 601 | it('shouldn\'t play if the parameter in incorrect', function () { 602 | expect(function () {myVivus.play('a');}).toThrow(new Error('Vivus [play]: invalid speed')); 603 | expect(function () {myVivus.play({});}).toThrow(new Error('Vivus [play]: invalid speed')); 604 | expect(function () {myVivus.play([]);}).toThrow(new Error('Vivus [play]: invalid speed')); 605 | expect(function () {myVivus.play('1');}).toThrow(new Error('Vivus [play]: invalid speed')); 606 | }); 607 | 608 | it('should return the correct status', function () { 609 | expect(myVivus.getStatus()).toEqual('start'); 610 | myVivus.setFrameProgress(0.5); 611 | expect(myVivus.getStatus()).toEqual('progress'); 612 | myVivus.finish(); 613 | expect(myVivus.getStatus()).toEqual('end'); 614 | myVivus.reset(); 615 | expect(myVivus.getStatus()).toEqual('start'); 616 | }); 617 | 618 | it('should play with the normal speed by default', function () { 619 | myVivus.play(); 620 | expect(myVivus.speed).toEqual(1); 621 | }); 622 | 623 | it('shouldn\'t run another process of drawing if the animation is in progress', function () { 624 | myVivus = new Vivus(svgTag, { 625 | duration: 6, 626 | start: 'manual' 627 | }); 628 | spyOn(myVivus, 'trace').and.callThrough(); 629 | 630 | myVivus.play(0.5); 631 | myVivus.play(0.5); 632 | triggerFrames(); 633 | expect(myVivus.trace.calls.count()).toEqual(12); 634 | }); 635 | 636 | it('should stop the animation only when the animation is running', function () { 637 | myVivus = new Vivus(svgTag, { 638 | duration: 6, 639 | start: 'manual' 640 | }); 641 | myVivus.play(); 642 | expect(myVivus.handle).toBeTruthy(); 643 | myVivus.stop(); 644 | expect(myVivus.handle).toBeFalsy(); 645 | myVivus.stop(); 646 | expect(myVivus.handle).toBeFalsy(); 647 | }); 648 | 649 | it('should remove all unecessary styling on every path element', function () { 650 | var i, paths; 651 | myVivus.destroy(); 652 | 653 | paths = svgTag.querySelectorAll('path'); 654 | for (i = 0; i < paths.length; i++) { 655 | expect(!!paths[i].style.strokeDashoffset).toEqual(false); 656 | expect(!!paths[i].style.strokeDasharray).toEqual(false); 657 | } 658 | }); 659 | 660 | /** 661 | * Where are the tests about `util` methods? 662 | * Well.... 663 | * to be honest, I've been struggling a bit for these kind of tests 664 | * which seems difficult to test from Karma. 665 | */ 666 | }); 667 | 668 | 669 | }); 670 | --------------------------------------------------------------------------------