Create a gist now

Instantly share code, notes, and snippets.

Draw an SVG bit by bit
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;
}
@TimothyGu

As this is MIT licensed, I added a standard MIT license header over at my fork.

Feel free to merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment