var steps = 0 // steps drawn on the current element, so far , info = document.getElementById('next_step') , svgs = [].slice.call(document.getElementsByTagName('svg')) , svg, walker, current; function lexSVGPath(d) { function command(seg) { var cmd = seg.match(/[a-z]/i), arg, cnt; if (cmd) { cmd = cmd[0]; // which subcommand cnt = arg_cnt[cmd.toLowerCase()]; arg = seg.match(numbers) || []; lexemes.push([cmd].concat(arg.splice(0, cnt))); if (cnt) while (arg.length) lexemes.push(arg.splice(0, cnt)); } } var numbers = /[+-]?(\d+(\.\d+(e\d+(\.\d+)?)?)?|\.\d+(e\d+(\.\d+)?)?)/gi , arg_cnt = { m: 2, z: 0, l: 2, h: 1, v: 1, s: 4, c: 6, q: 4, t: 2 } , lexemes = []; d.split(/(?=[a-z])/i).forEach(command); return lexemes; } function pathSegments(path, n) { function cmd(c) { return /[a-z]/i.test(c[0]) ? c[0] + c.slice(1).join(' ') : c.join(' '); } if ('string' === typeof path) path = lexSVGPath(path); path = path.slice(0, n); return path.map(cmd).join(' '); } function next(node) { return ({ g: NodeFilter.FILTER_SKIP , path: NodeFilter.FILTER_ACCEPT , circle: NodeFilter.FILTER_ACCEPT })[(node.nodeName||'').toLowerCase()] || NodeFilter.FILTER_REJECT; } function step(button) { var d, colour, styles, path, left = step.left; if (!current) { svg = svgs[svg ? (svgs.indexOf(svg) + 1) % svgs.length : 0]; walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT, next, !!0); left = 0; // unfill all paths, hide all non-paths, save colours and path descriptions while ((current = walker.nextNode())) { ++left; if ((d = current.getAttribute('d'))) { current._d = d; styles = getComputedStyle(current, ''); current._c = 'none' === styles.fill ? styles.stroke : styles.fill; current.style.fill = 'none'; } else current.style.display = 'none'; } button.title = button._t = pluralize(step.left = left, 'item') +' left'; // reset the walker and move to the first element walker.currentNode = walker.root; current = walker.nextNode(); steps = 0; } // not a path? just make it show up, and proceed to the next node if (!(d = current._d)) { current.style.display = ''; current = walker.nextNode(); button.title = button._t = pluralize(--step.left, 'item') +' left'; return; } // set the stroke to the element's fill colour and break the path into atoms current.style.stroke = current._c; path = lexSVGPath(current._d); // if we're not finished with all steps, just draw up to the next step if (++steps <= path.length) current.setAttribute('d', pathSegments(path, steps)); else { // all done; fill it and proceed to the next image sub-element current.style.fill = ''; current.style.stroke = ''; current = walker.nextNode(); button.title = button._t = pluralize(--step.left, 'item') +' left'; steps = 0; } button.title = button._t + '; sub-step '+ steps +'/'+ path.length; // completed? if (!current) { button.textContent = 'Done!'; explain('Click again to repaint the next SVG image!'); } else if (steps && steps === path.length) explain(button.textContent = 'Fill this SVG path element'); else { button.textContent = 'Draw next segment:'; explain(); } } function pluralize(n, name) { return n +' '+ name + (n === 1 ? '' : 's'); } function explain(msg) { if (!msg) { var path = lexSVGPath(current._d) , next = pathSegments(path, steps + 1) , what = next.replace(/^.*([a-z])[^a-z]*$/i, '$1') , type = what.toLowerCase() , args = path[steps].join(' ').replace(/^[a-z]/, '') , desc =({ m: 'move to' , z: 'close path' , l: 'line to' , h: 'horizontal line to' , v: 'vertical line to' , s: 'smooth curve to' , c: 'curve to' , q: 'quadratic Bézier curve to' , t: 'smooth quadratic Bézier curve to' })[type] , pref = type == 'z' || type == what ? '' : 'absolute '; msg = pref + desc +' = '+ what + args; } info.textContent = msg; }