[Feature] Generative art within the Candy Machine Command line (#506)
* Created randomized configuration file * Added packages * Updated with logs * Created the ability to generate json files * Added the image creation algorithm * Updated README * Added example traits config file * Added example assets
|
@ -31,3 +31,5 @@ hfuzz_workspace
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
.cache
|
.cache
|
||||||
js/packages/web/.env
|
js/packages/web/.env
|
||||||
|
traits
|
||||||
|
traits-configuration.json
|
||||||
|
|
|
@ -2,10 +2,88 @@
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-bf0e-db0ccc972ff7.mp4
|
https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-bf0e-db0ccc972ff7.mp4
|
||||||
|
|
||||||
|
## Creating generative art
|
||||||
|
|
||||||
|
1. Create a `traits` folder and create a list of directories for the traits (i.e. background, shirt, sunglasses). Look at the `example-traits` for guidance
|
||||||
|
2. Run the following command to create a configuration file called `traits-configuration.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
metaplex generate_art_configurations <directory>
|
||||||
|
ts-node cli generate_art_configurations <directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
The following file will be generated (based off of `example-traits`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"symbol": "",
|
||||||
|
"description": "",
|
||||||
|
"creators": [],
|
||||||
|
"collection": {},
|
||||||
|
"breakdown": {
|
||||||
|
"background": {
|
||||||
|
"blue.png": 0.04,
|
||||||
|
"brown.png": 0.04,
|
||||||
|
"flesh.png": 0.05,
|
||||||
|
"green.png": 0.02,
|
||||||
|
"light-blue.png": 0.06,
|
||||||
|
"light-green.png": 0.01,
|
||||||
|
"light-pink.png": 0.07,
|
||||||
|
"light-purple.png": 0.05,
|
||||||
|
"light-yellow.png": 0.06,
|
||||||
|
"orange.png": 0.07,
|
||||||
|
"pink.png": 0.02,
|
||||||
|
"purple.png": 0.03,
|
||||||
|
"red.png": 0.05,
|
||||||
|
"yellow.png": 0.43
|
||||||
|
},
|
||||||
|
"eyes": {
|
||||||
|
"egg-eyes.png": 0.3,
|
||||||
|
"heart-eyes.png": 0.12,
|
||||||
|
"square-eyes.png": 0.02,
|
||||||
|
"star-eyes.png": 0.56
|
||||||
|
},
|
||||||
|
"face": {
|
||||||
|
"cyan-face.png": 0.07,
|
||||||
|
"dark-green-face.png": 0.04,
|
||||||
|
"flesh-face.png": 0.03,
|
||||||
|
"gold-face.png": 0.11,
|
||||||
|
"grapefruit-face.png": 0.07,
|
||||||
|
"green-face.png": 0.05,
|
||||||
|
"pink-face.png": 0.05,
|
||||||
|
"purple-face.png": 0.02,
|
||||||
|
"sun-face.png": 0.1,
|
||||||
|
"teal-face.png": 0.46
|
||||||
|
},
|
||||||
|
"mouth": {
|
||||||
|
"block-mouth.png": 0.23,
|
||||||
|
"smile-mouth.png": 0.09,
|
||||||
|
"triangle-mouth.png": 0.68
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order": ["background", "face", "eyes", "mouth"],
|
||||||
|
"width": 1000,
|
||||||
|
"height": 1000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Go through and customize the fields in the `traits-configuration.json`, such as `name`, `symbol`, `description`, , `creators`, `collection`, `width`, and `height`.
|
||||||
|
4. After you have adjusted the configurations to your heart's content, you can run the following command to generate the JSON files along with the images.
|
||||||
|
|
||||||
|
```
|
||||||
|
metaplex create_generative_art -c <configuration_file_location> -n <number_of_images>
|
||||||
|
ts-node cli create_generative_art -c <configuration_file_location> -n <number_of_images>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. This will create an `assets` folder, with a set of the JSON and PNG files to make it work!
|
||||||
|
|
||||||
## assets folder
|
## assets folder
|
||||||
* Folder with file pairs named with inrementing integer numbers starting from 0.png and 0.json
|
|
||||||
* the image HAS TO be a `PNG`
|
- Folder with file pairs named with incrementing integer numbers starting from 0.png and 0.json
|
||||||
* JSON format can be checked out here: https://docs.metaplex.com/nft-standard. example below:
|
- the image HAS TO be a `PNG`
|
||||||
|
- JSON format can be checked out here: https://docs.metaplex.com/nft-standard. example below:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Solflare X NFT",
|
"name": "Solflare X NFT",
|
||||||
|
@ -61,6 +139,7 @@ https://user-images.githubusercontent.com/81876372/133098938-dc2c91a6-1280-4ee1-
|
||||||
```
|
```
|
||||||
|
|
||||||
Install and build
|
Install and build
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn install
|
yarn install
|
||||||
yarn build
|
yarn build
|
||||||
|
@ -74,48 +153,56 @@ yarn run package:macos
|
||||||
You can now either use `metaplex` OR the `ts-node cli` to execute the following commands.
|
You can now either use `metaplex` OR the `ts-node cli` to execute the following commands.
|
||||||
|
|
||||||
1. Upload your images and metadata. Refer to the NFT [standard](https://docs.metaplex.com/nft-standard) for the correct format.
|
1. Upload your images and metadata. Refer to the NFT [standard](https://docs.metaplex.com/nft-standard) for the correct format.
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
|
metaplex upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
|
||||||
ts-node cli upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
|
ts-node cli upload ~/nft-test/mini_drop --keypair ~/.config/solana/id.json
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Verify everything is uploaded. Rerun the first command until it is.
|
2. Verify everything is uploaded. Rerun the first command until it is.
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex verify --keypair ~/.config/solana/id.json
|
metaplex verify --keypair ~/.config/solana/id.json
|
||||||
ts-node cli verify --keypair ~/.config/solana/id.json
|
ts-node cli verify --keypair ~/.config/solana/id.json
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create your candy machine. It can cost up to ~15 solana per 10,000 images.
|
3. Create your candy machine. It can cost up to ~15 solana per 10,000 images.
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex create_candy_machine -k ~/.config/solana/id.json -p 1
|
metaplex create_candy_machine -k ~/.config/solana/id.json -p 1
|
||||||
ts-node cli create_candy_machine -k ~/.config/solana/id.json -p 3
|
ts-node cli create_candy_machine -k ~/.config/solana/id.json -p 3
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Set the start date and update the price of your candy machine.
|
4. Set the start date and update the price of your candy machine.
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
|
metaplex update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
|
||||||
ts-node cli update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
|
ts-node cli update_candy_machine -k ~/.config/solana/id.json -d "20 Apr 2021 04:20:00 GMT" -p 0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Test mint a token (provided it's after the start date)
|
5. Test mint a token (provided it's after the start date)
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex mint_one_token -k ~/.config/solana/id.json
|
metaplex mint_one_token -k ~/.config/solana/id.json
|
||||||
ts-node cli mint_one_token -k ~/.config/solana/id.json
|
ts-node cli mint_one_token -k ~/.config/solana/id.json
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Check if you received any tokens.
|
6. Check if you received any tokens.
|
||||||
|
|
||||||
```
|
```
|
||||||
spl-token accounts
|
spl-token accounts
|
||||||
```
|
```
|
||||||
|
|
||||||
7. If you are listed as a creator, run this command to sign your NFTs post sale. This will sign only the latest candy machine that you've created (stored in .cache/candyMachineList.json).
|
7. If you are listed as a creator, run this command to sign your NFTs post sale. This will sign only the latest candy machine that you've created (stored in .cache/candyMachineList.json).
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json
|
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json
|
||||||
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json
|
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json
|
||||||
```
|
```
|
||||||
|
|
||||||
8. If you wish to sign metadata from another candy machine run with the --cndy flag.
|
8. If you wish to sign metadata from another candy machine run with the --cndy flag.
|
||||||
|
|
||||||
```
|
```
|
||||||
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
|
metaplex sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
|
||||||
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
|
ts-node cli sign_candy_machine_metadata -k ~/.config/solana/id.json --cndy CANDY_MACHINE_ADDRESS_HERE
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"1","symbol":"","image":"0.png","properties":{"files":[{"uri":"0.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"blue"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}
|
After Width: | Height: | Size: 8.8 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"2","symbol":"","image":"1.png","properties":{"files":[{"uri":"1.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-blue"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}
|
After Width: | Height: | Size: 9.1 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"3","symbol":"","image":"2.png","properties":{"files":[{"uri":"2.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-pink"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"sun-face"}],"collection":{}}
|
After Width: | Height: | Size: 9.3 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"4","symbol":"","image":"3.png","properties":{"files":[{"uri":"3.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-yellow"},{"trait_type":"eyes","value":"square-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}
|
After Width: | Height: | Size: 7.7 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"5","symbol":"","image":"4.png","properties":{"files":[{"uri":"4.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"brown"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"block-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"6","symbol":"","image":"5.png","properties":{"files":[{"uri":"5.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"yellow"},{"trait_type":"eyes","value":"square-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"purple-face"}],"collection":{}}
|
After Width: | Height: | Size: 8.0 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"7","symbol":"","image":"6.png","properties":{"files":[{"uri":"6.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-green"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"cyan-face"}],"collection":{}}
|
After Width: | Height: | Size: 9.0 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"8","symbol":"","image":"7.png","properties":{"files":[{"uri":"7.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"green"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"teal-face"}],"collection":{}}
|
After Width: | Height: | Size: 8.8 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"9","symbol":"","image":"8.png","properties":{"files":[{"uri":"8.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"light-yellow"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"grapefruit-face"}],"collection":{}}
|
After Width: | Height: | Size: 9.0 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"name":"10","symbol":"","image":"9.png","properties":{"files":[{"uri":"9.png","type":"image/png"}],"category":"image","creators":[]},"description":"","seller_fee_basis_points":500,"attributes":[{"trait_type":"background","value":"red"},{"trait_type":"eyes","value":"star-eyes"},{"trait_type":"mouth","value":"triangle-mouth"},{"trait_type":"face","value":"sun-face"}],"collection":{}}
|
After Width: | Height: | Size: 7.9 KiB |
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"symbol": "",
|
||||||
|
"description": "",
|
||||||
|
"creators": [],
|
||||||
|
"collection": {},
|
||||||
|
"breakdown": {
|
||||||
|
"background": {
|
||||||
|
"blue.png": 0.04,
|
||||||
|
"brown.png": 0.04,
|
||||||
|
"flesh.png": 0.05,
|
||||||
|
"green.png": 0.02,
|
||||||
|
"light-blue.png": 0.06,
|
||||||
|
"light-green.png": 0.01,
|
||||||
|
"light-pink.png": 0.07,
|
||||||
|
"light-purple.png": 0.05,
|
||||||
|
"light-yellow.png": 0.06,
|
||||||
|
"orange.png": 0.07,
|
||||||
|
"pink.png": 0.02,
|
||||||
|
"purple.png": 0.03,
|
||||||
|
"red.png": 0.05,
|
||||||
|
"yellow.png": 0.43
|
||||||
|
},
|
||||||
|
"eyes": {
|
||||||
|
"egg-eyes.png": 0.3,
|
||||||
|
"heart-eyes.png": 0.12,
|
||||||
|
"square-eyes.png": 0.02,
|
||||||
|
"star-eyes.png": 0.56
|
||||||
|
},
|
||||||
|
"face": {
|
||||||
|
"cyan-face.png": 0.07,
|
||||||
|
"dark-green-face.png": 0.04,
|
||||||
|
"flesh-face.png": 0.03,
|
||||||
|
"gold-face.png": 0.11,
|
||||||
|
"grapefruit-face.png": 0.07,
|
||||||
|
"green-face.png": 0.05,
|
||||||
|
"pink-face.png": 0.05,
|
||||||
|
"purple-face.png": 0.02,
|
||||||
|
"sun-face.png": 0.1,
|
||||||
|
"teal-face.png": 0.46
|
||||||
|
},
|
||||||
|
"mouth": {
|
||||||
|
"block-mouth.png": 0.23,
|
||||||
|
"smile-mouth.png": 0.09,
|
||||||
|
"triangle-mouth.png": 0.68
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order": ["background", "face", "eyes", "mouth"],
|
||||||
|
"width": 1000,
|
||||||
|
"height": 1000
|
||||||
|
}
|
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -52,7 +52,14 @@
|
||||||
"ipfs-http-client": "^52.0.3",
|
"ipfs-http-client": "^52.0.3",
|
||||||
"jsonschema": "^1.4.0",
|
"jsonschema": "^1.4.0",
|
||||||
"loglevel": "^1.7.1",
|
"loglevel": "^1.7.1",
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1",
|
||||||
|
"weighted": "^0.3.0",
|
||||||
|
"canvas": "^2.8.0",
|
||||||
|
"image-data-uri": "^2.0.1",
|
||||||
|
"imagemin": "^7.0.1",
|
||||||
|
"imagemin-pngquant": "^9.0.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"merge-images": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.15.6",
|
"@babel/preset-env": "^7.15.6",
|
||||||
|
|
|
@ -24,11 +24,14 @@ import {
|
||||||
import { Config } from './types';
|
import { Config } from './types';
|
||||||
import { upload } from './commands/upload';
|
import { upload } from './commands/upload';
|
||||||
import { verifyTokenMetadata } from './commands/verifyTokenMetadata';
|
import { verifyTokenMetadata } from './commands/verifyTokenMetadata';
|
||||||
|
import { generateConfigurations } from './commands/generateConfigurations';
|
||||||
import { loadCache, saveCache } from './helpers/cache';
|
import { loadCache, saveCache } from './helpers/cache';
|
||||||
import { mint } from './commands/mint';
|
import { mint } from './commands/mint';
|
||||||
import { signMetadata } from './commands/sign';
|
import { signMetadata } from './commands/sign';
|
||||||
import { signAllMetadataFromCandyMachine } from './commands/signAll';
|
import { signAllMetadataFromCandyMachine } from './commands/signAll';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
import { createMetadataFiles } from './helpers/metadata';
|
||||||
|
import { createGenerativeArt } from './commands/createArt';
|
||||||
|
|
||||||
program.version('0.0.2');
|
program.version('0.0.2');
|
||||||
|
|
||||||
|
@ -139,7 +142,7 @@ programCommand('upload')
|
||||||
`ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`,
|
`ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`,
|
||||||
);
|
);
|
||||||
if (warn) {
|
if (warn) {
|
||||||
log.info('not all images have been uplaoded, rerun this step.');
|
log.info('not all images have been uploaded, rerun this step.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -624,6 +627,56 @@ programCommand('sign_all')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
programCommand('generate_art_configurations')
|
||||||
|
.argument('<directory>', 'Directory containing traits named from 0-n', val =>
|
||||||
|
fs.readdirSync(`${val}`),
|
||||||
|
)
|
||||||
|
.action(async (files: string[]) => {
|
||||||
|
log.info('creating traits configuration file');
|
||||||
|
const startMs = Date.now();
|
||||||
|
const successful = await generateConfigurations(files);
|
||||||
|
const endMs = Date.now();
|
||||||
|
const timeTaken = new Date(endMs - startMs).toISOString().substr(11, 8);
|
||||||
|
if (successful) {
|
||||||
|
log.info('traits-configuration.json has been created!');
|
||||||
|
log.info(
|
||||||
|
`ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info('The art configuration file was not created');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
programCommand('create_generative_art')
|
||||||
|
.option(
|
||||||
|
'-n, --number-of-images <string>',
|
||||||
|
'Number of images to be generated',
|
||||||
|
'100',
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'-c, --config-location <string>',
|
||||||
|
'Location of the traits configuration file',
|
||||||
|
'./traits-configuration.json',
|
||||||
|
)
|
||||||
|
.action(async (directory, cmd) => {
|
||||||
|
const { numberOfImages, configLocation } = cmd.opts();
|
||||||
|
|
||||||
|
log.info('Loaded configuration file');
|
||||||
|
|
||||||
|
// 1. generate the metadata json files
|
||||||
|
const randomSets = await createMetadataFiles(
|
||||||
|
numberOfImages,
|
||||||
|
configLocation,
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info('JSON files have been created within the assets directory');
|
||||||
|
|
||||||
|
// 2. piecemeal generate the images
|
||||||
|
await createGenerativeArt(configLocation, randomSets);
|
||||||
|
|
||||||
|
log.info('Images have been created successfully!');
|
||||||
|
});
|
||||||
|
|
||||||
function programCommand(name: string) {
|
function programCommand(name: string) {
|
||||||
return program
|
return program
|
||||||
.command(name)
|
.command(name)
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import canvas from 'canvas';
|
||||||
|
import imagemin from 'imagemin';
|
||||||
|
import imageminPngquant from 'imagemin-pngquant';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
|
import { readJsonFile, sleep } from '../helpers/various';
|
||||||
|
import { ASSETS_DIRECTORY, TRAITS_DIRECTORY } from '../helpers/metadata';
|
||||||
|
|
||||||
|
const { createCanvas, loadImage } = canvas;
|
||||||
|
|
||||||
|
const createImage = async (order = [], image, width, height) => {
|
||||||
|
const canvas = createCanvas(width, height);
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const ID = parseInt(image.id, 10) - 1;
|
||||||
|
await Promise.all(
|
||||||
|
order.map(async cur => {
|
||||||
|
const imageLocation = `${TRAITS_DIRECTORY}/${cur}/${image[cur]}`;
|
||||||
|
const loadedImage = await loadImage(imageLocation);
|
||||||
|
context.patternQuality = 'best';
|
||||||
|
context.quality = 'best';
|
||||||
|
context.drawImage(loadedImage, 0, 0, width, height);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const buffer = canvas.toBuffer('image/png');
|
||||||
|
const optimizedImage = await imagemin.buffer(buffer, {
|
||||||
|
plugins: [
|
||||||
|
imageminPngquant({
|
||||||
|
quality: [0.6, 0.95],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
log.info(`Placed ${ID}.png into the ${ASSETS_DIRECTORY}`);
|
||||||
|
fs.writeFileSync(`${ASSETS_DIRECTORY}/${ID}.png`, optimizedImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createGenerativeArt(
|
||||||
|
configLocation: string,
|
||||||
|
randomizedSets,
|
||||||
|
) {
|
||||||
|
const { order, width, height } = await readJsonFile(configLocation);
|
||||||
|
const PROCESSING_LENGTH: number = 10;
|
||||||
|
|
||||||
|
const processImage = async (marker = 0) => {
|
||||||
|
const slice = randomizedSets.slice(marker, marker + PROCESSING_LENGTH + 1);
|
||||||
|
// generate images for the portion
|
||||||
|
await Promise.all(
|
||||||
|
slice.map(async image => {
|
||||||
|
await createImage(order, image, width, height);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
marker += PROCESSING_LENGTH;
|
||||||
|
await sleep(1000);
|
||||||
|
if (marker < randomizedSets.length - 1) {
|
||||||
|
processImage(marker);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// recurse until completion
|
||||||
|
processImage();
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
|
import { generateRandoms } from '../helpers/various';
|
||||||
|
|
||||||
|
const { readdir, writeFile } = fs.promises;
|
||||||
|
|
||||||
|
export async function generateConfigurations(
|
||||||
|
traits: string[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
let generateSuccessful: boolean = true;
|
||||||
|
const configs = {
|
||||||
|
name: '',
|
||||||
|
symbol: '',
|
||||||
|
description: '',
|
||||||
|
creators: [],
|
||||||
|
collection: {},
|
||||||
|
breakdown: {},
|
||||||
|
order: traits,
|
||||||
|
width: 1000,
|
||||||
|
height: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
traits.map(async trait => {
|
||||||
|
const attributes = await readdir(`./traits/${trait}`);
|
||||||
|
const randoms = generateRandoms(attributes.length - 1);
|
||||||
|
const tmp = {};
|
||||||
|
|
||||||
|
attributes.forEach((attr, i) => {
|
||||||
|
tmp[attr] = randoms[i] / 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
configs['breakdown'][trait] = tmp;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
generateSuccessful = false;
|
||||||
|
log.error('Error created configurations', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile('./traits-configuration.json', JSON.stringify(configs));
|
||||||
|
} catch (err) {
|
||||||
|
generateSuccessful = false;
|
||||||
|
log.error('Error writing configurations to configs.json', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateSuccessful;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import log from 'loglevel';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { generateRandomSet, getMetadata, readJsonFile } from './various';
|
||||||
|
|
||||||
|
const { writeFile, mkdir } = fs.promises;
|
||||||
|
|
||||||
|
export const ASSETS_DIRECTORY = './assets';
|
||||||
|
export const TRAITS_DIRECTORY = './traits';
|
||||||
|
|
||||||
|
export async function createMetadataFiles(
|
||||||
|
numberOfImages: string,
|
||||||
|
configLocation: string,
|
||||||
|
): Promise<any[]> {
|
||||||
|
let numberOfFilesCreated: number = 0;
|
||||||
|
const randomizedSets = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(ASSETS_DIRECTORY)) {
|
||||||
|
try {
|
||||||
|
await mkdir(ASSETS_DIRECTORY);
|
||||||
|
} catch (err) {
|
||||||
|
log.error('unable to create assets directory', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
breakdown,
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
creators,
|
||||||
|
description,
|
||||||
|
seller_fee_basis_points,
|
||||||
|
collection,
|
||||||
|
} = await readJsonFile(configLocation);
|
||||||
|
|
||||||
|
while (numberOfFilesCreated < parseInt(numberOfImages, 10)) {
|
||||||
|
const randomizedSet = generateRandomSet(breakdown);
|
||||||
|
|
||||||
|
if (!_.some(randomizedSets, randomizedSet)) {
|
||||||
|
randomizedSets.push(randomizedSet);
|
||||||
|
|
||||||
|
const metadata = getMetadata(
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
numberOfFilesCreated,
|
||||||
|
creators,
|
||||||
|
description,
|
||||||
|
seller_fee_basis_points,
|
||||||
|
randomizedSet,
|
||||||
|
collection,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(
|
||||||
|
`${ASSETS_DIRECTORY}/${numberOfFilesCreated}.json`,
|
||||||
|
JSON.stringify(metadata),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`${numberOfFilesCreated} failed to get created`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfFilesCreated += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map through after because IDs would make sets unique
|
||||||
|
const randomSetWithIds = randomizedSets.map((item, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return randomSetWithIds;
|
||||||
|
}
|
|
@ -1,4 +1,25 @@
|
||||||
import { LAMPORTS_PER_SOL, AccountInfo } from '@solana/web3.js';
|
import { LAMPORTS_PER_SOL, AccountInfo } from '@solana/web3.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import weighted from 'weighted';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const { readFile } = fs.promises;
|
||||||
|
|
||||||
|
export async function readJsonFile(fileName: string) {
|
||||||
|
const file = await readFile(fileName, 'utf-8');
|
||||||
|
return JSON.parse(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateRandomSet = breakdown => {
|
||||||
|
const tmp = {};
|
||||||
|
Object.keys(breakdown).forEach(attr => {
|
||||||
|
const randomSelection = weighted.select(breakdown[attr]);
|
||||||
|
tmp[attr] = randomSelection;
|
||||||
|
});
|
||||||
|
|
||||||
|
return tmp;
|
||||||
|
};
|
||||||
|
|
||||||
export const getUnixTs = () => {
|
export const getUnixTs = () => {
|
||||||
return new Date().getTime() / 1000;
|
return new Date().getTime() / 1000;
|
||||||
};
|
};
|
||||||
|
@ -97,6 +118,64 @@ export function chunks(array, size) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRandoms(
|
||||||
|
numberOfAttrs: number = 1,
|
||||||
|
total: number = 100,
|
||||||
|
) {
|
||||||
|
const numbers = [];
|
||||||
|
const loose_percentage = total / numberOfAttrs;
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfAttrs; i++) {
|
||||||
|
const random = Math.floor(Math.random() * loose_percentage) + 1;
|
||||||
|
numbers.push(random);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = numbers.reduce((prev, cur) => {
|
||||||
|
return prev + cur;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
numbers.push(total - sum);
|
||||||
|
return numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetadata = (
|
||||||
|
name: string = '',
|
||||||
|
symbol: string = '',
|
||||||
|
index: number = 0,
|
||||||
|
creators,
|
||||||
|
description: string = '',
|
||||||
|
seller_fee_basis_points: number = 500,
|
||||||
|
attrs,
|
||||||
|
collection,
|
||||||
|
) => {
|
||||||
|
const attributes = [];
|
||||||
|
for (const prop in attrs) {
|
||||||
|
attributes.push({
|
||||||
|
trait_type: prop,
|
||||||
|
value: path.parse(attrs[prop]).name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: `${name}${index + 1}`,
|
||||||
|
symbol,
|
||||||
|
image: `${index}.png`,
|
||||||
|
properties: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
uri: `${index}.png`,
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
category: 'image',
|
||||||
|
creators,
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
seller_fee_basis_points,
|
||||||
|
attributes,
|
||||||
|
collection,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getMultipleAccountsCore = async (
|
const getMultipleAccountsCore = async (
|
||||||
connection: any,
|
connection: any,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
|
|