├── .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 |
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 |
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.
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 |
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 |
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.
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 | 
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 | 
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 | 
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 |
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 |
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 |
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 | 
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 |
264 | ```
265 |
266 | This scenario should give us
267 |
268 | 
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 |
--------------------------------------------------------------------------------
/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 |
160 | To use this page you must use an HTTP server to serve files. Run
161 | npm run serve in the repository then go to the
162 | test page
163 |
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 |
243 |
253 |
254 |
255 |
265 |
266 |
267 |
277 |
278 |
279 |
280 |
281 |
291 |
292 |
293 |
303 |
304 |
305 |
315 |
316 |
317 |
318 |
319 |
329 |
330 |
331 |
341 |
342 |
343 |
353 |
354 |
355 |
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 |
--------------------------------------------------------------------------------
/test/manual/polaroid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/manual/synth.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------