Nudity & the WebSocket

this is the canonical original for an essay hosted on Medium

Drawing life with SVG images and websocket messages

So you are a techie and an artist, a tartist, well this article is for you, here we investigate how to transport massive SVG drawings over WebSockets, with LZW string compression and unpack it into a user interface.

By massive drawings, I mean a file anywhere from 2 MBytes and up. The illustration above, weighs just over 6MB, in its raw format. Which by web standards is enormous. Still, if you deliver it in tiny pieces, then the interface can start responding immediately, and the user is not waiting for something to happen. In essence, it is a zero wait accumulative experience. The byproduct of streaming the sketch, line by line, is that the user gets an instant response and they have the “experience” of the drawing process, as it occurred initially.

See it for yourself. http://lifedrawing.fliptopbox.com/#5

This article is NOT a step-by-step tutorial, rather a commentary on various ideas that were used to achieve the outcome. It is also a self-criticism looking back at my code almost six years later, a confession and an appreciating how JavaScript has evolved and me along with it.

You will learn more from your own labour of love than a search engine answer. One teaches you to “learn”, the other teaches you to copy. Do both in equal measure.

How did you make the SVG sketches?

You enrol in a life-drawing class, and instead of pencil and paper, you use a digital pen, that records your pen strokes. Have you ever heard of Wacom Inkling? It is (unfortunately) a dead product. A great sadness because that is what I use. It is an ink pen with a clip-on transceiver, which you attach to a page and it records the pen strokes. When you are done, you use the Wacom software to convert the raw data into SVG illustrations, which you can manipulate with a vector drawing program, like Inkscape or Illustrator.

The successor to the Inkling looks to be the Neo Smartpen N2, whatever product you use the vital point is that it exports vector illustrations and if it does SVG natively that’s awesome.

After many awkward months staring at naked people and desperately trying to improve your eye-hand coordination, you should have a body of work to use for this proof of concept. Alternatively, I have some SVG you could borrow. I highly recommend the naked people route, first.

In principle, this technique will work for any SVG, provided that the XML-DOM conforms to a specific structure, and we get to determine that schema. More about that later.

Before we go technical, there is one thing to say about drawing with a ballpoint pen. Not many people do it, for one straightforward reason. It is unforgiving. The pencil you can smudge or erase and that helps submerge your mistakes, not pen. Oh, no, siree! You see everything.

As an artist, I like this discipline; it forces you to commit to the stroke. And when the mistakes pile up, it also shows how you searched for the line of the form and suggests how your eye explored the shapes and undulations as it struggled to transpose them onto the page. Almost as if a fly, dipped in ink was walking around.

The great thing about an “SVG pen”, it that you get edit your drawing afterwards. And I did exactly that, I could not remove the pen mark from the paper, but I could remove the path from the canvas. And change fill and stroke and composition, etc.

TL;DR So here is the full round trip overview

Open the SVG as a text stream, extract attributes, compress the payload, send JSON and finally, reconstruct the SVG on the client-side. And now these same headlines with code snippets.

From the server, we want to take an SVG document …

<svg
width="598px"
height="697px"
viewBox="0 0 598 697"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
>

<title>portrait in 60 seconds</title>
<desc>(c) 2014 Bruce Thomas - Life drawing</desc>
<defs></defs>
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
sketch:type="MSPage"
>

<g
id="Sketch166"
sketch:type="MSLayerGroup"
stroke="#9B9B9B"
stroke-width="0.0966"
fill="#4A4A4A"
>

<path
id="Shape"
sketch:type="MSShapeGroup"
d="M328.624,374.943 L329.252,373.2 L329.881,371.648 L330.564,370.32 L331.039,368.955 L331.477,367.821 L331.575,364.073 .....(very, very long).... L330.303,365.281 L330.249,365.506"
>
</path>
/>
</g>
</g>
</svg>

Turn it into Javascript Object literals, for JSON.stringify() …

{ type: "svg", attributes: {width: "598px", height: "697px"} }
{ type: "title", value: "portrait in 60 seconds"}
{ type: "desc", value: "(c) 2014 Bruce Thomas - Life drawing"}
{ type: "defs", value: ""}
{ type: "g", attributes{id: "Page-1" stroke: "none",
"stroke-width": "1" "fill": "none" "fill-rule" "evenodd"
}}
{ type: "g", attributes: {id: "Sketch166"} }
{ type: "path", attributes: {id: "Shape",
"d": "M328.624,374.943 L329.252,373.2 L329.881,371.648
L330.564,370.32 L331.039,368.955 L331.477,367.821
L331.762,366.771 ........ L330.346,364.787
L330.249,365.506"
}}

And then finally send a heavily compressed UTF8 string, like this …

M328.624,374.943 LĂ9.252ĉ73ĕđē.881ę1ą48ĝ30.56ĈĊĪĂ Ē3Ĥ039ĉ6Ą955ĨĤ477Ĺ7ğ21Ŀ.7ĆĹ6Ŋ7ňijĤ8ĬĹ5ņIJ3ĴŊ98Ŗ.06ʼn753ĹČ514ʼn5ŤŧŠĚʼnķŞ36ěĠ3řśŇĹŷĐŒŠ0ĮŶğŚĨĪġŦŵŷďƇą9ƊƄ99ź......ɌȺțƝȯǜĠɧʮ͜ȱ

… to the browser, which will “unzip” the compressed string, JSON parses it back into an Object so that a switch statement can identify the element type (namespace), and create the corresponding DOM Element, nest each of the incoming parts into respective child Elements. And then do it all over again, for the next sketch. The end.

LZW string compression

Okay, what is this black magic? And why should we worship this devil? Consider this argument; here is a sequence of numbers: 65,66,67 and the byte length is 8 (incl commas). However, if we used an ASCII character, in place of the numbers, we get this “ABC”, with a length of 3.

Three really smart guys: Lempel, Ziv and Welch (LZW) used this idea in their algorithm which became defacto lossless compression for GIF and TIF. Except they went one step further, they added them up so 65 + 66 + 67 = 198 (or the character: “Æ”) with a length of … 1.

Very clever stuff.

If you want to explore this for your self here is a String prototype, that impliments LZW compression. as a Gist.

How do we stream XML?

Spoiler alert, you don’t. When I began this project (a long time ago) I made the initial mistake of reaching for XPATH — the traditional way to work with XML. BUT I abandoned that because essentially it needs to open the entire file BEFORE it can do it’s parsing. In that case, you may as well use GET to fetch the SVG and wait for the download. You see XML, HTML and SVG are DOMs. Some more strict than others. Luckily XML/SVG is very strict.

I abandoned the XPATH approach and decided to trust that SVG is strict; this allows us to make some presumptions. All tags are balanced; for every opening tag, there is a closing tag. (Unlike HTML, which is very tolerant, eg. is an unclosed tag). The other assumption is that everything is nested. This means we can use a text stream to determine the parent element, into which the others will be appended.

Let’s use an example to illustrate this.

On the server we are running node, with two NPM packages, “websocket” and “express”, the rest is default node. The server is hosted with Heroku, who give you a “Dyno” for-free, for-ever, for-personal projects.

const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
input: fs.createReadStream(svgFileName),
output: process.stdout,
terminal: false
});

rl.on('line', (text) => {
//// gimme that line, everything up to the \n
});

readLine will provide us with this first line …

<svg width="598px" height="697px" viewBox="0 0 598 697" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">

… when it starts streaming the text file line by line, it’s not closed, but … that’s all we need because the client-side JavaScript will create the closed Element, all it needs are the relevant attributes. The stuff in bold. The other attributes are all part of the Element’s namespace, which get generated automatically.

The client-side code will be a loop that does something like this:

const ns = "http://www.w3.org/2000/svg";
const type = "svg";
const el = document.createElementNS(ns, type);
el.setAttribute("width", "598px");
el.setAttribute("height", "697px");
el.setAttribute("viewBox", "0 0 598 697");

This means our JSON schema for the Socket Message is simple, so far, and takes care of 4 out of 7 tags that we need to use. { type: "svg", attributes: {width, height, viewBox}} The next type of tag we need to accommodate will take care of the remaining three. This type has NO attributes and a single text element as it’s a child.

<title>portrait in 60 seconds</title>
<desc>(c) 2014 Bruce Thomas - Life drawing</desc>
<defs></defs>
For these, we extend the schema into its final shape.
{
type: "String",
attributes: {Object} -OR- null,
text: "String" -OR- null
}

So with the schema for the WebSocket messages done, we need to figure out a way to pluck the type and attributes (or value) from the incoming line of text and ensure that it ignores the cruft, i.e HTML comments and the now un-needed closing tag. This is an ideal candidate for …

Regular Expressions

The regular expressions below are all that we need. Admittedly we will use them in two distinct parts, but primarily it identifies the tag element, and the attribute key/value pairs, while ignoring the cruft.

/<(\w+)\s(([\w]+)="([^"]+)"|[^<]+)>/gi ///// tag with attributes
/<(\w+)>([^<]{0,})/gi ///// tag with innerText only

Perhaps you are already confident with regex, maybe not, either way, please check out this phenomenal website, regex101.com. I have prepared a case study for you, related to this article. I use this tool often. So useful for debugging! So gooooood! Enjoy.

attributes — https://regex101.com/r/AHAspm/1/

innerText — https://regex101.com/r/AHAspm/3

key/value capture group — https://regex101.com/r/AHAspm/4 (screenshot) notice that this regex is actually the last capture group of our first RE, the part in bold

what you don’t see here are: the explanation, quick reference, substitutions & unit testing panels. So good. So good.

What I like about using RegEx is that you write one expression, and use as both an integrity check — using test() — and to extract content — using match(). The remarkable thing about match is it puts you in in Array land and all the lovely methods that come with ES6 Array.prototype.

Here is a comparison of how it was originally, and how the new refactor is achieving the same objective, the first loops through an array, splits pairs on “=” strips off unwanted quotes and spaces, then adds a key and value to a global Object. The second does not iterate in a loop, it uses methods and returns the complete Object. No global variable. Both use mainly the same RegEx that has not changed, but the code has … dramatically.

Original

var pairs = text.match(/\s([^=]+="[^""]+")/g);
var atts = {};
// Parse the correctly formatted XML text line into
// attribute pairs, and store them in a dictionary
pairs.forEach(function(pair, n) {
var array = pair.split('='),
key = String(array[0]).trim(),
value = String(array[1])
.trim()
.replace(/^"|"$/g, '');

// add the pair to the dictionary
atts[key] = value;
});

Refactor (a single line of code, broken for readability)

const pairs = /\s?(([\w:]+)="([^"]+)")/g;
// Create an Object by concatenation of mapped results
const attributes = Object.assign.apply({},
attributes.match(pairs)
.map(string.match(/(\w[^=]+)="([^"]+)"/).slice(1,3))
.map(([key, value]) => ({[key]: value}))
);

To turn great code into shit code … just wait five years, sometimes as little as 3 months will do the same :D

It was difficult to release the code for this project because it is so out-of-date. I was going to refactor it into ES6, and pretend that it’s better than it was. And I am doing that, but I wanted you to know the code itself is not important. The problem is important because the problem stays the same.

The front-end is where it all happens

There is a gotcha on the front-end, and it relates to the SVG group (“g”). A group is like a folder on your file-system; it can contain files or other folders. So knowing which folder to put the file in is the gotcha.

Creating the SVG Element is simple, they all share the same pattern, with one exception, and for our schema, a single function will take care of all the use cases.

function getSvgElement(
type,
attributes,
classnames = null,
ns = 'http://www.w3.org/2000/svg'
) {
const el = document.createElementNS(ns, type);
// handle inner text
if (typeof attributes === "string") {
el.innerHTML = attributes;
}
// handle attribute Object
if (attributes && attributes.constructor === Object) {
Object.entries(attributes).forEach(array => {
const [key, value] = array;
el.setAttribute(key, value);
});
}
// add CSS classnames (if they exist)
if (classnames && classnames.constructor === Array) {
el.classList.add(...classnames);
}
return el;
}
Since we are not traversing the “folder hierarchy”, merely putting stuff into the last group, we can use the array slice() method to ensure the incoming paths go into the correct group. A word of caution … avoid working directly on the SVG DOM at all cost.
It is cheaper on performance to keep a reference to a DOM element than to always fetch it with something like document.getElementById(). What I ended up doing was appending the SVG group element, to its parent in the DOM, and storing the same Element in an array. All subsequent appends to the group is done by slicing the last Element from the reference Array.
// a snippet of the above explanation
// it uses the function getSvgElement() defined above
this.groups = [];
this.svg = {the root SVG Document};
select (type) {
...
case 'g':
const last = this.groups.length;
const classnames = ['svg-group', `group-${last}`];
const g = getSvgElement(type, attributes, classnames);
//// if the group array exists use the last element
const group = last === 0 ? this.svg : this.groups[last - 1];
group.appendChild(g);
//// keep a reference (used by 'path' below)
this.groups.push(g);
break;
case 'path':
// a path is always nested within a group
// so grab the DOM reference for last added group
let parent = this.groups[this.groups.length - 1];
const path = getSvgElement(type, attributes, ['svg-path']);
parent.appendChild(path);
break;
...
}

Finishing touches

Now that we had got all the lines into the DOM, we can enhance the sketch with some random CSS styling. In the GIF below you will notice splashes of very thick white lines. And if you watch closely you will also see some lines get thinner and much darker (on the shoulder, and left eyelashes). These punch a little bit of extra contrast into the image, this is all post-processed and adds a pleasant aesthetic detail, which makes each sketch truly unique by entropy.

The highlight post-process only applies to paths over a certain length, and those paths were randomly filtered for an even smaller subset. Bear in mind that some of these sketched contain well over 1800 paths, and a single path can easily exceed 25,000 characters. So smacking the DOM with last-minute CSS updates is best limited to a minimal range of elements.

The actual SVG is 8.075 MBytes — but the interface feels fast … right? (GIF made with Peek)

Utilities that helped compose this article:

If you are like me then you love to check out the “other” things … the stuff in the background, and that other devs use; a plugin or tool or window manager, so here are some things to take with you.

Request from the author — hi, this is my very first Medium article, so I would really appreciate your feedback. Is this article too long, too vague, too boring? Whatever it is let me know, I am keen to improve my game.0