Picross pseudo-3D

This website shares an affection for scalable vector graphics, but also nonograms, intriguing puzzles where you fill a grid to conjure up interesting figures.

With SVG you are able to visualize solved puzzles quite effectively in two dimensions, and exploring a different projection you can even try and break the mold, so to explore a third axis.

The effect is quite convincing — there are certainly plenty of levels you can visualize in their complete, crisp state —, but with the benefit of a JavaScript library like Zdog you can go further, and really explore deeper shapes. Better yet, you are able to visualize more complex pieces. Not only squares, not only cubes.

SVG

Say you want to draw the eagle you found at the top of this very page. In SVG you are able to do so quite concisely with a single <path> element and a slightly elaborate d attribute.

<path d="M 3 1 A 1 1 0 0 1 4 0 L 5 0 5 2 6 2 A 1 1 0 0 1 7 1 L 9 1 A 1 1 0 0 1 8 2 L 7 2 A 1 1 0 0 1 8 3 L 6 3 A 1 1 0 0 1 7 4 L 6 4 7 5 6 5 5 4 5 5 6 6 3 6 4 5 4 4 3 5 2 5 3 4 2 4 A 1 1 0 0 1 3 3 L 1 3 A 1 1 0 0 1 2 2 L 1 2 A 1 1 0 0 1 0 1 L 2 1 A 1 1 0 0 1 3 2 L 4 2 4 1 Z" />

Like a pencil on a scrap of paper, you draw arcs and straight lines to mark the winged creature.

What if you break down the figure in pieces however, similar to one of the levels in nonogram puzzles? In the simpler instance of past, 2D efforts, we only had squares, we only had cubes. For these, we managed to describe a level with a multi-line string filled with two characters, “x”_s and“o”_s.

const level = `xxxoo
xxxox
xxxox
xxxox
xxoox
oooox
oooox
oxxox
oxxox`;

The approach here is similar, but there is a far greater number of shapes, a far greater range of letters.

const level = `
---EA----
DAB-A-EAC
-EAAAAAB-
--EAAAB--
--IGAHF--
---IAF---
`;

Starting with the letter “A” for the square, I decided to navigate the alphabet with each successive shape. First the round-ish wedges, then the triangles. The decision is not as important as picking a convention and sticking to it. In the end, you have a vocabulary, matching letters to specific shapes.

A B C D E F G H I

How do you draw the level? Again and in the simpler instance, we only had squares. Once we managed to build a grid of columns and rows, we needed to only add <rect> elements at precise x, y coordinates.

<rect x="3" y="0" width="1" height="1" />

With a greater number of letters, a greater number of shapes, you don’t need to check the value with a series of conditionals. Matching the vocabulary of characters, you are indeed able to create a collection of shapes in the <svg> element between <defs> tags.

<svg>
	<defs>
		<!-- ... -->
	</defs>
</svg>

Between the opening and closing bracket even regular shapes, even <rect> and <path> elements are not shown. This makes it possible to define the shapes with a specific id.

<defs>
    <rect id="A" width="1" height="1">
    <path id="B" d="M 0 0 L 0 1 1 1 A 1 1 0 0 0 0 0 Z" />
    <path id="C" d="M 0 0 L 1 0 A 1 1 0 0 1 0 1 Z" />
    <!-- ... -->
</defs>

Making sure to pair the id to the letter is bound to simplify our task a bit, but first, take a moment to appreciate the multi-line string once more.

const level = `
---EA----
DAB-A-EAC
-EAAAAAB-
--EAAAB--
--IGAHF--
---IAF---
`;

We need to break down the string in a manageable, helpful data structure, like an array where each item is an object with three properties: x, y and href.

{
    x: 3,
    y: 0,
    href: '#E'
}

The first two help to position the shapes with the matching x and y attributes. The third once again provides a perfect match for the href attribute, and is exactly how we draw one of the shapes with the <use> element.

<use x="3" y="0" href="#E" />

With the attribute you point to the element through the unique id.

Back to the task at hand, from string to array. The string shows the level in rows, so that the first step is to break down the sequence per the new line character “\n”.

const grid = level
    .trim()
    .split("\n");

For good measure I decided to call the trim function, removing white-space at the beginning and end, but eventually, you end up with an array of rows.

[
	'---EA----',
	'DAB-A-EAC'
	// ...
];

Looping through the rows, break the strings further to isolate the characters.

const grid = level
	.trim()
	.split('\n')
	.map((row) => row.split(''));

With this, you find a grid. A 2D array describing first the rows, then the columns.

[
	['-', '-', '-', 'E', 'A', '-', '-', '-', '-'],
	['D', 'A', 'B', '-', 'A', '-', 'E', 'A', 'C']
	// ...
];

And the construct is already useful to describe the dimensions of our graphic. What is the height of our SVG? That would match the number of rows, the length of the grid.

const height = grid.length;

What about the width? For that consider the number of columns, the length of any nested structure.

const width = grid[0].length;

With the two values we are able to size the SVG canvas in its tightest configuration.

<svg viewBox="0 0 {width} {height}">
	<!-- ... -->
</svg>

That being said, and personally, I get a little fidgety when it comes to nested arrays. I prefer a single, one dimensional list, describing only the necessary shapes. No need to consider the cells without letters after all.

You can streamline the logic with a reduce function or two, but for readability, it helps to create the data structure in multiple steps.

const uses = [];

First, loop through the grid, accessing the rows. Then again, loop through the rows to find the letters.

for (let row = 0; row < grid.length; row++) {
	for (let column = 0; column < grid[row].length; column++) {
		const letter = grid[row][column];
	}
}

With the character you can now build the desired object. And only when necessary.

if (letter !== '-') {
	uses.push({
		x: column,
		y: row,
		href: `#${letter}`
	});
}

The horizontal position, that refers to the index of the nested loop. The vertical measure, the index in the parent scope. For the reference you can point to the letter and, to be accurate, prefix the string with the hash character ”#”.

Add the object to the list and you obtain the precious collection. An array of objects with x, y, and href properties. To draw the level in its entirety, loop through the structure one last time to add the <use> elements. With Javascript, or the framework of your choice.

{#each uses as { x, y, href }}
	<use {x} {y} {href} />
{/each}

And marvel at the result.

Majestic as the eagle might be, I can see how the logic might raise a few questions. Among which: ”why?”. Indubitably, you find the same picture you first saw with one <path> element. This time, however, you have a vocabulary of shapes. You have a recipe to draw more than one figure.

What is more, you have the ingredients to build a more complex showcase in pseudo-3D.

Zdog

With Zdog you don’t have to pretend to draw a cube. For that you have a Box class describing the dimensions with three properties in total.

new Zdog.Box({
	width: 1,
	height: 1,
	depth: 1
});

The shape matches the first definition, the square, but what about the other, more rotund figures? What about the sharp wedges? Unfortunately, the library does not provide a class for the specific shapes, yet. That being said, the box illustrates a way forward.

A box is but a collection of faces, a collection of rectangles positioned with care and forethought. Think of six rectangles, glued together in a tight fit.

This means that in Zdog you can draw the cube with a Box, but also a series of Shapes.

new Zdog.Shape({
	path: [
		{ x: -0.5, y: -0.5, z: 0.5 },
		{ x: 0.5, y: -0.5, z: 0.5 },
		{ x: 0.5, y: 0.5, z: 0.5 },
		{ x: -0.5, y: 0.5, z: 0.5 }
	]
});

Instruct Zdog with the vertices and the library draws a rectangle. Just make sure to add fill to true to see the polygon in full.

new Zdog.Shape({
	// ...
	fill: true
});

You need to fabricate the faces, but for the rectangular shape the process is clear, if repetitive. For the face in the back you’d repeat the instruction with a negative z value.

path: [
	{ x: -0.5, y: -0.5, z: -0.5 },
	{ x: 0.5, y: -0.5, z: -0.5 },
	{ x: 0.5, y: 0.5, z: -0.5 },
	{ x: -0.5, y: 0.5, z: -0.5 }
];

For the sides, you’d change the depth while keeping the same x, or y coordinate.

path: [
	{ x: -0.5, y: -0.5, z: -0.5 },
	{ x: -0.5, y: -0.5, z: 0.5 },
	{ x: -0.5, y: 0.5, z: 0.5 },
	{ x: -0.5, y: 0.5, z: -0.5 }
];

You navigate the axis so that all together, the rectangles pack up to make a box.

How do you draw more complex shapes? In precisely the same manner. Think of a wedge as a combination of shapes, of faces with a more varied design.

Some faces are easy to sketch out — think of the bottom and left sides repeating the parts of the box. Some faces require a bit more tinkering. Consider the front and the back, for instance. On top of three vertices.

path: [
	{ x: -0.5, y: -0.5, z: -0.5 },
	{ x: -0.5, y: 0.5, z: -0.5 },
	{ x: 0.5, y: 0.5, z: -0.5 }
	//...
];

You need an arc back to the top.

path: [
	//... { x: 0.5, y: 0.5, z: -0.5 }
	{
		arc: [
			{ x: 0.5, y: -0.5, z: -0.5 },
			{ x: -0.5, y: -0.5, z: -0.5 }
		]
	}
];

For the curved surface, the syntax is even more mind-bending, and requires a shift in perspective. Not only you need an arc, up and to the right.

path: [
	{ x: 0.5, y: 0.5, z: 0.5 },
	{
		arc: [
			{ x: 0.5, y: -0.5, z: 0.5 },
			{ x: -0.5, y: -0.5, z: 0.5 }
		]
	}
	// ...
];

But also a segment, drawing the shape along the z-axis.

path: [
	// ... { x: -0.5, y: -0.5, z: 0.5 }
	{ x: -0.5, y: -0.5, z: -0.5 }
];

Do this, and retrace the curve back with another arc in the deeper layer.

path: [
	// ... { x: -0.5, y: -0.5, z: -0.5 }
	{
		arc: [
			{ x: 0.5, y: -0.5, z: -0.5 },
			{ x: 0.5, y: 0.5, z: -0.5 }
		]
	}
];

The sequence is far more intuitive, we are more used drawing in a 2D plane far more than are in 3D. That being said, it is almost beautiful as you appreciate the instructions.

You can draw a box and the faces for one of the round wedges. And this is one instance where I’m more than glad to repeat myself: you don’t need to run the code with a series of conditionals. You don’t even need to create as many definitions as you may think. In SVG, you were able to define the shapes with <rect> and <path> elements. In Zdog, you can achieve something similar with plain variables.

For the box, for the first shape, you can refer directly to the class.

const a = new Zdog.Box(/*...*/);

For the other composition, you can keep a reference to an Anchor class.

const b = new Zdog.Anchor();

The idea is simple. Once you set up the anchor, add the shapes to the specific construct.

new Zdog.Shape({
	addTo: b
	// ...
});

Do this for the five faces and the anchor works as a container of sorts. A container you can then re-use with the copyGraph method. You can even position the shape with the translate property at a precise location.

b.copyGraph({
	translate: { x: 3, y: 0 }
});

You might anticipate how the instruction completes our pseudo-3d picture, but in a convenient twist, you can rely on the method to save you even more time. Think back of the wedges, or again the triangles. You can draw the shapes one by one, and that was certainly the approach in SVG.

That being said, there is just one of each shape, rotated at different angles.

Instead of drawing the faces around the clock, you can repeat the same anchor, turning the shape on the z axis.

const c = b.copyGraph({
	rotate: { x: Zdog.TAU / 4 }
});

In much the same manner you are able to complete the vocabulary. For the first quartet and based on b, c, d, and e.

const d = b.copyGraph({
	rotate: { x: Zdog.TAU / 2 }
});

const e = b.copyGraph({
	rotate: { x: (Zdog.TAU * 3) / 4 }
});

For the second, more triangular set and based on a different collection of faces, f, g, h and i.

const f = new Anchor();
// ...add shapes to f

const g = f.copyGraph({
	rotate: { x: Zdog.TAU / 4 }
});

const h = f.copyGraph({
	rotate: { x: Zdog.TAU / 2 }
});

const i = f.copyGraph({
	rotate: { x: (Zdog.TAU * 3) / 4 }
});

You do need to build the one triangle — there’s a lot of work behind the scenes — but the effort is about to pay dividends. And the SVG, 2D détour is about to come back to save us once more.

You can rely on the same grid, the same collection of objects. If you are particular you might want to rename the array with a fitting label.

const copies = [];

But in the object, you can repeat the same values. The indexes to position the shapes, and on the letter to point to the variable.

copies.push({
	x: column,
	y: row,
	shape: letter.toLowerCase()
});

Out of preference, I decided to name the constants with a lowercase letter, but you can very well use the uppercase variant if you so choose. In the end, you can build the vocabulary with an object.

const shapes = { a, b, c, d, e, f, g, h, i };

To draw the shapes with Zdog, to draw the shapes in the illustration, you then loop through the helpful data structure. Truly one last time.

for (const { x, y, shape } of copies) {
	// ...
}

Within the for loop, you have access to all the values you need. The letter to copy the appropriate shape. The coordinates to position the piece.

shapes[shape].copyGraph({
	addTo: illustration,
	translate: { x, y }
});

And the code works. You can certainly repeat the instructions and take flight with the winged creature, now in pseudo-3D. Or, try your luck with a brand new level.

const level = `
---FEBI---
--IAAAAF--
-HAGEBHAG-
DAGEDCBHAC
-H-C--D-G-
`;

After all, you can draw an eagle, and any other puzzling figure. Again, there’s a lot of work in terms of setup, in terms of vocabulary and shapes. But if you go through the effort, with a bit of trial and error, you can appreciate the result. And you might even see the figure through letters. Even upside-down.