Publishing a large NFT collection on OpenSea:Part 2

smile0307

Smile

Posted on April 8, 2022

Publishing a large NFT collection on OpenSea:Part 2

Welcome to Part 2 of “Publishing a large NFT collection on OpenSea” — the story of how we published the Krypto Pandaz collection. Part 1 covered selecting the blockchain and defining your smart contract. In Part 2 we will focus on generating images and creating a metadata server.

Create images and a metadata server

Most large NFT collections consist of similar images, each having a unique set of variations. The most popular collections have anywhere from 1,000 to 10,000 NFTs which would be almost impossible to create by hand. We assume that many of these large collections are programmatically generated; which is what we decided to do.

Set up an image generator project

We decided to use NodeJS to generate the pieces for our collection. We started by installing a few libraries to our project:

yarn add color jimp sharp path fs-extra lodash async
Enter fullscreen mode Exit fullscreen mode

We used the color library for parsing and manipulating hex color codes, jimp for manipulating images, sharp for compositing our image layers, path for working with file and directory paths, fs-extra for reading and writing files, lodash for utility methods, and async for managing asynchronous operations.

Create a configuration for image assets

Before you enter this step, you will need to have some image assets that will eventually be layered together to create a single NFT.
For this example, I’ll show you how we created the configuration required to generate a panda with various color schemes and clothing.

Let’s start by setting up a JSON object that contains a list of sections that will come together to create an individual NFT.

import * as path from 'path';

const pandaAssetsDir = path.join(__dirname, '../../assets/panda');
const config = {
    sections: [
        {
            name: 'Panda',
            chanceToBeEmpty: 0,
            pieces: [{
                    name: 'Panda',
                    baseImage: path.join(pandaAssetsDir, 'panda.png')
            }]
        },
        {
            name: 'Body',
            chanceToBeEmpty: 0.2,
            pieces: [
                {
                    name: 'Lab Coat',
                    baseImage: path.join(pandaAssetsDir, 'lab-coat.png')
                },
                {
                    name: 'Scrubs',
                    baseImage: path.join(pandaAssetsDir, 'scrubs.png')
                }
            ]
        }
    ]
};
Enter fullscreen mode Exit fullscreen mode

You can see here that we added two sections; one for the panda and another for the clothing it’ll have on its body. This alone does not give us many variations so let’s add some configuration that’ll help us generate new themes for the panda.
We can add a new property called scheme to our pieces, we want to generate themes for. In this object, we’ll define the colors in the base image that we want to modify as well as a list of themes containing colors that will replace the corresponding base colors.
Tip: The color library is very helpful for creating different colors from a single color code.

import * as path from 'path';
import Color from 'color';

const pandaAssetsDir = path.join(__dirname, '../../assets/panda');

const orange = Color('#d35400');
const pink = Color('#e84393');
const blue = Color('#0984e3');
const green = Color('#00b894');
const red = Color('#d63031');
const gray = Color('#dfe6e9');

const config = {
    sections: [
        {
            name: 'Panda',
            chanceToBeEmpty: 0,
            pieces: [{
                name: 'Panda',
                keepBaseImage: true,
                baseImage: path.join(pandaAssetsDir, 'panda.png'),
                scheme: {
                    baseColors: ['#75388B', '#453451', '#2F243B'],
                    intensity: 0.6,
                    themes: [
                        { name: 'Pink', colors: [pink, pink.darken(0.15), pink.darken(0.3)] },
                        { name: 'Blue', colors: [blue, blue.darken(0.15), blue.darken(0.3)] },
                        { name: 'Green', colors: [green, green.darken(0.15), green.darken(0.3)] },
                        { name: 'Red', colors: [red, red.darken(0.15), red.darken(0.3)] },
                        { name: 'Gray', colors: [gray, gray.darken(0.15), gray.darken(0.3)] },
                    ],
                }
            }]
        }
    ]
};
Enter fullscreen mode Exit fullscreen mode

Now that we have some configuration to work with, we can move forward with writing the code that will generate our images!

Theme individual assets
The last thing we added to our configuration was the scheme property to create different color variations of our panda. I wrote a function that relies on jimp to apply a color to an image:

function getDistanceBetweenColors(color1: number[], color2: number[]) {
    const r = Math.abs(color1[0] - color2[0]);
    const g = Math.abs(color1[1] - color2[1]);
    const b = Math.abs(color1[2] - color2[2]);

    return r + g + b;
}

function isColorIsNearTo(color: number[], colorToCompare: number[], distance: number) {
    return getDistanceBetweenColors(color, colorToCompare) < distance;
}

function applyColorToImage(image: jimp, color: Color, intensity: number, shouldColorBeChanged: any = () => true) {
    image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (x, y, idx) {
        const currentRed = this.bitmap.data[idx + 0];
        const currentGreen = this.bitmap.data[idx + 1];
        const currentBlue = this.bitmap.data[idx + 2];

        if (shouldColorBeChanged([currentRed, currentGreen, currentBlue])) {
            const redDiff = currentRed - color.red();
            const greenDiff = currentGreen - color.green();
            const blueDiff = currentBlue - color.blue();

            const red = redDiff > 0 ? currentRed - redDiff * intensity : currentRed + Math.abs(redDiff * intensity);
            const green = greenDiff > 0 ? currentGreen - greenDiff * intensity : currentGreen + Math.abs(greenDiff * intensity);
            const blue = blueDiff > 0 ? currentBlue - blueDiff * intensity : currentBlue + Math.abs(blueDiff * intensity);

            this.bitmap.data[idx + 0] = red;
            this.bitmap.data[idx + 1] = green;
            this.bitmap.data[idx + 2] = blue;
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Using the above functions we are now able to go through every theme and apply our new colors onto the base colors of the image.

const parsedImagePath = path.parse(baseImage);
fs.mkdirpSync(path.join(parsedImagePath.dir, '/variants/'));

async.map(scheme.themes, (theme, callback) => {
    const newImagePath = path.join(parsedImagePath.dir, '/variants/', `${parsedImagePath.name}--${theme.name}${parsedImagePath.ext}`);

    jimp.read(baseImage, (err, image) => {
        if (err) throw err;

        theme.colors.forEach((color, i) => {
            applyColorToImage(image, color, opts.scheme.intensity, (c) => isColorIsNearTo(c, scheme.baseColors[i].rgb().array(), 1));
        });

        image.write(newImagePath);
    });
}, (err, results) => {
    if (err) return reject(err);
    resolve();
})
Enter fullscreen mode Exit fullscreen mode

With this code, we are now able to generate variations for each of our assets by simply adding some more configuration!

Generate images
Now that we have different sections in our configuration we need to write some code to put the different pieces together. This is where the sharp library comes in handy.

const firstImage = path.join(__dirname, '../assets/panda.png');
const comboImages = [
    path.join(__dirname, '../assets/variants/scrubs--red.png'),
    path.join(__dirname, '../assets/nurse-hat.png')
];

await sharp(firstImage)
    .composite(comboImages.map(image => ({
        input: image
    })))
    .toFile(path.join(__dirname, 'panda-1.png'));
Enter fullscreen mode Exit fullscreen mode

This small bit of code takes our base panda image and adds red scrubs and a nurse hat.

Generate metadata

Once you have the code needed to generate your NFT images, you’ll want to enhance it so that it also generates metadata for each NFT. OpenSea has documentation for its metadata standards here.
We created one large JSON array which we put into a file that contains metadata for each NFT.
Each entry has the following properties:
description : A description of the NFT, can be the same for each one but we decided to generate a description based on the properties of the Panda.
image : The name of the image file for the NFT.
name : The name for the NFT.
attributes : A list of attributes for the NFT.
This is the first metadata object we generated for our collection:

{
  "description": "Green Panda with Lab Coat, Nurse Hat on a Pink background",
  "image": "panda-1.png",
  "name": "Panda #1",
  "attributes": [
    {
      "trait_type": "Panda",
      "value": "Green Panda"
    },
    {
      "trait_type": "Body",
      "value": "Lab Coat"
    },
    {
      "trait_type": "Head",
      "value": "Nurse Hat"
    },
    {
      "trait_type": "Background",
      "value": "Pink"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is what the above NFT will look like on OpenSea:

Upload images to a publicly accessible location

There are many options you can use for storing your NFT images. You can use IPFS, a cloud storage provider like Google Cloud Storage, or some other solution that provides public access to your images.
We went with Google Cloud Storage as it’s very easy to use. After we used our script to generate all our pandas, we simply ran a command using gsutil to copy them all to a Cloud Storage bucket.
gsutil -m cp *.png gs://nft-pandas

Create a metadata server

In order for people and services to know about the assets that are available in our collection, we need to create a server to serve the JSON metadata we created above. We decided to use Google App Engine.
First, we installed ExpressJS: npm install express --save
Then we created a NodeJS script to start an ExpressJS server that serves a JSON object with some details about our collection and an endpoint for each individual NFT:

const express = require('express')
const db = require('./src/panda-manifest.json')

const PORT = process.env.PORT || 5000

const app = express().set('port', PORT)

app.get('/', function(req, res) {
  res.send({
    "name": "Pandas",
    "description": "Pandas are adorable pandas. Adopt one today to make us reach. Jump on the NFT bandwagon and get your own Panda!",
    "image": "https://storage.googleapis.com/nft-pandas/panda-0.png",
    "seller_fee_basis_points": 100,
    "fee_recipient": "0xA97F337c39cccE66adfeCB2BF99C1DdC54C2D721"
  });
})

app.get('/api/token/:token_id', function(req, res) {
  const tokenId = parseInt(req.params.token_id);
  const panda = db[tokenId];
  res.send({
    description: panda.description,
    image: 'https://storage.googleapis.com/nft-pandas/' + panda.image,
    name: panda.name,
    attributes: panda.attributes,
  })
})

app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
})
Enter fullscreen mode Exit fullscreen mode

Now that we have the script for our metadata server, we just need to create an app.yamlthat tells App Engine what environment to run our server with and a .gcloudignore that tells it what files to ignore when deploying:

The final step is to deploy our app to Google App Engine:

gcloud config set project nfts-326700
gcloud app deploy --quiet
Enter fullscreen mode Exit fullscreen mode

Once the deployment completes your metadata server will be accessible via your App Engine project!
This is our metadata server: https://nfts-326700.uc.r.appspot.com/
Now that you have your metadata server exposed on the Internet, go
back to your contract and insert the metadata server URL in lines 22, 26 of the contract that we covered in Part 1.

Summary

In Part 2 we learned how to generate a large collection of different images. We also covered how to create a metadata server that describes the properties of images in a format that is understood by OpenSea.
In Part 3, we will finally publish our contract, mint NFT’s and list the collection on OpenSea.

Back to Part1

Go to Part3

💖 💪 🙅 🚩
smile0307
Smile

Posted on April 8, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related