Nudity and the WebSocket

Bruce Thomas
Feb 9 · 11 min read
Streaming pen strokes over WebSockets
Streaming pen strokes over WebSockets
screenshot of lifedrawing.fliptopbox.com

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?

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.

So here is the full round trip overview TL;DR

<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.


String compression with LZW

If you want to explore this for your self here is a String prototype gist.


How do we stream XML?

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. <img src=”thing.jpg” > 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 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

/<(\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.

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 resultsconst 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

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

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:


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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade