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.
Top comments (21)
Hi Victor, nice work you've been doing, i've used your code to generate some images, but from what i've read also from a post you wrote, the way to publish this amount of images (+100), would be to write a smart contract associated with the images and store them somewhere, then i could create the images on opensea for instance, but even doing so, i would need to create each new image item on opensea or the entire smart contract would be listed on opensea as a collection.
Best regards and keep doing the good work.
Miguel
Is there a way to use this so that one [blank] background is divided into 6 squares and each of those squares is filled by a random image from a large list (with rarity amounts)?
Sorry if that’s a dumb question: I’m brand new to coding so I barely know what I’m looking at. Any help is super appreciated!
This script won't support with rarity because it will generate every possible combination of the trait layers (aka attributes). You can add rarity of these attributes into the script. However, I have another blog post maybe quiet similar to your idea. It has rarity and different shapes which would be quiet easy to follow especially for beginner.
dev.to/victorquanlam/nft-images-ge...
Ah that makes sense! Thank you this is extremely helpful!
Hey!
This page no longer exists
Perhaps you have article on another resource or code example, so please be so kind as to share it)
For example ->
Aliens/Apes should only ever be assigned “hat” attributes — never “hair” attributes. Logical — as Aliens/Apes don’t grow human hair styles (but Zombies do, of course, as they were formerly humans).
How can we add exceptions? i.e if layer 2.1 is used don't add layer 4.2 ?
Hello Victor! How can I use your script when I have animations instead of images? The animation represented by sequence with PNGs. Like body_gold_01, body_gold_02...body_gold_49, then body_black_01...body_black_49. Where numbers are frames of animation. So I have the same situation like cryptopunks but with difference in animated parts.
I am noob at coding, so if you would help me I will be very glad!
Also, contact to me on Twitter if possible twitter.com/zhurukvova
Hi Victor, thanks for sharing. This is so cool. I'm helping my son to try to make an NFT and we are stuck at putting in arguments after npm build hehe.... idk why we are struggling..
Can you share a sample example of code one would enter after npm build __________ to create a character. We are messing up on key value pairs, I think?
Here you go github.com/victorquanlam/cryptopun...
Hey, we just launched an NFT generator much like this. However, we are using NodeJS on the frontend, and Golang on the backend. Would you be into reviewing it by any chance?
generate-nft.online
How do you use button ? instead of command for this is working
congratulate!!!! kkk
Thanks
Hello mate, how to run this project with nextjs and make a like interface generator with form upload and preview, thanks.
I reckon you can build a UI for it.
Can we adapt this to python?
yeah of course
where can we get other attributes to create new NFT collection, obviously we can't sell this one but we can create new one and sell it right.
yeap that's the whole point actually! You can easily add remove trait layers and attributes with this code. It will generate the images for you.