Skip to content

Commit 9f6a344

Browse files
authored
Merge pull request #13 from caiaga/main
#12 allow defining specific percentages for rarity/layer combinations...
2 parents 2d74294 + 54e9efc commit 9f6a344

File tree

3 files changed

+176
-91
lines changed

3 files changed

+176
-91
lines changed

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,34 @@ Create generative art by using the canvas api and node js, feel free to contribu
99
# How to use
1010
## Run the code
1111
1. Run `node index.js`
12-
2. Open the `./output` folder to find your generated images to use as NFTs
12+
2. Open the `./output` folder to find your generated images to use as NFTs, as well as the metadata to use for NFT marketplaces.
1313

1414
## Adjust the provided configuration and resources
1515
### Configuration file
1616
The file `./input/config.js` contains the following properties that can be adjusted to your preference in order to change the behavior of the NFT generation procedure:
1717
- width: - of your image in pixels. Default: `1000px`
1818
- height: - of your image in pixels. Default: `1000px`
1919
- dir: - where image parts are stored. Default: `./input`
20-
- description: - of your generated NFT
20+
- description: - of your generated NFT. Default: `This is an NFT made by the coolest generative code.`
2121
- baseImageUri: - URL base to access your NFTs from. This will be used by platforms to find your image resource. This expects the image to be accessible by it's id like `${baseImageUri}/${id}`.
22-
- startEditionFrom: - number (int) to start naming NFTs from.
23-
- editionSize: - number (int) to end edition at.
24-
- rarityWeights: - allows to provide rarity categories and how many of each type to include in an edition.
22+
- startEditionFrom: - number (int) to start naming NFTs from. Default: `1`
23+
- editionSize: - number (int) to end edition at. Default: `10`
24+
- editionDnaPrefix: - value (number or string) that indicates which dna from an edition is used there. I.e. dna `0` from to independent batches in the same edition may differ, and can be differentiated using this. Default: `0`
25+
- rarityWeights: - allows to provide rarity categories and how many of each type to include in an edition. Default: `1 super_rare, 4 rare, 5 original`
2526
- layers: list of layers that should be used to render the image. See next section for detail.
2627

2728
### Image layers
28-
The image layers are different parts that make up a full image by overlaying on top of each other. E.g. in this example we start with the eyeball and layer features like the eye lids or iris on top to create the completed and unique eye, which we can then use as part of our NFT collection.
29+
The image layers are different parts that make up a full image by overlaying on top of each other. E.g. in the example input content of this repository we start with the eyeball and layer features like the eye lids or iris on top to create the completed and unique eye, which we can then use as part of our NFT collection.
2930
To ensure uniqueness, we want to add various features and multiple options for each of them in order to allow enough permutations for the amount of unique images we require.
3031

3132
To start, copy the layers/features and their images in a flat hierarchy at a directory of your choice (by default we expect them in `./input/`). The features should contain options for each rarity that is provided via the config file.
3233

33-
After adding the layers, adjust them accordingly in the `config.js` by providing the directory path, positioning and sizes.
34+
After adding the `layers`, adjust them accordingly in the `config.js` by providing the directory path, positioning and sizes.
35+
Use the existing `addLayers` calls as guidance for how to add layers. This can either only use the name of the layer and will use default positioning (x=0, y=0) and sizes (width=configured width, height=configure height), or positioning and sizes can be provided for more flexibility.
36+
37+
### Allowing different rarities for certain rarity/layer combinations
38+
It is possible to provide a percentage at which e.g. a rare item would contain a rare vs. common part in a given layer. This can be done via the `addRarityPercentForLayer` that can be found in the `config.js` as well.
39+
This allows for more fine grained control over how much randomness there should be during the generation process, and allows a combination of common and rare parts.
3440

3541
# Development suggestions
3642
- Preferably use VSCode with the prettifier plugin for a consistent coding style (or equivalent js formatting rules)

index.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,14 @@ const drawElement = (_element) => {
9292
// drawing on a canvas
9393
const constructLayerToDna = (_dna = [], _layers = [], _rarity) => {
9494
let mappedDnaToLayers = _layers.map((layer, index) => {
95-
let selectedElement = layer.elements[_rarity][_dna[index]];
95+
let selectedElement = layer.elements.find(element => element.id === _dna[index]);
9696
return {
9797
location: layer.location,
9898
position: layer.position,
9999
size: layer.size,
100100
selectedElement: selectedElement,
101101
};
102102
});
103-
104103
return mappedDnaToLayers;
105104
};
106105

@@ -111,13 +110,35 @@ const isDnaUnique = (_DnaList = [], _dna = []) => {
111110
return foundDna == undefined ? true : false;
112111
};
113112

113+
const getRandomRarity = (_rarityOptions) => {
114+
let randomPercent = Math.random() * 100;
115+
let percentCount = 0;
116+
117+
for (let i = 0; i <= _rarityOptions.length; i++) {
118+
percentCount += _rarityOptions[i].percent;
119+
if (percentCount >= randomPercent) {
120+
console.log(`use random rarity ${_rarityOptions[i].id}`)
121+
return _rarityOptions[i].id;
122+
}
123+
}
124+
return _rarityOptions[0].id;
125+
}
126+
114127
// create a dna based on the available layers for the given rarity
115128
// use a random part for each layer
116129
const createDna = (_layers, _rarity) => {
117130
let randNum = [];
131+
let _rarityWeight = rarityWeights.find(rw => rw.value === _rarity);
118132
_layers.forEach((layer) => {
119-
let num = Math.floor(Math.random() * layer.elements[_rarity].length);
120-
randNum.push(num);
133+
let num = Math.floor(Math.random() * layer.elementIdsForRarity[_rarity].length);
134+
if (_rarityWeight && _rarityWeight.layerPercent[layer.id]) {
135+
// if there is a layerPercent defined, we want to identify which dna to actually use here (instead of only picking from the same rarity)
136+
let _rarityForLayer = getRandomRarity(_rarityWeight.layerPercent[layer.id]);
137+
num = Math.floor(Math.random() * layer.elementIdsForRarity[_rarityForLayer].length);
138+
randNum.push(layer.elementIdsForRarity[_rarityForLayer][num]);
139+
} else {
140+
randNum.push(layer.elementIdsForRarity[_rarity][num]);
141+
}
121142
});
122143
return randNum;
123144
};

input/config.js

Lines changed: 138 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,161 @@
1+
/**************************************************************
2+
* UTILITY FUNCTIONS
3+
* - scroll to BEGIN CONFIG to provide the config values
4+
*************************************************************/
15
const fs = require("fs");
2-
const width = 1000;
3-
const height = 1000;
46
const dir = __dirname;
5-
const description = "This is an NFT made by the coolest generative code.";
6-
const baseImageUri = "https://hashlips/nft";
7-
const startEditionFrom = 1;
8-
const editionSize = 10;
9-
const rarityWeights = [
10-
{
11-
value: "super_rare",
12-
from: 1,
13-
to: 1,
14-
},
15-
{
16-
value: "rare",
17-
from: 2,
18-
to: 5,
19-
},
20-
{
21-
value: "original",
22-
from: 5,
23-
to: editionSize,
24-
},
25-
];
267

8+
// adds a rarity to the configuration. This is expected to correspond with a directory containing the rarity for each defined layer
9+
// @param _id - id of the rarity
10+
// @param _from - number in the edition to start this rarity from
11+
// @param _to - number in the edition to generate this rarity to
12+
// @return a rarity object used to dynamically generate the NFTs
13+
const addRarity = (_id, _from, _to) => {
14+
const _rarityWeight = {
15+
value: _id,
16+
from: _from,
17+
to: _to,
18+
layerPercent: {}
19+
};
20+
return _rarityWeight;
21+
};
22+
23+
// get the name without last 4 characters -> slice .png from the name
2724
const cleanName = (_str) => {
2825
let name = _str.slice(0, -4);
2926
return name;
3027
};
3128

32-
const getElements = (path) => {
29+
// reads the filenames of a given folder and returns it with its name and path
30+
const getElements = (_path, _elementCount) => {
3331
return fs
34-
.readdirSync(path)
32+
.readdirSync(_path)
3533
.filter((item) => !/(^|\/)\.[^\/\.]/g.test(item))
3634
.map((i) => {
3735
return {
36+
id: _elementCount,
3837
name: cleanName(i),
39-
path: `${path}/${i}`,
38+
path: `${_path}/${i}`
4039
};
4140
});
4241
};
4342

43+
// adds a layer to the configuration. The layer will hold information on all the defined parts and
44+
// where they should be rendered in the image
45+
// @param _id - id of the layer
46+
// @param _position - on which x/y value to render this part
47+
// @param _size - of the image
48+
// @return a layer object used to dynamically generate the NFTs
49+
const addLayer = (_id, _position, _size) => {
50+
if (!_id) {
51+
console.log('error adding layer, parameters id required');
52+
return null;
53+
}
54+
if (!_position) {
55+
_position = { x: 0, y: 0 };
56+
}
57+
if (!_size) {
58+
_size = { width: width, height: height }
59+
}
60+
// add two different dimension for elements:
61+
// - all elements with their path information
62+
// - only the ids mapped to their rarity
63+
let elements = [];
64+
let elementCount = 0;
65+
let elementIdsForRarity = {};
66+
rarityWeights.forEach((rarityWeight) => {
67+
let elementsForRarity = getElements(`${dir}/${_id}/${rarityWeight.value}`);
68+
69+
elementIdsForRarity[rarityWeight.value] = [];
70+
elementsForRarity.forEach((_elementForRarity) => {
71+
_elementForRarity.id = `${editionDnaPrefix}${elementCount}`;
72+
elements.push(_elementForRarity);
73+
elementIdsForRarity[rarityWeight.value].push(_elementForRarity.id);
74+
elementCount++;
75+
})
76+
elements[rarityWeight.value] = elementsForRarity;
77+
});
78+
79+
let elementsForLayer = {
80+
id: _id,
81+
position: _position,
82+
size: _size,
83+
elements,
84+
elementIdsForRarity
85+
};
86+
return elementsForLayer;
87+
};
88+
89+
// adds layer-specific percentages to use one vs another rarity
90+
// @param _rarityId - the id of the rarity to specifiy
91+
// @param _layerId - the id of the layer to specifiy
92+
// @param _percentages - an object defining the rarities and the percentage with which a given rarity for this layer should be used
93+
const addRarityPercentForLayer = (_rarityId, _layerId, _percentages) => {
94+
let _rarityFound = false;
95+
rarityWeights.forEach((_rarityWeight) => {
96+
if (_rarityWeight.value === _rarityId) {
97+
let _percentArray = [];
98+
for (let percentType in _percentages) {
99+
_percentArray.push({
100+
id: percentType,
101+
percent: _percentages[percentType]
102+
})
103+
}
104+
_rarityWeight.layerPercent[_layerId] = _percentArray;
105+
_rarityFound = true;
106+
}
107+
});
108+
if (!_rarityFound) {
109+
console.log(`rarity ${_rarityId} not found, failed to add percentage information`);
110+
}
111+
}
112+
113+
/**************************************************************
114+
* BEGIN CONFIG
115+
*************************************************************/
116+
117+
// image width in pixels
118+
const width = 1000;
119+
// image height in pixels
120+
const height = 1000;
121+
// description for NFT in metadata file
122+
const description = "This is an NFT made by the coolest generative code.";
123+
// base url to use in metadata file
124+
// the id of the nft will be added to this url, in the example e.g. https://hashlips/nft/1 for NFT with id 1
125+
const baseImageUri = "https://hashlips/nft";
126+
// id for edition to start from
127+
const startEditionFrom = 1;
128+
// amount of NFTs to generate in edition
129+
const editionSize = 10;
130+
// prefix to add to edition dna ids (to distinguish dna counts from different generation processes for the same collection)
131+
const editionDnaPrefix = 0
132+
133+
// create required weights
134+
// for each weight, call 'addRarity' with the id and from which to which element this rarity should be applied
135+
let rarityWeights = [
136+
addRarity('super_rare', 1, 1),
137+
addRarity('rare', 2, 5),
138+
addRarity('original', 5, 10)
139+
];
140+
141+
// create required layers
142+
// for each layer, call 'addLayer' with the id and optionally the positioning and size
143+
// the id would be the name of the folder in your input directory, e.g. 'ball' for ./input/ball
44144
const layers = [
45-
{
46-
elements: {
47-
original: getElements(`${dir}/ball/original`),
48-
rare: getElements(`${dir}/ball/rare`),
49-
super_rare: getElements(`${dir}/ball/super_rare`),
50-
},
51-
position: { x: 0, y: 0 },
52-
size: { width: width, height: height },
53-
},
54-
{
55-
elements: {
56-
original: getElements(`${dir}/eye color/original`),
57-
rare: getElements(`${dir}/eye color/rare`),
58-
super_rare: getElements(`${dir}/eye color/super_rare`),
59-
},
60-
position: { x: 0, y: 0 },
61-
size: { width: width, height: height },
62-
},
63-
{
64-
elements: {
65-
original: getElements(`${dir}/iris/original`),
66-
rare: getElements(`${dir}/iris/rare`),
67-
super_rare: getElements(`${dir}/iris/super_rare`),
68-
},
69-
position: { x: 0, y: 0 },
70-
size: { width: width, height: height },
71-
},
72-
{
73-
elements: {
74-
original: getElements(`${dir}/shine/original`),
75-
rare: getElements(`${dir}/shine/rare`),
76-
super_rare: getElements(`${dir}/shine/super_rare`),
77-
},
78-
position: { x: 0, y: 0 },
79-
size: { width: width, height: height },
80-
},
81-
{
82-
elements: {
83-
original: getElements(`${dir}/bottom lid/original`),
84-
rare: getElements(`${dir}/bottom lid/rare`),
85-
super_rare: getElements(`${dir}/bottom lid/super_rare`),
86-
},
87-
position: { x: 0, y: 0 },
88-
size: { width: width, height: height },
89-
},
90-
{
91-
elements: {
92-
original: getElements(`${dir}/top lid/original`),
93-
rare: getElements(`${dir}/top lid/rare`),
94-
super_rare: getElements(`${dir}/top lid/super_rare`),
95-
},
96-
position: { x: 0, y: 0 },
97-
size: { width: width, height: height },
98-
},
145+
addLayer('ball', { x: 0, y: 0 }, { width: width, height: height }),
146+
addLayer('eye color'),
147+
addLayer('iris'),
148+
addLayer('shine'),
149+
addLayer('bottom lid'),
150+
addLayer('top lid')
99151
];
100152

153+
// provide any specific percentages that are required for a given layer and rarity level
154+
// all provided options are used based on their percentage values to decide which layer to select from
155+
addRarityPercentForLayer('super_rare', 'ball', { 'super_rare': 33, 'rare': 33, 'original': 33 });
156+
addRarityPercentForLayer('super_rare', 'eye color', { 'super_rare': 50, 'rare': 25, 'original': 25 });
157+
addRarityPercentForLayer('original', 'eye color', { 'super_rare': 50, 'rare': 25, 'original': 25 });
158+
101159
module.exports = {
102160
layers,
103161
width,

0 commit comments

Comments
 (0)