Beyond 2D SVG

If you are reading this article, chances are you share a liking for vector graphics. In the blog, there is no shortage of material devoted to the topic, and the results are often remarkable. Possibly pointless, but always intriguing.

There is a lot you can achieve writing SVG by hand and relying on the SVG specification only, but there are features which extend the scope of the language. Or at least, features where a scripting language like JavaScript help to make a much more attainable reality. For instance: 3D shapes. You can add a sense of depth with a judicious use of color or again size. You can even play with different projections to branch into fresh new directions. When it comes to higher dimensions, however, there are libraries to ease the task.

The end-all be-all seems to be three.js. The wall of projects you find in the landing page are bound to occupy you for a solid afternoon, but I find the library as exciting as overwhelming.

For a softer introduction, Zdog promises to escape the second dimension with a more approachable API. Better yet, the library works with a <canvas> or an <svg> element. And we’re quite acquainted with the latter.

Ready

It is easy to tinker without aim, so let’s set a clear goal: take a vector and recreate the shapes in pseudo-3D.

For the SVG, a few websites offer illustrations to decorate your content, and themeisle has an impressive showcase which fits our needs. The visuals are beefy enough to challenge the 2D label already, and simple enough we can draw them ourselves with a handful of elements.

Code snippets A folder hinting at the many lines written in code.

A few <path>s, several more <circles>s. There’s nothing inherently complex about the syntax. And do not worry, you’ll get the opportunity to appreciate the code as we recreate the shapes with the library.

Set

We have our SVG with a fixed width and height.

<svg width="400" height="300">
	<!-- ...gorgeous artwork -->
</svg>

I tend to rely on the viewBox attribute to size most vectors, but as Zdog begins with fixed measures I decided to follow the example.

With JavaScript, you are going to target the node to create an illustration.

const element = document.querySelector('svg');
const illustration = new Zdog.Illustration({
	element
});

You add shapes on this illustration with other classes, always from the Zdog object.

new Zdog.Ellipse({
	addTo: illustration
});

Finally, you complete the picture with one essential method: updateRenderGraph.

illustration.updateRenderGraph();

The process is rather intuitive, and we can almost start drawing, but there is one short stop before we get there. One instruction which will simplify our work at length.

In a Zdog illustration you draw the shapes from the center of the canvas. In the <svg> element, I drew the artwork from the top left corner.

<svg width="400" height="300">
	<circle transform="translate(30 210)" r="7" />
	<!-- ... -->
</svg>

Now, you can adapt the values to the new origin. If there is a circle drawn 30 units right, 210 units down from the top left, you can certainly figure out where to position the shape from the center. A quick subtraction should solve the issue.

But we have JavaScript at our disposal, and in the spirit of making life easier, and doing less work in the process, there is a better way.

In SVG, you have group elements as general, helpful containers. With Zdog, you have a Group class with a similar purpose.

const group = new Zdog.Group({
	//...
});

The idea is then simple. Create a group and add it to the illustration.

const group = new Zdog.Group({
	addTo: illustration
});

Apply a translation to this group, moving the origin back to the top left.

const group = new Zdog.Group({
	addTo: illustration,
	translate: { x: -200, y: -150 }
});

When it comes to drawing shapes, then, add them to the group itself. .

new Zdog.Ellipse({
	addTo: group,
	translate: { x: 30, y: 210 }
});

Code. Making life easier.

Lift off

Around the makeshift folder you find a series of dots. And in SVG, these are drawn with <circle> elements at different x, y, coordinates.

<circle transform="translate(30 210)" r="7" /> 
<circle transform="translate(45 120)" r="7" />

I repeated the element with a given radius and positioned the dots with the transform attribute. To add a touch of variety, then, I took advantage of the attribute to scale the figures as well.

<circle transform="translate(30 210) scale(0.9)" r="7" />
<circle transform="translate(45 120) scale(0.6)" r="7" />

In Zdog, we can recreate the dots with an ellipse, the first class explained in the docs, but a Shape is the best class to draw a perfect sphere. A Shape with a positive stroke.

new Zdog.Shape({
	addTo: group,
	stroke: 14,
	translate: { x: 30, y: 210 }
});

You can draw one sphere in a spot, and then repeat the process for the other copies. And once again, we can set ourselves up to success with some logic.

One array, describing the position and scale of the particles. You can very well copy the values from the transform attribute since we took care to reposition the group.

const circles = [
	{ x: 30, y: 210, scale: 0.9 },
	{ x: 45, y: 120, scale: 0.6 }
	// ...
];

One loop, cycling through the data structure to extract the values and inject them in the class.

for (const { x, y, scale } of circles) {
	new Zdog().Shape({
		addTo: group,
		stroke: 14,
		translate: { x, y }
	});
}

There is a dot-sized issue for the actual scale. Zdog supports a scale property, in the same fashion as translate. Even if you add the line, however, the spheres do not change in size.

new Zdog().Shape({
	// ...
	translate: { x, y },
	scale
});

The problem: scale does not work on the stroke. But there’s always a solution, and in this instance, you can work around the issue in a rather dumb way. If the scale property does not update the stroke, you can update the value yourself.

Instead of scaling the class, the figure, resize the stroke, the diameter.

new Zdog().Shape({
	// ...
	stroke: 14 * scale,
	translate: { x, y }
});

Problem, solved.

On to more complex shapes — this might be a soft introduction, but we want to reach for the sky.

Always around the folder you find a couple of particles. Just two of them, but their presence is more than welcomed. For these, you have to thank a <path> element drawing a star with four arcs.

<path d="M -5 0 a 5 5 0 0 0 5 -5 5 5 0 0 0 5 5 5 5 0 0 0 -5 5 5 5 0 0 0 -5 -5" />

One <path>, repeated twice at different x, y coordinates, exactly as the circles. To this end, we can repeat some of the logic.

One data structure, detailing the sparkles.

const sparkles = [
	{ x: 80, y: 250, scale: 1 },
	{ x: 385, y: 155, scale: 1.2 }
];

One loop, extracting the values and drawing a shape.

for (const { x, y, scale } of circles) {
	new Zdog().Shape({
		addTo: group
		// ...
	});
}

Not a circle, however. And therefore, not through the stroke alone. In Zdog, strokeis essential to add depth. And in the context of a Shape, if you rely only on the stroke you end up with a perfect sphere.

To draw more complex figures, the key is the path property.

new Zdog().Shape({
	addTo: group,
	path: [
		{ x: -5, y: 0 },
		{ arc: [] }
	]
});

A point to start, an arc to follow.

Except, once again we stumble on a small obstacle. I might have drawn the particles with arcs, but in the d attribute, I decided to draw them with relative numbers.

<path d="M -5 0 a 5 5 0 0 0 5 -5 ..." />

Move 5 units left, draw an arc 5 units right and above.

In Zdog, however, the coordinates are absolute values.

const path = [
	{ x: -5, y: 0 },
	{ arc: [
		{ x: 0, y: 0 }, // corner point
		{ x: 0, y: -5 } // end point
	] }
];

And this time, there is no shortcut. You need to adjust the values by hand. And the next time, consider absolute numbers when writing SVG as well SVG. At least when you plan to move to Zdog.

const path = [
	{ x: -5, y: 0 },
	{ arc: [{ x: 0, y: 0 }, { x: 0, y: -5 }] },
	{ arc: [{ x: 0, y: 0 }, { x: 5, y: 0 }] },
	{ arc: [{ x: 0, y: 0 }, { x: 0, y: 5 }] },
	{ arc: [{ x: 0, y: 0 }, { x: -5, y: 0 }] }
];

Arc after arc, you are able to recreate the sparkle, but then need a stroke. A fixed stroke in this instance.

new Zdog().Shape({
	// ...
	stroke: 4
});

translate works to position the shape and this time scale does work to resize the shape.

new Zdog().Shape({
	// ...
	translate: { x, y },
	scale
});

Let’s move on to the center piece, the folder smack in the center of the illustration. For this, we have three <path> elements, and starting with the two brackets right in the middle, we get to know another useful part of the library.

In SVG, you can draw the brackets with two separate paths, two distinct d attributes. Or, create just the one, and repeat the same with a negative scale.

<path d="M -25 -28 -53 0 -25 28" />
<path transform="scale(-1 1)" d="M -25 -28 -53 0 -25 28" />

You are essentially flipping the coordinate system around the x axis. The first path might draw an arrow to the left, but the mirrored version points to the right.

And in Zdog, the logic is conveniently similar. You can draw the brackets with two separate classes, two distinct path properties. Or, create just the one.

new Zdog.Shape({
	// ...
	path: [
		{ x: -25, y: -28 },
		{ x: -53, y: 0 },
		{ x: -25, y: 28 }
	]
});

Keep a reference to the object in a variable.

const bracket = new Zdog.Shape({
	// ...
});

Repeat the shape with the copy function and the still-working, negative scale.

bracket.copy({
	scale: { x: -1 }
});

I know you are ready to throw in the towel, but in one last stretch, try to consider the final piece. The bulky folder in the background…

<path
	d="M 0 -87.5 h 100 c 35 0 35 40 35 50 v 70 c 0 60 -50 60 -75 60 h -120 c -40 0 -75 -10 -75 -60 v -70 c 0 -30 0 -80 45 -80 c 80 0 80 0 90 30"
/>

You might be staring at the snippet with dread. And for good reasons. As I drew the illustration I kept using lowercase characters, I kept using relative numbers. And if the Zdog library does support bezier curves with a bezier key, I won’t subject you to the manual conversion between the instructions.

You’ve endured enough, and are more than excused to reap the rewards.

To get started, you can change the rotation of the illustration, on one of the possible axes.

illustration.rotate.x = Zdog.TAU / 64;

You do need to consider an angle in radians, but Zdog gives a reference to a full rotation, to twice Math.PI. And the good news is that every shape will rotate from the center even if you were cheeky enough to add the classes from somewhere else.

The result? I don’t have it. There isn’t even a plot twist where you find the graphic at the top of the page was pseudo-3D all along. I have the multi-dimensional example on CodePen, and you’ll need to visit my profile to be impressed. I might not have a wall of projects, but you might be entertained by a few demos…

Anticlimactic? I get it. And as I often remedy these situations, let me offer you something new to send you off on a higher note.

Cosmic adventureIn the vast space a solitary ship roams among the stars.

The really good news? You can very well create something similar all on your own. I tried to animate the piece, but outside of the smooth change, the logic is eerily similar.

Take a moment to appreciate the library, the detailed docs and you are primed and ready for adventure. Primed and ready to explore the dimensions. x, y and z.