After a little research, I was able to break the initial task from the abstract “create an NFT collection” into smaller and more specific ones, namely:
- c generate 10,000 unique images
- c generate 10,000 metadata for each image
- upload 10,000 images along with metadata to decentralized storage
- create a smart contract for NFT tokens
- upload the created smart contract to the Ethereum mainnet
- create a website that will interact with our smart contract using web3, where users themselves will be able to change their ethers to our NFT tokens
It would seem trifles, but no, at each stage, unforeseen moments awaited me, which we will now talk about.
How to generate 10,000 unique images?
Why exactly 10,000? The answer is quite simple, most popular NFT projects offer collections of exactly 10,000 NFT tokens. Each creator is free to decide how many NFT tokens he wants to release, but we decided not to deviate from the canon and also made 10,000 tokens.
So how do you generate 10,000 unique images anyway? Of course, with the help of automatic layering on top of each other. After some thought, the artist and I came to the conclusion that for our project we need the following layers:
- background – 20 pcs
- character’s torso – 25 pcs
- head – 15 pcs
- emotions – 20 pcs
- clothes – 30 pcs
- shoes – 25 pcs
- accessories – 40 pcs
In total, we ended up with approximately 175 unique png layers, which is more than enough to get 10,000 unique characters. Now there is nothing left at all, namely to write a utility that will accept blanks in the form of layers at the input, and at the output, it will give ready-made characters.
I will write to Golang, so let’s go. First, we need to define 2 structures in the domain package, one for the layers and one for the canvas.
package domain
import (
"image"
"image/color"
)
// ImageLayer struct.
type ImageLayer struct {
Image image.Image
Priotiry int
XPos int
YPos int
}
//BgProperty is background property struct.
type BgProperty struct {
Width int
Length int
BgColor color.Color
}
Let’s take a closer look at both structures.
ImageLayer:
- Image – layer image
- Priority – layer priority, because layers must be applied in a certain order, first the background, then the torso, then the head, etc…
- XPos, YPos – position of the canvas layer
BgProperty:
- Width – canvas width
- Length – canvas length
So, when the basic structures are described, we can move on to writing a service that will actually combine our layers in a certain order.
The service code is quite simple, at the input the service takes a list of layers and canvas parameters, and at the output, it returns bytes of the generated image. I would like to note that Go has a fairly good library for working with images, and it is with it that we will work, on the actual code:
package combiner
import (
"bytes"
"image"
"image/draw"
"image/png"
"nft/internal/domain"
"sort"
)
type service struct {
}
func NewBasicImageCombiner() domain.ImageCombiner {
return &service{}
}
func (s *service) CombineLayers(layers []*domain.ImageLayer, bgProperty *domain.BgProperty) ([]byte, error) {
// Sort list by position.
layers = sortByPriotiry(layers)
// Create image's background.
bgImg := image.NewRGBA(image.Rect(0, 0, bgProperty.Width, bgProperty.Length))
// Set the background color.
draw.Draw(bgImg, bgImg.Bounds(), &image.Uniform{bgProperty.BgColor}, image.Point{}, draw.Src)
// Looping image layers, higher position -> upper layer.
for _, img := range layers {
// Set the image offset.
offset := image.Pt(img.XPos, img.YPos)
// Combine the image.
draw.Draw(bgImg, img.Image.Bounds().Add(offset), img.Image, image.Point{}, draw.Over)
}
// Encode image to buffer.
buff := new(bytes.Buffer)
if err := png.Encode(buff, bgImg); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func sortByPriotiry(list []*domain.ImageLayer) []*domain.ImageLayer {
sort.Slice(list, func(i, j int) bool {
return list[i].Priotiry < list[j].Priotiry
})
return list
}
Great, when the code for generating images is ready, we can move on to generating metadata.