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
- 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
Please download all the trait-layers and punk images here.
We will be using node-canvas package to draw image in this project. Please make sure that you follow 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 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.
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.
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 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.
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
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'
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
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;
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!
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 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
(() => {
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
- Source code: victorquanlam/cryptopunk-nft-generator)
- Stackoverflow: Cartesian product of array values
Please drop a like if you like this post.

















congratulate!!!! kkk