DEV Community

Cover image for NFT images generator with Javascript Nodejs (800,000 cryptopunks)
Victor Quan Lam
Victor Quan Lam

Posted on • Updated on

NFT images generator with Javascript Nodejs (800,000 cryptopunks)

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

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.

Folder structure:
Folder structure

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
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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`);

};
Enter fullscreen mode Exit fullscreen mode

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())));
Enter fullscreen mode Exit fullscreen mode
  • 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
   }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Next we will call getPermutation (n) function using for loop

    for(var i = 0; i < numPerms; i++) {
        combinations.push(getPermutation(i, arraysToCombine));
    }
Enter fullscreen mode Exit fullscreen mode

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;

};
Enter fullscreen mode Exit fullscreen mode

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

performance test

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")
        );

}
Enter fullscreen mode Exit fullscreen mode

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);
    }
};
Enter fullscreen mode Exit fullscreen mode

Call outputs directory register and main function

(() => {

    recreateOutputsDir();

    main();

})();
Enter fullscreen mode Exit fullscreen mode

Run index.js

Open cmd/powershell and run

node index.js
Enter fullscreen mode Exit fullscreen mode

Or

npm build
Enter fullscreen mode Exit fullscreen mode

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

Resources

  1. Source code: victorquanlam/cryptopunk-nft-generator)
  2. Stackoverflow: Cartesian product of array values

Please drop a like if you like this post.

Top comments (21)

Collapse
 
akunatex profile image
Miguel

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

Collapse
 
jayaych profile image
Jeremy H

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!

Collapse
 
victorquanlam profile image
Victor Quan Lam

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...

Collapse
 
jayaych profile image
Jeremy H

Ah that makes sense! Thank you this is extremely helpful!

Collapse
 
flpvdmt profile image
Dmitrii Filippov

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)

Collapse
 
mrksta profile image
Marko Matić

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 ?

Collapse
 
zhurukvova profile image
Vova Zhuruk

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

Collapse
 
nagoadvisory profile image
Nago

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?

Collapse
 
victorquanlam profile image
Victor Quan Lam
Collapse
 
zehpierce profile image
Kyle Pierce

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

Collapse
 
publr profile image
Publr

How do you use button ? instead of command for this is working

Collapse
 
bibinguyen238 profile image
Bibi Nguyen

congratulate!!!! kkk

Collapse
 
victorquanlam profile image
Victor Quan Lam

Thanks

Collapse
 
wesrajoko profile image
Wesra Joko

Hello mate, how to run this project with nextjs and make a like interface generator with form upload and preview, thanks.

Collapse
 
victorquanlam profile image
Victor Quan Lam

I reckon you can build a UI for it.

Collapse
 
sizkadinlar profile image
sizkadinlar

Can we adapt this to python?

Collapse
 
victorquanlam profile image
Victor Quan Lam

yeah of course

Collapse
 
tikam02 profile image
Tikam Singh Alma

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.

Collapse
 
victorquanlam profile image
Victor Quan Lam

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.