Immutable Metadata

How the 21MMpixels contract immutably determines the position of a tile

Kevin
Coinmonks
Published in
5 min readFeb 8, 2022

--

One of the features of the 21MMpixels contract is that the position of every tile can be determined with a contract view function, tileDescriptionArray, or its friendlier counterpart, tileDescription. Unlike most NFT projects with hosted metadata files, there is no way for the position of a tile within the 21 million pixel image to ever be altered, it is truly immutable.

The easy solution to this problem would have been for tile 1 to be at the upper left corner, with tile numbers increasing sequentially by row or column. This solution seemed too easy, and not in the spirit of rewarding early purchasers of 21 million pixels tiles. We decided that tiles would be issued in a diagonal sequence, such that the left most tiles (x-coord = 0) would be 1, 2, 4, 7, 11…, and the top tiles (y-coord = 0) would be 1, 3, 6, 10, 15…

Grid showing tile locations in 21 million pixels image

That second set of numbers may be familiar to some, as they represent triangular numbers, a count of the number of objects in an equilateral triangle. Unfortunately with a rectangular image that consists of a grid of 70 x 120 tiles, this sequence only holds for the first 2485 tiles (the 70th triangular number in the sequence), at which point the top tiles with a y-coordinate of 0 increase in increments of 70 until we get to another set of tiles with triangular number properties.

Unless you’re drawing a 70 x 120 tiles grid as you read along, this is likely somewhat confusing. I feel like the actual code makes this easier to understand, so I will cover it in the three segments outlined above (plus the special case of Tile 1).

Tile 1

It’s a special case in our logic, located at X: 0, Y: 0, row equal 0 and column equals 0.

Tiles 2–2485

For these tiles, we use a while loop to find a number that is larger than the tile number minus 1 (tempId). This loop modifies two variables, ind and bigger, both initialized at 0. With each loop we first increment ind by one, and then calculate bigger as ind plus bigger (we’re calculating triangular numbers minus the row number plus one). We continue to loop while bigger is less than tempId.

Loop to determine tile position

At this point, if bigger is equal to tempId we know that the row is equal to ind, and column is zero. Otherwise, row equals bigger minus tempId minus 1, and column equals tempId plus ind minus bigger.

Tiles 2486–5985

This is the easy portion, straight forward artihmetic. We use the way that Solidity does integer math to calculate ind as the (tile number minus 1 minus 2485) / 70. In other programming languages, this would be the equivalent to a floor or round-down calculation. We then calculate bigger as 2485 plus ind * 70. If bigger equals the tile number minus 1, row is 69, and column is equal to ind plus 1. Otherwise, column equals the tile number minus bigger plus ind, and row equals bigger plus 69 minus the tile number minus one.

The easy arithmetic for tiles in the middle.

Tiles 5486–8400

We’re back to triangular numbers again. We once again use the variables ind and bigger, with initial value of 0 and 5985 initially. With our loop we once again increment ind, and then calculate bigger as bigger plus (70 — ind) while bigger is less than the tile number minus 1 (tempId).

The where loop for tiles 5985–8400.

If bigger is equal to tempId, column equals ind plus 51, and row equals 69. Otherwise, column equals 120 plus tempId minus bigger, and row equals bigger plus ind minus tempId minus 1.

Tiles 10,001–18,396 (if they exist)

We subtract 10,000 from the tile number and use the math above. You’ll notice tile numbers 18,397–18,400 cannot exist, which we’ll cover when we detail the find adjacent and merge tile functions.

Tiles 20,001–28,376 (if they exist)

We subtract 20,000 from the tile number and use the functions for determining the position of tiles 1–8,400. Like tiles 18,397–18,400, tile numbers 28,377 and above can not exist.

Determining the Coordinates and Dimensions

To determine the (x, y) coordinates of the tile, we multiply both row and column by 50. Tile numbers 1 — 8,400 have dimensions of 50 x 50 pixels, tiles numbers 10,001 — 18,396 have dimensions of 100 x 100 pixels, and tile numbers above 20,000 have dimensions of 200 x 200 pixels.

Final Notes

As you likely noticed, the loops in the tileDescriptionArray function are not gas optimal. Since this is only used as a view function, we chose to keep the logic simpler in this function. The _adjacentTiles function in the 21MMpixels contract is used in both a view function, as well as in the mergeTiles and unmergeTiles functions. While the logic to finding adjacent tiles is similar to determining the position of tiles, we made certain changes in order to both minimize gas use, and to make gas use more consistent across the full range of tile numbers. We’ll cover the _adjacentTiles determination in our next article.

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also, Read

--

--