NFT images generator with Javascript Nodejs (800,000 cryptopunks)
Tags: blockchain, javascript, node, tutorial
Published: 2021-09-05 10:48:10


CryptoPunks is one of the most popular NFT projects out there. And now, they are selling for millions of dollars. Yeah I know! Shocking! There are only 10,000 uniquely punks. Each of them has a set of attributes which make them special and stand out from rest.

## Cryptopunk Types and Attributes 
#### Punk Types
1. Alien
2. Ape
3. Zombie
4. Female
5. Male

#### Attributes 
There are approximately 89 attributes avail for each punk type.
![punk attributes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks3zgy7f8us4l2707dnh.PNG)

#### Attribute Counts
Each punk can have none or up to 7 attributes at a time.

From the given materials, we can potentially create more than 800,000 cryptopunk nfts.

---
Let's put everything aside and write a little Javascript command-line app to generate a bunch of these punks. On top of that, we will get a chance to solve the "cartesian product of multiple arrays" challenge in Javascript.

## Setting up
Please download all the trait-layers and punk images [here](https://github.com/victorquanlam/cryptopunk-nft-generator/tree/initial-stage).

Folder structure:
![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p948bfibauzlczp3qnr5.png)

We will be using [node-canvas ](https://github.com/Automattic/node-canvas) package to draw image in this project. Please make sure that you follow the [installation instruction](https://github.com/Automattic/node-canvas/wiki/Installation:-Windows) if you run into problems. More helps can be found [here](https://github.com/Automattic/node-canvas/issues/1511).

    npm install canvas

## Add imports and config variables
```javascript
const  fs = require("fs");

const { createCanvas, loadImage } = require("canvas");

const  console = require("console");

const  imageFormat = {

	width: 24,

	height: 24

};

// initialize canvas and context in 2d
const  canvas = createCanvas(imageFormat.width, imageFormat.height);

const  ctx = canvas.getContext("2d");

// some folder directories that we will use throughout the script
const  dir = {

	traitTypes  : `./layers/trait_types`,

	outputs: `./outputs`,

	background: `./layers/background`,

}

// we will update this total punks in the following steps.
let  totalOutputs = 0;

// set the order of layers that you want to print first
const  priorities = ['punks','top','beard'];
```
## Refresh outputs function 
- Create a function to remove the outputs data for us. Then it recreates the outputs folder along with new metadata and punk folders inside.
```javascript
const  recreateOutputsDir = () => {

	if (fs.existsSync(dir.outputs)) {

		fs.rmdirSync(dir.outputs, { recursive: true });

	}

	fs.mkdirSync(dir.outputs);

	fs.mkdirSync(`${dir.outputs}/metadata`);

	fs.mkdirSync(`${dir.outputs}/punks`);

};
```
## Calculate all the possible outcomes

In this step, we will figure out how to generate combinations from multiple arrays of trait layers. Now let's get down to business and have some fun. Don't copy and paste the code yet. 

There're many ways to implement this so-called simple function. 

- First is using the Reduce and FlatMap functions which were introduced in ECMAScript 2019. This is the shortest option and yet easiest to understand.
```javascript
const cartesian = (...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));
```
- Another common option is to use Recursion function

```javascript
const cartesian = (arr) => {
  if (arr.length == 1) {
    return arr[0];
  } else {
    var result = [];
    var allCasesOfRest = cartesian (arr.slice(1)); // recur with the rest of array
    for (var i = 0; i < allCasesOfRest.length; i++) {
      for (var j = 0; j < arr[0].length; j++) {
      	var childArray = [].concat(arr[0][j], allCasesOfRest[i])
        result.push(childArray);
      }
    }
    return result;
  }
}
```
Most of the options require to have an absurd amount of recursion, or heavily nested loops or to store the array of permutations in memory. It will get really messy when we run them agains hundreds of different trait layers. These will use up all of your device's memory and eventually crash your PC/laptop.  I got my PC fried multiple times. So don't be me. 

-  Instead of using recursion function or nested loops, we can create a function to calculate the total possible outcomes which is the product of all array's length. 

```javascript
var permsCount = arraysToCombine[0].length;
for(var i = 1; i < arraysToCombine.length; i++) {
    permsCount *= arraysToCombine[i].length;
}
```

- Next, we will set the divisors value to solve the array size differ

```javascript
for (var i = arraysToCombine.length - 1; i >= 0; i--) {
      divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;
   }
```

Add another function to return a unique permutation between index '0' and 'numPerms - 1' by calculating the indices it needs to retrieve its characters from, based on 'n'

```javascript
const getPermutation = (n, arraysToCombine) => {

	var  result = [],

	curArray;

	for (var  i = 0; i < arraysToCombine.length; i++) {
	
		curArray = arraysToCombine[i];

		result.push(curArray[Math.floor(n / divisors[i]) % curArray.length]);

	}

	return result;
}
```
Next we will call getPermutation (n) function using for loop

```javascript
    for(var i = 0; i < numPerms; i++) {
        combinations.push(getPermutation(i, arraysToCombine));
    }
```

The complete script that we need.
```javascript 
const  allPossibleCases = (arraysToCombine) => {

	const  divisors = [];

	let  permsCount = 1;

	for (let  i = arraysToCombine.length - 1; i >= 0; i--) {

		divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;

		permsCount *= (arraysToCombine[i].length || 1);

	}

	totalOutputs = permsCount;

	const  getCombination = (n, arrays, divisors) =>  arrays.reduce((acc, arr, i) => {

		acc.push(arr[Math.floor(n / divisors[i]) % arr.length]);

		return  acc;

	}, []);

	  

	const  combinations = [];

	for (let  i = 0; i < permsCount; i++) {

		combinations.push(getCombination(i, arraysToCombine, divisors));

	}

	return  combinations;

};
```
According to this quick performance test, the last version completely outperforms the others. Looks promising to me!

![performance test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dggzk2wckgy99tcsampy.png)

## Create draw image function
```javascript
const  drawImage= async (traitTypes, background, index) => {

	// draw background

	const  backgroundIm = await  loadImage(`${dir.background}/${background}`);

	ctx.drawImage(backgroundIm,0,0,imageFormat.width,imageFormat.height);

	  

	//'N/A': means that this punk doesn't have this trait type

	const  drawableTraits = traitTypes.filter(x=>  x.value !== 'N/A')

	// draw all the trait layers for this one punk

	for (let  index = 0; index < drawableTraits.length; index++) {

		const  val = drawableTraits[index];

		const  image = await  loadImage(`${dir.traitTypes}/${val.trait_type}/${val.value}`);

		ctx.drawImage(image,0,0,imageFormat.width,imageFormat.height);

	}

	console.log(`Progress: ${index}/ ${totalOutputs}`)

	// save metadata
	fs.writeFileSync(

		`${dir.outputs}/metadata/${index}.json`,

		    JSON.stringify({

		    name: `punk ${index}`,

		    attributes: drawableTraits

		}),
		function(err){
		    if(err) throw  err;
		})

		// save image as png file

		fs.writeFileSync(
		    `${dir.outputs}/punks/${index}.png`,
		    canvas.toBuffer("image/png")
		);

}
```

## Create main function 
```javascript
const  main = async () => {

const  traitTypesDir = dir.traitTypes;

// register all the traits 

const  types = fs.readdirSync(traitTypesDir);

// set all prioritised layers which will be drawn first. for eg: punk type, hair and then hat. You can set these values in the priorities array in line 21
const  traitTypes = priorities.concat(types.filter(x=> !priorities.includes(x)))

								.map(traitType  => ( 
									fs.readdirSync(`${traitTypesDir}/${traitType}/`)

								.map(value=> { 
									return {trait_type: traitType, value: value}

									}).concat({trait_type: traitType, value: 'N/A'})
								));
  
// register all the backgrounds
const  backgrounds = fs.readdirSync(dir.background);
  
// trait type avail for each punk

const  combinations = allPossibleCases(traitTypes)

	for (var  n = 0; n < combinations.length; n++) {

		const  randomBackground = backgrounds[Math.floor(Math.random() * backgrounds.length)]

		await  drawImage(combinations[n] , randomBackground, n);
	}
};
```

## Call outputs directory register and main function

```javascript 
(() => {

	recreateOutputsDir();

	main();

})();
```
## Run index.js
Open cmd/powershell and run

    node index.js
Or

    npm build

Ta-da. Let's the app run and generate all the nfts for us. 

## Resources
1. Source code: [victorquanlam/cryptopunk-nft-generator)](https://github.com/victorquanlam/cryptopunk-nft-generator)
2. Stackoverflow: [Cartesian product of array values](https://stackoverflow.com/a/4331713)

Please drop a like if you like this post.