This content originally appeared on DEV Community and was authored by Victor Quan Lam
CryptoPunks is one of the most popular NFT projects out there. And now, they are sold for millions of dollars. Yeah I know! Shocking! There are 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
- Alien
- Ape
- Zombie
- Female
- Male
Attributes
There are approximately 89 attributes avail for each punk type.
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
Download all the layers and punks image from this res.
We will be using node-canvas package to draw image in this project. Please make sure that you the installation instruction if you run into problems. More helps can be found here.
npm install canvas
Add imports and config variables
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 output folder function
- A function helps you to remove the outputs and its data. Then it recreate the outputs folder along with new metadata and punk folders inside.
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.
- Reduce and FlatMap functions which were introduced in ECMAScript 2019. This is the shortest option and yet easiest to understand.
const cartesian = (...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));
- Another common option is to use Recursion function
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 absorb amount of recursion, or heavily nested loops or to store the array of permutations in memory. It will get really messy working with 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.
var permsCount = arraysToCombine[0].length;
for(var i = 1; i < arraysToCombine.length; i++) {
permsCount rms *= arraysToCombine[i].length;
}
- Next, we will set the divisors value to solve the array size differ
for (var i = arraysToCombine.length - 1; i >= 0; i--) {
divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;
}
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
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 with permsCount as
for(var i = 0; i < numPerms; i++) {
combinations.push(getPermutation(i, arraysToCombine));
}
The complete script that we need.
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;
totalOutputs123-_
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 outperform the others. Looks very promising to me.
Create draw image function
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
const main = async () => {
const traitTypesDir = dir.traitTypes;
// register all the traits
const types = fs.readdirSync(traitTypesDir);
// set all priotized layers to be drawn first. for eg: punk type, top... 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
//main
(() => {
recreateOutputsDir();
main();
})();
Run index.js
Open cmd/powershell and run
node index.js
Or
npm build
Resources
- Source code: victorquanlam/cryptopunk-nft-generator)
- Stackoverflow: Cartesian product of array values
That's all. I hope that you enjoy the blog.
This content originally appeared on DEV Community and was authored by Victor Quan Lam

Victor Quan Lam | Sciencx (2021-09-05T10:48:10+00:00) Generate 879,120 cryptopunk NFTs with Javascript Nodejs command-line app (step by step). Retrieved from https://www.scien.cx/2021/09/05/generate-879120-cryptopunk-nfts-with-javascript-nodejs-command-line-app-step-by-step/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.