-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
196 lines (184 loc) · 5.86 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
const crypto = require("crypto");
const { createCanvas, loadImage } = require("canvas");
const { rmdirSync, mkdirSync, writeFileSync } = require("fs");
const {
DEFAULT_IMAGES_PATH,
DEFAULT_METADATA_PATH,
IMAGES_BASE_URI,
IMAGES_HEIGHT,
IMAGES_WIDTH,
TOKEN_NAME_PREFIX,
TOKEN_DESCRIPTION,
TOTAL_TOKENS,
ORDERED_TRAITS_LIST: traitsList,
} = require("./config");
const canvas = createCanvas(IMAGES_WIDTH, IMAGES_HEIGHT);
const ctx = canvas.getContext("2d", { alpha: false });
const uniqueCombinationsHashes = new Set();
const createUniqueTokens = () => {
console.log("--> Creating unique tokens...");
return Array.from(Array(TOTAL_TOKENS)).map((_, i) => ({
tokenId: i,
traits: createUniqueTraitsCombination(),
}));
};
const createUniqueTraitsCombination = () => {
const traits = [];
traitsList.forEach(({ display, type, options, ignore }) => {
// Use only options that fulfill their allowed/forbidden conditions (if they have)
const filteredOptions = filterOptionsByConditions(options, traits);
// Randomly select a trait option
const option = getRandomWeightedOption(filteredOptions);
// Push selected trait option (if it has a defined value)
if (option.value) {
traits.push({
...(type && { type }),
...(display && { display }),
...(ignore && { ignore }),
...option,
});
}
});
// Filter out traits that need to be ignored for uniqueness calculation,
// and then calculate the hash of the rest of the selected traits combination
const traitsHash = hash(traits.filter(({ ignore }) => !ignore));
// Use recursion if the traits combination was already used
if (uniqueCombinationsHashes.has(traitsHash)) {
return createUniqueTraitsCombination();
}
// Else save the hash and return the traits combination
uniqueCombinationsHashes.add(traitsHash);
return traits;
};
const filterOptionsByConditions = (options, traits) => {
return options.filter(({ allowed, forbidden }) => {
if (
allowed &&
allowed.length > 0 &&
!traits.some(({ value }) => allowed.includes(value))
) {
return false;
}
if (
forbidden &&
forbidden.length > 0 &&
traits.some(({ value }) => forbidden.includes(value))
) {
return false;
}
return true;
});
};
const getRandomWeightedOption = (options) => {
// Transform weights array into an accumulated weights array
// for instance: [20, 30, 50] --> [20, 50, 100]
const accWeights = options.reduce(
(acc, { weight }, i) => acc.concat(weight + (acc[i - 1] || 0)),
[]
);
// Select one of the options, based on a rand number
const rand = Math.random() * accWeights[accWeights.length - 1];
const index = accWeights.findIndex((accWeight) => rand < accWeight);
return options[index];
};
const hash = (object) => {
return crypto
.createHash("sha256")
.update(JSON.stringify(object))
.digest("hex");
};
const generateTokensFiles = async (tokens) => {
console.log("\n--> Generating tokens files...");
directoryGuard(DEFAULT_METADATA_PATH);
directoryGuard(DEFAULT_IMAGES_PATH);
for (let token of tokens) {
generateTokenMetadata(token);
await generateTokenImage(token);
process.stdout.write(
`Current progress: ${Math.round((token.tokenId / TOTAL_TOKENS) * 100)}%\r`
);
}
process.stdout.write(`Current progress: 100%\r`);
};
const directoryGuard = (directory) => {
rmdirSync(directory, { recursive: true });
mkdirSync(directory, { recursive: true });
};
const generateTokenMetadata = ({ tokenId, traits }) => {
const metadata = {
tokenId,
name: `${TOKEN_NAME_PREFIX}${tokenId}`,
...(TOKEN_DESCRIPTION && { description: TOKEN_DESCRIPTION }),
image: `${IMAGES_BASE_URI}${tokenId}.png`,
attributes: traits.map(({ display, type, value }) => ({
...(display && { display_type: display }),
...(type && { trait_type: type }),
value,
})),
};
writeFileSync(
`${DEFAULT_METADATA_PATH}${tokenId}`,
JSON.stringify(metadata, null, 2)
);
};
const generateTokenImage = async ({ tokenId, traits }) => {
ctx.clearRect(0, 0, canvas.width,canvas.height)
for (let { image } of traits) {
if (image) {
const layerImage = await loadImage(image);
ctx.drawImage(layerImage, 0, 0);
}
}
writeFileSync(
`${DEFAULT_IMAGES_PATH}${tokenId}.png`,
canvas.toBuffer("image/png")
);
};
const printStats = (tokens) => {
console.log(`\nTOTAL NUMBER OF TOKENS: ${tokens.length}`);
traitsList.forEach(({ type, options }) => {
// Calculate trait stats
console.log(`\nTRAIT TYPE: ${type || "<generic-type>"}`);
const traitStats = options.map(({ value }) => {
const count = tokens.filter(({ traits }) => {
if (value) {
return traits.some(
(trait) =>
(type ? trait.type === type : true) && trait.value === value
);
}
return !traits.some((trait) => trait.type === type);
}).length;
const percentage = `${((count / tokens.length) * 100).toFixed(2)}%`;
return { value: value || "<none>", count, percentage };
});
// Print stats table with traits sorted by rarity (desc)
console.table(
traitStats
.sort((a, b) => a.count - b.count)
.reduce(
(acc, { value, count, percentage }) => ({
...acc,
[value]: { count, percentage },
}),
{}
)
);
});
};
/** MAIN SCRIPT **/
(async () => {
try {
const tokens = createUniqueTokens();
printStats(tokens);
await generateTokensFiles(tokens);
console.log("\n\nSUCCESS!");
} catch (err) {
if (err instanceof RangeError) {
console.log(
`\nERROR: it was impossible to create ${TOTAL_TOKENS} unique tokens with the current configuration.`,
"\nTo fix: try lowering the value of TOTAL_TOKENS or update ORDERED_TRAITS_LIST in the config."
);
} else throw err;
}
})();