├── .eslintrc.js
├── .gitbook.yml
├── .gitbook
└── assets
│ ├── Branching result.png
│ ├── Branching with nested folders.png
│ ├── Screen Shot 2022-02-21 at 9.53.59 AM.png
│ ├── image (1).png
│ ├── image (2).png
│ ├── image (3).png
│ ├── image (4).png
│ ├── image (5).png
│ └── image.png
├── .gitignore
├── .prettierrc.js
├── BASIC-README.md
├── LICENSE
├── README.md
├── Solana
└── solana_config.js
├── Tezos
├── tezos_config.js
└── updateinfo.js
├── documentation
├── .gitbook
│ └── assets
│ │ ├── Branching result.png
│ │ ├── Branching with nested folders (1) (1) (1).png
│ │ ├── Branching with nested folders (1) (1).png
│ │ ├── Branching with nested folders (1).png
│ │ ├── Branching with nested folders.png
│ │ ├── image (1) (1).png
│ │ ├── image (1).png
│ │ ├── image (2).png
│ │ ├── image.png
│ │ └── z-index.png
├── README.md
├── SUMMARY.md
├── other-blockchains
│ ├── cardano.md
│ ├── solana.md
│ └── tezos.md
├── readme
│ ├── branching-if-then.md
│ ├── extending-a-collection.md
│ ├── inclusions-and-exclusions.md
│ ├── quickstart.md
│ └── z-index-layer-order.md
└── utils
│ ├── README.md
│ ├── metadata.md
│ ├── rarity.md
│ ├── regenerate.md
│ ├── replace-for-rares.md
│ └── stats.md
├── index.js
├── layers
├── .DS_Store
├── Accessory
│ ├── Helmet#10
│ │ └── Helmet.png
│ ├── MetallicShades#1.png
│ ├── NONE#100.png
│ └── Purple Glasses#25
│ │ ├── Glasses front.png
│ │ └── z-10,Glasses back.png
├── Back Accessory
│ ├── Backpack#50
│ │ ├── 1backpack-lines.png
│ │ └── backpack-FILL.png
│ └── rockets#40.png
├── Background
│ ├── coulds#5.png
│ ├── stars#5.png
│ └── swirl#5.png
├── Clothes
│ ├── Shirt three#44
│ │ ├── 3shadow.png
│ │ └── shirt-FILL.png
│ ├── floral#30.png
│ ├── gray tee#30.png
│ ├── shirt one #3.png
│ └── shirt two#3.png
├── Eyes
│ ├── z1,eye#50
│ │ ├── happy#50
│ │ │ └── eye2.png
│ │ └── normal#50
│ │ │ └── eye1.png
│ └── z1,star eyes#30.png
├── Hair
│ └── z1,Hair#20
│ │ ├── bluehair#50.png
│ │ └── purplehair#50.png
├── Head
│ ├── faceA#3.png
│ └── faceB#3.png
└── Shirt Accessories
│ ├── Golden Sakura#1.png
│ ├── gold chain#40.png
│ └── nametag#40.png
├── modules
└── HashlipsGiffer.js
├── package-lock.json
├── package.json
├── src
├── blendMode.js
├── config.js
└── main.js
├── ultraRares
├── images
│ ├── 1.png
│ └── 2.png
└── json
│ ├── 1.json
│ └── 2.json
├── utils
├── cardano.js
├── createPreviewCollage.js
├── metaplex.js
├── preview_gif.js
├── provenance.js
├── rarity.js
├── rebuildAll.js
├── regenerate.js
├── regenerateMetadata.js
├── removeTrait.js
├── replace.js
├── resize.js
├── tezos.js
└── updateInfo.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es2021: true,
6 | },
7 | extends: ["eslint:recommended", "plugin:node/recommended"],
8 | parserOptions: {
9 | ecmaVersion: 12,
10 | },
11 | rules: {
12 | "no-unused-vars": "warn",
13 | indent: ["error", 2],
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/.gitbook.yml:
--------------------------------------------------------------------------------
1 | root: ./documentation/
2 |
--------------------------------------------------------------------------------
/.gitbook/assets/Branching result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/Branching result.png
--------------------------------------------------------------------------------
/.gitbook/assets/Branching with nested folders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/Branching with nested folders.png
--------------------------------------------------------------------------------
/.gitbook/assets/Screen Shot 2022-02-21 at 9.53.59 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/Screen Shot 2022-02-21 at 9.53.59 AM.png
--------------------------------------------------------------------------------
/.gitbook/assets/image (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image (1).png
--------------------------------------------------------------------------------
/.gitbook/assets/image (2).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image (2).png
--------------------------------------------------------------------------------
/.gitbook/assets/image (3).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image (3).png
--------------------------------------------------------------------------------
/.gitbook/assets/image (4).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image (4).png
--------------------------------------------------------------------------------
/.gitbook/assets/image (5).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image (5).png
--------------------------------------------------------------------------------
/.gitbook/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/.gitbook/assets/image.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | # Logs
3 |
4 | logs
5 | _.log
6 | npm-debug.log_
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log\*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | build/
54 | build*/
55 | dist/
56 | node_modules/
57 | jspm_packages/
58 |
59 | # TypeScript v1 declaration files
60 |
61 | typings/
62 |
63 | # TypeScript cache
64 |
65 | \*.tsbuildinfo
66 |
67 | # Optional npm cache directory
68 |
69 | .npm
70 |
71 | # Optional eslint cache
72 |
73 | .eslintcache
74 |
75 | # Microbundle cache
76 |
77 | .rpt2_cache/
78 | .rts2_cache_cjs/
79 | .rts2_cache_es/
80 | .rts2_cache_umd/
81 |
82 | # Optional REPL history
83 |
84 | .node_repl_history
85 |
86 | # Output of 'npm pack'
87 |
88 | \*.tgz
89 |
90 | # Yarn Integrity file
91 |
92 | .yarn-integrity
93 |
94 | # dotenv environment variables file
95 |
96 | .env
97 | .env.test
98 |
99 | # parcel-bundler cache (https://parceljs.org/)
100 |
101 | .cache
102 |
103 | # Next.js build output
104 |
105 | .next
106 |
107 | # Nuxt.js build / generate output
108 |
109 | .nuxt
110 | dist
111 |
112 | # Gatsby files
113 |
114 | .cache/
115 |
116 | # Comment in the public line in if your project uses Gatsby and _not_ Next.js
117 |
118 | # https://nextjs.org/blog/next-9-1#public-directory-support
119 |
120 | # public
121 |
122 | # vuepress build output
123 |
124 | .vuepress/dist
125 |
126 | # Serverless directories
127 |
128 | .serverless/
129 |
130 | # FuseBox cache
131 |
132 | .fusebox/
133 |
134 | # DynamoDB Local files
135 |
136 | .dynamodb/
137 |
138 | # TernJS port file
139 |
140 | .tern-port
141 |
142 | # OSX
143 |
144 | .DS_Store
145 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | {
2 | tabWidth: 2
3 | }
4 |
--------------------------------------------------------------------------------
/BASIC-README.md:
--------------------------------------------------------------------------------
1 | # This is a fork of the HashLips Art Engine 🔥
2 |
3 | Create generative art by using the canvas api and node js. Before you use the generation engine, make sure you have node.js installed.
4 |
5 | ## Installation 🛠️
6 |
7 | If you are cloning the project then run this first, otherwise you can download the source code on the release page and skip this step.
8 |
9 | ```sh
10 | git clone https://github.com/HashLips/hashlips_art_engine.git
11 | ```
12 |
13 | Go to the root of your folder and run this command if you have yarn installed.
14 |
15 | ```sh
16 | yarn install
17 | ```
18 |
19 | Alternatively you can run this command if you have Node (v14 or above) installed.
20 |
21 | ```sh
22 | npm install
23 | ```
24 |
25 | ## Usage ℹ️
26 |
27 | Create your different layers as folders in the 'layers' directory, and add all the layer assets in these directories. You can name the assets anything as long as it has a rarity weight attached in the file name like so: `example element#70.png`. You can optionally change the delimiter `#` to anything you would like to use in the variable `rarityDelimiter` in the `src/config.js` file.
28 |
29 | Once you have all your layers, go into `src/config.js` and update the `layerConfigurations` objects `layersOrder` array to be your layer folders name in order of the back layer to the front layer.
30 |
31 | _Example:_ If you were creating a portrait design, you might have a background, then a head, a mouth, eyes, eyewear, and then headwear, so your `layersOrder` would look something like this:
32 |
33 | ```js
34 | const layerConfigurations = [
35 | {
36 | growEditionSizeTo: 100,
37 | layersOrder: [
38 | { name: "Head" },
39 | { name: "Mouth" },
40 | { name: "Eyes" },
41 | { name: "Eyeswear" },
42 | { name: "Headwear" },
43 | ],
44 | },
45 | ];
46 | ```
47 |
48 | The `name` of each layer object represents the name of the folder (in `/layers/`) that the images reside in.
49 |
50 | Optionally you can now add multiple different `layerConfigurations` to your collection. Each configuration can be unique and have different layer orders, use the same layers or introduce new ones. This gives the artist flexibility when it comes to fine tuning their collections to their needs.
51 |
52 | _Example:_ If you were creating a portrait design, you might have a background, then a head, a mouth, eyes, eyewear, and then headwear and you want to create a new race or just simple re-order the layers or even introduce new layers, then you're `layerConfigurations` and `layersOrder` would look something like this:
53 |
54 | ```js
55 | const layerConfigurations = [
56 | {
57 | // Creates up to 50 artworks
58 | growEditionSizeTo: 50,
59 | layersOrder: [
60 | { name: "Background" },
61 | { name: "Head" },
62 | { name: "Mouth" },
63 | { name: "Eyes" },
64 | { name: "Eyeswear" },
65 | { name: "Headwear" },
66 | ],
67 | },
68 | {
69 | // Creates an additional 100 artworks
70 | growEditionSizeTo: 150,
71 | layersOrder: [
72 | { name: "Background" },
73 | { name: "Head" },
74 | { name: "Eyes" },
75 | { name: "Mouth" },
76 | { name: "Eyeswear" },
77 | { name: "Headwear" },
78 | { name: "AlienHeadwear" },
79 | ],
80 | },
81 | ];
82 | ```
83 |
84 | Update your `format` size, ie the outputted image size, and the `growEditionSizeTo` on each `layerConfigurations` object, which is the amount of variation outputted.
85 |
86 | You can mix up the `layerConfigurations` order on how the images are saved by setting the variable `shuffleLayerConfigurations` in the `config.js` file to true. It is false by default and will save all images in numerical order.
87 |
88 | If you want to have logs to debug and see what is happening when you generate images you can set the variable `debugLogs` in the `config.js` file to true. It is false by default, so you will only see general logs.
89 |
90 | If you want to play around with different blending modes, you can add a `blend: MODE.colorBurn` field to the layersOrder object. If you need a layers to have a different opacity then you can add the `opacity: 0.7` field to the layersOrder object as well. Both the `blend: MODE.colorBurn` and `opacity: 0.7` can be addes on the same layer if you want to.
91 |
92 | Here is an example on how you can play around with both filter fields:
93 |
94 | ```js
95 | const layerConfigurations = [
96 | {
97 | growEditionSizeTo: 5,
98 | layersOrder: [
99 | { name: "Background" },
100 | { name: "Eyeball" },
101 | { name: "Eye color", blend: MODE.colorBurn },
102 | { name: "Iris" },
103 | { name: "Shine" },
104 | { name: "Bottom lid", blend: MODE.overlay, opacity: 0.7 },
105 | { name: "Top lid", opacity: 0.7 },
106 | ],
107 | },
108 | ];
109 | ```
110 |
111 | Here is a list of the different blending modes that you can optionally use.
112 |
113 | ```js
114 | const MODE = {
115 | sourceOver: "source-over",
116 | sourceIn: "source-in",
117 | sourceOut: "source-out",
118 | sourceAtop: "source-out",
119 | destinationOver: "destination-over",
120 | destinationIn: "destination-in",
121 | destinationOut: "destination-out",
122 | destinationAtop: "destination-atop",
123 | lighter: "lighter",
124 | copy: "copy",
125 | xor: "xor",
126 | multiply: "multiply",
127 | screen: "screen",
128 | overlay: "overlay",
129 | darken: "darken",
130 | lighten: "lighten",
131 | colorDodge: "color-dodge",
132 | colorBurn: "color-burn",
133 | hardLight: "hard-light",
134 | softLight: "soft-light",
135 | difference: "difference",
136 | exclusion: "exclusion",
137 | hue: "hue",
138 | saturation: "saturation",
139 | color: "color",
140 | luminosity: "luminosity",
141 | };
142 | ```
143 |
144 | When you are ready, run the following command and your outputted art will be in the `build/images` directory and the json in the `build/json` directory:
145 |
146 | ```sh
147 | npm run build
148 | ```
149 |
150 | or
151 |
152 | ```sh
153 | node index.js
154 | ```
155 |
156 | The program will output all the images in the `build/images` directory along with the metadata files in the `build/json` directory. Each collection will have a `_metadata.json` file that consists of all the metadata in the collection inside the `build/json` directory. The `build/json` folder also will contain all the single json files that represent each image file. The single json file of a image will look something like this:
157 |
158 | ```json
159 | {
160 | "dna": "d956cdf4e460508b5ff90c21974124f68d6edc34",
161 | "name": "#1",
162 | "description": "This is the description of your NFT project",
163 | "image": "https://hashlips/nft/1.png",
164 | "edition": 1,
165 | "date": 1731990799975,
166 | "attributes": [
167 | { "trait_type": "Background", "value": "Black" },
168 | { "trait_type": "Eyeball", "value": "Red" },
169 | { "trait_type": "Eye color", "value": "Yellow" },
170 | { "trait_type": "Iris", "value": "Small" },
171 | { "trait_type": "Shine", "value": "Shapes" },
172 | { "trait_type": "Bottom lid", "value": "Low" },
173 | { "trait_type": "Top lid", "value": "Middle" }
174 | ],
175 | "compiler": "HashLips Art Engine"
176 | }
177 | ```
178 |
179 | You can also add extra metadata to each metadata file by adding your extra items, (key: value) pairs to the `extraMetadata` object variable in the `config.js` file.
180 |
181 | ```js
182 | const extraMetadata = {
183 | creator: "Daniel Eugene Botha",
184 | };
185 | ```
186 |
187 | If you don't need extra metadata, simply leave the object empty. It is empty by default.
188 |
189 | ```js
190 | const extraMetadata = {};
191 | ```
192 |
193 | That's it, you're done.
194 |
195 | ## Utils
196 |
197 | ### Updating baseUri for IPFS
198 |
199 | You might possibly want to update the baseUri after you have ran your collection. To update the baseUri simply run:
200 |
201 | ```sh
202 | node utils/updateBaseUri.js
203 | ```
204 |
205 | ### Generate a preview image
206 |
207 | Create a preview image collage of your collection, run:
208 |
209 | ```sh
210 | node utils/createPreviewCollage.js
211 | ```
212 |
213 | ### Re-generate the \_metadata.json file
214 |
215 | This util will only work if you have all the individual json files and want to re-generate the \_metadata.json file if you lost it, run:
216 |
217 | ```sh
218 | node utils/regenerateMetadata.js
219 | ```
220 |
221 | ### Printing rarity data (Experimental feature)
222 |
223 | To see the percentages of each attribute across your collection, run:
224 |
225 | ```sh
226 | node utils/rarityData.js
227 | ```
228 |
229 | The output will look something like this:
230 |
231 | ```sh
232 | Trait type: Bottom lid
233 | { trait: 'High', chance: '20', occurrence: '15% out of 100%' }
234 | { trait: 'Low', chance: '40', occurrence: '40% out of 100%' }
235 | { trait: 'Middle', chance: '40', occurrence: '45% out of 100%' }
236 |
237 | Trait type: Iris
238 | { trait: 'Large', chance: '20', occurrence: '15% out of 100%' }
239 | { trait: 'Medium', chance: '20', occurrence: '15% out of 100%' }
240 | { trait: 'Small', chance: '60', occurrence: '70% out of 100%' }
241 | ```
242 |
243 | Hope you create some awesome artworks with this code 👄
244 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 HashLips
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DOCUMENTATION https://generator.nftchef.dev/
2 |
3 | # FAQ, general questions, and help
4 |
5 | Please check the discussions for similar issues or open a new discussion.
6 |
7 | ## https://github.com/nftchef/art-engine/discussions
8 |
9 | #### You can find me on twitter or Discord,
10 |
11 | - Twitter: https://twitter.com/nftchef
12 | - Discord genkihagata#3074
13 |
14 | ## ❤️ Support This generator ❤️
15 |
16 | Eth address: `0xeB23ecf1fa9911fca08ecAbe83d426b6bd525bB0`
17 |
18 | # Features
19 |
20 | _This repository is a hard-fork from the original Hashlips generator (c.2021) and makes a couple of **fundamental changes** to how layers are expected to be named and organized. Please read this README's Nesting Structure section and the related Advanced options for a better understanding of how things should be named and organized._
21 |
22 | ## Nested Structures
23 |
24 | - [Nested Layer Support and Trait Type definition modification/branch](#nested-layer-support-and-trait-type-definition-modification-branch)
25 | - [Example](#example)
26 | - [Nesting structure](#nesting-structure)
27 | - [Advanced options](#advanced-options)
28 | - [Required files](#required-files)
29 | - [Sublayer options](#sublayer-options)
30 | - [Metadata Name + Number](#metadata-name-and-number)
31 |
32 | ## Options and conditional output
33 |
34 | - [Chance of "NONE" or skipping a trait](#Chance-of-"NONE"-or-skipping-a-trait)
35 | - [Controlling layer order (z-index)](#Controlling-layer-order-z-index)
36 | - [Flagging Incompatible layers](#flagging-incompatible-layers)
37 | - [Forced Combinations](#forced-combinations)
38 | - [Output Files as JPEG](#outputting-jpegs)
39 | - [ Attribute Display Types and Overrides](#attribute-display-types-and-overrides)
40 | - [ Trait Value Overrides](#trait-value-overrides)
41 |
42 | ## Other Blockchains
43 |
44 | - [Solana](#solana-metadata)
45 | - [Cardano](#cardano-metadata)
46 |
47 | ## Utils
48 |
49 | - [Provenance Hash Generation](#provenance-hash-generation)
50 | - [UTIL: Remove traits from Metadata](#Remove-Trait-Util)
51 | - [Randomly Insert Rare items - Replace Util](#Randomly-Insert-Rare-items---Replace-Util)
52 |
53 | ### Notes
54 |
55 | - [Incompatibilities with original Hashlips](#incompatibilities)
56 |
57 |
58 |
59 |
60 |
61 | # Nested Layer Support and Trait Type definition modification/branch
62 |
63 | This branch of the Hashlips generator builds on the example (v.1.0.6) and allows you to _nest_ sub-folders within your top layer folders, and, optionally gives you a configuration option to overwrite the `trait_type` that is written to the metadata from those layers.
64 |
65 | ## Example
66 |
67 | The following example (included in this repository) uses multiple `layer_configurations` in `config.js` to generate male and female characters, as follows.
68 |
69 | ```js
70 | const layerConfigurations = [
71 | {
72 | growEditionSizeTo: 2,
73 | layersOrder: [
74 | { name: "Background" },
75 | { name: "Female Hair", trait: "Hair" },
76 | ],
77 | },
78 | {
79 | growEditionSizeTo: 5,
80 | layersOrder: [
81 | { name: "Background" },
82 | { name: "Eyeball" },
83 | { name: "Male Hair", trait: "Hair" },
84 | ],
85 | },
86 | ];
87 | ```
88 |
89 | The Hair layers, exist as their own layers in the `layers` directory and use the `trait` key/property to overwrite the output metadata to always look like, the following, regardless of layer folder it is using–so both Male and Female art have a `Hair` trait.
90 |
91 | ```js
92 | {
93 | "trait_type": "Hair",
94 | "value": "Rainbow Ombre"
95 | }
96 | ```
97 |
98 | ## Nesting structure
99 |
100 | In this modified repository, nesting subdirectories is supported and each directory **can** have it's own rarity weight WITH nested weights inside for individual PNG's.
101 |
102 |
103 |
104 | For the example above, `Female Hair` can be read as:
105 |
106 | > Female Hair layer is required from config -> Randomly select either `common` or `rare` with a respective chance of `70% / 30%`. If Common is chosen, randomly pick between Dark Long (20% chance) or Dark Short (20%)
107 |
108 | ## Advanced options
109 |
110 | ### Required files
111 |
112 | Additionally, `png` files that ommit a rarity weight **will be included** always and are considered "required".
113 |
114 | This means, that if you need multiple images to construct a single "trait", e.g., lines layer and fill layer, you could do the following:
115 |
116 | ```js
117 | HAIR
118 | |-- Special Hair#10
119 | |----- 1-line-layer.png
120 | |----- 2-fill-layer.png
121 | ```
122 |
123 | Where the containing folder will define the traits _rarity_ and in the event that it is selected as part of the randomization, BOTH nested images will be included in the final result, in alphabetical oder–hence the 1, 2, numbering.
124 |
125 | ### Options
126 |
127 | #### Exclude a layer from DNA
128 |
129 | If you want to have a layer _ignored_ in the DNA uniqueness check, you can set `bypassDNA: true` in the `options` object. This has the effect of making sure the rest of the traits are unique while not considering the `Background` Layers as traits, for example. The layers _are_ included in the final image.
130 |
131 | ```js
132 | layersOrder: [
133 | { name: "Background" },
134 | { name: "Background" ,
135 | options: {
136 | bypassDNA: false;
137 | }
138 | },
139 | ```
140 |
141 | #### Use Parent folders as trait_value in attributes
142 |
143 | In some projects, you may define a root folder in `layersOrder` only as a starting point in the branching structure and not want to use that folders name as the `trait_value` in the attributes. Rather, nested, weighted subfolders should be the trait_type for example:
144 |
145 | ```
146 | layers/Headware
147 | ├── Cap#10
148 | │ ├── cap a#6.png
149 | │ └── cap b#6.png
150 | └── Hair#10\
151 | ├── hair a#6.png
152 | └── hair b#6.png
153 |
154 | ```
155 |
156 | Rather than the attribute data always listing `Headware` as the type, you can use `Cap` or `Hair` instead.
157 |
158 | Set useRootTraitType`to`false` in config.js to use parent folder names instead of the root.
159 |
160 | ### Sublayer Options
161 |
162 | 🧪 BETA FEATURE
163 |
164 | #### Rename Sublayer traits
165 |
166 | In the case that your folder names need to include number or anything else that you do not want in the final metadata, you can clean up the `trait_type` by passing in the `trait` option to the sublayerOptions object where the nested folder lives. For example, if we are using a subfolder named `1-SubAccessory` and want to rename it to `Backpack Accessory`, pass the following configuration
167 |
168 | ```js
169 | layersOrder: [
170 | {
171 | name: "Back Accessory",
172 | sublayerOptions: {
173 | "1-SubAccessory": { trait: "Backpack Accessory" },
174 | },
175 | },
176 | { name: "Head" },
177 | ```
178 |
179 | #### Blend and Opacity
180 |
181 | By default, nested folders will inherit `blend` and `opacity` settings from the root-level layer defined in `layersOrder`. When you need to overwrite that on a sublayer-basis (by name of the nested folder, not by filename), you can specify a sublayerOptions object.
182 |
183 | ```js
184 | layersOrder: [
185 | { name: "Clothes" },
186 | {
187 | name: "Bases",
188 | blend: "destination-over", // optional
189 | sublayerOptions: { Hands: { blend: "source-over" } },
190 | },
191 | { name: "Holdableitems" },
192 | ],
193 | ```
194 |
195 | #### Common Errors
196 |
197 | When combining multiple sublayer options, remember that each Sublayer is an object that accepts key:value pairs for each valid option above.
198 |
199 | For example, a single layer may have multiple sublayer options in the following format.
200 |
201 | ```js
202 | sublayerOptions: {
203 | Hands: {
204 | trait: 'New trait name',
205 | blend: 'source-over',
206 | opacity: 0.9,
207 | },
208 | Face: {
209 | trait: 'the face',
210 | blend: 'source-over',
211 | opacity: 0.9,
212 | }
213 | }
214 | ```
215 |
216 | **In the example above**: The intended stacking order is `Base > Clothes > Hand > holdableItem`, because `Hands` are a nested subfolder of `Bases`, this can be tricky. By defining `blend: destination-over` for the Base, and then `source-over` for the Hands, the stacking order can be controlled to draw the Base _under_ the clothes and _then_ the Hand above the clothes.
217 |
218 | # Metadata Name and Number
219 |
220 | Name + Number prefix and reset for configuration sets
221 |
222 | If you are using the generator with multiple `layerConfiguration` objects to generate different species/genders/types, it is possible to add a name prefix and a reset counter for the name, so the token names start at `1` for each type.
223 |
224 | for example, if you are creating multiple animals, and each animal should start with `Animal #1`, but the token numbers should increment as normal, you can use the following `namePrefix` and `resetNameIndex` properties to acheive this.
225 |
226 | ```js
227 | growEditionSizeTo: 10,
228 | namePrefix: "Lion",
229 | resetNameIndex: true, // this will start the Lion count at #1 instead of #6
230 | layersOrder: [
231 | { name: "Background" },
232 | { name: "Eyeball" },
233 | { name: "Male Hair", trait: "Hair" },
234 | ],
235 |
236 | ```
237 |
238 | You may choose to omit the `resetNameIndex` or set it to false if you would instead like each layer set to use the token (\_edition) number in the name–it does this by default.
239 |
240 | # Chance of "NONE" or skipping a trait
241 |
242 | If you would like any given layer or sublayer to have a chance at _rolling_ `NONE`, you can do this by adding a blank PNG to the folder, and giving it the name of `NONE` + the weight you would like to use for it's chance of being chosen–identical to any other layer.
243 |
244 | ```
245 | NONE#20.png
246 | ```
247 |
248 | By using this feature, the trait **will not be included in the metadata**. For example, rather than having `trait_type: 'Hat', value: 'NONE"` in the metadata, it will be skipped.
249 |
250 | ## You can change the name
251 |
252 | If you need to change the name from "NONE" to something else, you can change the following in config.js
253 |
254 | ```js
255 | // if you use an empty/transparent file, set the name here.
256 | const emptyLayerName = "NONE";
257 | ```
258 |
259 | ⚠️ NOTE: if you _would_ like the trait to appear as "NONE", change the empty layer name to something you are _not_ using. e.g., 'unused'.
260 |
261 | # Controlling layer order (z-index)
262 |
263 | Generally, layer order is defined by using `layersOrder` in config.js. However, when using nested folders, layer order defaults to alphanumerec sorting (0-9,a-z)
264 |
265 | To manually define stacking order for sublayers (nested folders) use the `z#,` (`z` followed by a positive or negative number, followed bu a `,` commma) prefix for folder names and/or file names.
266 |
267 | Example folders
268 |
269 | ```
270 | |- z-1,folder
271 | |-normal folder/
272 | |-z1,foldername
273 | |-z2,foldername/
274 | |-- z-2,image-that-needs-to-go-under-everything.png
275 | ```
276 |
277 | Folders and files without a `z#,` prefix default to a z-index of `0`
278 |
279 | If a file has a `z#,` prefix, and it's parent folder(s) does as well, the files z-index will be used, allowing you to overwrite and define an individual's layer order.
280 |
281 | Layer order z-indexes are "global" meaning the index is relative to ALL OTHER z-indices defined in every folder/file
282 |
283 | # Flagging Incompatible layers
284 |
285 | > 👉 **For edge cases only** Nested Folders should always be used first and can solve 90% of "if this, then _not_ that" use cases.
286 |
287 | To flag certain images that _should never be used with_ another image, for this, you can use the incompatible configuration in `config.js`
288 |
289 | To set incompatible items, in the `incompatible` object, use the layer/images `cleanName` (the name without rarity weight, or .png extension) as a key, and create an array of incompatible layer names (again, clean names). Layers that have space or hyphens in their names should be wrapped in quotes
290 |
291 | ⚠️ NOTE: Names are expected to be unique. if you have multiple files with the same name, you may accidentally exclude those images.
292 |
293 | ```js
294 | const incompatible = {
295 | Red: ["Dark Long"],
296 | "Name with spaces": ["buzz","rare-Pink-Pompadour" ]
297 | // directory incompatible with directory example
298 | White: ["rare-Pink-Pompadour"],
299 | };
300 | ```
301 |
302 | ⚠️ NOTE: This relies on the layer order to set incompatible DNA sets. For example the key should be the image/layer that comes first (from top to bottom) in the layerConfiguration. in other words, IF the item (KEY) is chosen, then, the generator will know not to pick any of the items in the `[Array]` that it lists.
303 |
304 | # Forced Combinations
305 |
306 | 
307 |
308 | When you need to force/require two images to work together that are in different root-layer-folders (the layer config), you can use the `forcedCombinations` object in `config.js`.
309 |
310 | ```js
311 | const forcedCombinations = {
312 | floral: ["MetallicShades", "Golden Sakura"],
313 | };
314 | ```
315 |
316 | Using the _clean Name_ (file name without weight and .png extension), specify the key (if names have spaces, use quotes `"file name" :`)
317 |
318 | Then, create an array of names that should be required.
319 |
320 | Note: the layer order matters here. The key (name on the left) should be a file withing a layer that comes first in the `layersOrder` configuration, then, files that are required (in the array), should be files in layers _afterward_.
321 |
322 | ### Debugging:
323 |
324 | set `debugLogging = true` in config to check whether files are being `set` and `picked` for the forced combinations. You should see output that looks like the following if it is wokring:
325 |
326 | 
327 |
328 | If not, double check your file names, layer ordering, and quotation marks.
329 |
330 | # Outputting Jpegs
331 |
332 | If you're working with higher res, it's recommended for your storage-costs-sake to output the image to jpeg, to enable this, set `outputJPEG` in `config.js` to `true`.
333 |
334 | ```js
335 | const outputJPEG = true; // if false, the generator outputs png's
336 | ```
337 |
338 | ⚠️ NOTE: If you're running an M1 Mac, you may run into issues with canvas outputting jpegs and may require additional libraries (e.g. Cairo) to solve and may not work at this time.
339 |
340 | ## Attribute Display Types and Overrides
341 |
342 | If you need to add randomized values for traits and different display types supported by OpenSea, this branch re-purposes the `extraAttributes` configuration for that purpose.
343 |
344 | in config.js
345 |
346 | ```js
347 | const extraAttributes = () => ([
348 | {
349 | // Optionally, if you need to overwrite one of your layers attributes.
350 | // You can include the same name as the layer, here, and it will overwrite
351 | //
352 | "trait_type": "Bottom lid",
353 | value:` Bottom lid # ${Math.random() * 100}`,
354 | },
355 | {
356 | display_type: "boost_number",
357 | trait_type: "Aqua Power",
358 | value: Math.random() * 100,
359 | },
360 | {
361 | display_type: "boost_number",
362 | trait_type: "Mana",
363 | value: Math.floor(Math.random() * 100),
364 | },
365 | ```
366 |
367 | You are free to define _extra_ traits that you want each generated image to include in it's metadata, e.g., **health**.
368 |
369 | _Be sure to pass in a randomization function here, otherwise every json file will result in the same value passed here._
370 |
371 | ## Optional
372 |
373 | This also supports overwriting a trait normally assigned by the layer Name/folder and file name. If you'd like to overwrite it with some other value, adding the _same_ trait in `extraMetadata` will overwrite the default trait/value in the generated metadata.
374 |
375 | # Trait Value Overrides
376 |
377 | 🧪 BETA FEATURE
378 |
379 | When you need to override the `trait_value` generated in the metadata.
380 |
381 | By default trait values come from the file name _or_ subfolder that is _chosen_ (the last one in a nested structure with a weight delimiter).
382 | Because many options require the filenames to be unique, there may be a situation where you need to overwrite the default value. To do this, set the overrides in `config.js`
383 |
384 | ```js
385 | /**
386 | * In the event that a filename cannot be the trait value name, for example when
387 | * multiple items should have the same value, specify
388 | * clean-filename: trait-value override pairs. Wrap filenames with spaces in quotes.
389 | */
390 | const traitValueOverrides = {
391 | Helmet: "Space Helmet",
392 | "gold chain": "GOLDEN NECKLACE",
393 | };
394 | ```
395 |
396 | # Provenance Hash Generation
397 |
398 | If you need to generate a provenance hash (and, yes, you should, [read about it here](https://medium.com/coinmonks/the-elegance-of-the-nft-provenance-hash-solution-823b39f99473) ), make sure the following in config.js is set to `true`
399 |
400 | ```js
401 | //IF you need a provenance hash, turn this on
402 | const hashImages = true;
403 | ```
404 |
405 | Then…
406 | After generating images and data, each metadata file will include an `imageHash` property, which is a Keccak256 hash of the output image.
407 |
408 | ## To generate the **Provenance Hash**
409 |
410 | run the following util
411 |
412 | ```
413 | node utils/provenance.js
414 | ```
415 |
416 | **The Provenance information is saved** to the build directory in `_prevenance.json`. This file contains the final hash as well as the (long) concatednated hash string.
417 |
418 | \*Note, if you regenerate the images, **You will also need to regenerate this hash**.
419 |
420 | # Remove Trait Util
421 |
422 | If you need to remove a trait from the generated `attributes` for ALL the generated metadata .json files, you can use the `removeTrait` util command.
423 |
424 | ```
425 | node utils/removeTrait.js "Trait Name"
426 | ```
427 |
428 | If you would like to print additional logging, use the `-d` flag
429 |
430 | ```
431 | node utils/removeTrait.js "Background" -d
432 | ```
433 |
434 | # Randomly Insert Rare items - Replace Util
435 |
436 | If you would like to manually add 'hand drawn' or unique versions into the pool of generated items, this utility takes a source folder (of your new artwork) and inserts it into the `build` directory, assigning them to random id's.
437 |
438 | ## Requirements
439 |
440 | - create a source directory with an images and json folder (any name, you will specify later)
441 | - Name images sequentially from 1.png/jpeg (order does not matter) and place in the images folder.
442 | - Put matching, sequential json files in the json folder
443 |
444 | example:
445 |
446 | ```
447 | ├── ultraRares
448 | │ ├── images
449 | │ │ ├── 1.png
450 | │ │ └── 2.png
451 | │ └── json
452 | │ ├── 1.json
453 | │ └── 2.json
454 | ```
455 |
456 | **You must have matching json files for each of your images.**
457 |
458 | ## Setting up the JSON.
459 |
460 | Because this script randomizes which tokens to replace/place, _it is important_ to update the metadata properly with the resulting tokenId #.
461 |
462 | **_Everywhere_ you need the edition number in the metadata should use the `##` identifier.**
463 |
464 | ```json
465 | "edition": "##",
466 | ```
467 |
468 | **Don't forget the image URI!**
469 |
470 | ```json
471 | "name": "## super rare sunburn ",
472 | "image": "ipfs://NewUriToReplace/##.png",
473 | "edition": "##",
474 | ```
475 |
476 | _if you need `#num` with the `#` in the name for example, use a different symbol other than `##`. see the --replacementSymbol flag below_
477 |
478 | ## Running
479 |
480 | Run the script with the following command, passing in the source directory name, (relateive to the current working dir)
481 |
482 | ```sh
483 | node utils/replace.js [Source Directory]
484 | ```
485 |
486 | example
487 |
488 | ```sh
489 | node utils/replace.js ./ultraRares
490 | ```
491 |
492 | ## Flags
493 |
494 | ### `--help`
495 |
496 | Outputs command help.
497 |
498 | ### `--Debug`
499 |
500 | `-d` outputs additional logging information
501 |
502 | ### `--replacementSymbol`
503 |
504 | If you need the output data to have `#12` for example, with the leading #, the default `##` in the metadata is a problem. use this flag in combination with a different symbol in the metadata json files to replace the passed in symbol with the appropriate edition number
505 |
506 | ```
507 | node index.js ./ultraRares -r '@@'
508 | ```
509 |
510 | This will replace all instances of `@@` with the item number
511 |
512 | ### `--identifier`
513 |
514 | `-i` Change the default object identifier/location for the edition/id number. defaults to "edition". This is used when the metadata object does not have "edition" in the top level, but may have it nested in "properties", for example, in which case you can use the following to locate the proper item in \_metadata.json
515 |
516 | ```
517 | node utils/replace.js ./ultraRares -i properties.edition
518 | ```
519 |
520 | ⚠️ This step should be done BEFORE generating a provenance hash. Each new, replaced image generates a new hash and is inserted into the metadata used for generating the provenance hash.
521 |
522 | ⚠️ This util requires the `build` directory to be complete (after generation)
523 |
524 |
525 |
526 | # Solana Metadata
527 |
528 | 🧪 BETA FEATURE
529 |
530 | If you are building for Solana, all the image generation options in config are available and are the same.
531 |
532 | ## To setup your Solana specific metadata
533 |
534 | Configure the `solona_config.js` file located in the `Solana/` folder.
535 | Here, enter in all the necessary information for your collection.
536 |
537 | You can run the generator AND output Solana data by running the following command from your terminal
538 |
539 | ```
540 | yarn generate:solana
541 | ```
542 |
543 | If you are using npm,
544 |
545 | ```
546 | npm run generate:solana
547 | ```
548 |
549 | **After running, your Solana ready files will be in `build/solana`**
550 |
551 |
552 |
553 | If you need to convert existing images/json to solana metadata standards, you can run the util by itself with,
554 |
555 | ```
556 | node utils/metaplex.js
557 | ```
558 |
559 | # Cardano Metadata
560 |
561 | 🧪 BETA FEATURE: Work in progress
562 | ⚠️ Check the output metadata for cardano standards accuracy
563 |
564 | If you are generating for Cardano, you can generate and output cardano formatted data at the same time, or run the util script separately after generation (or to an existing collection with proper data)
565 |
566 | First, edit the `cardano_config.js` file in the `Cardano/` folder with your information.
567 |
568 | Then, to generate images _and_ cardano data at once, run:
569 |
570 | ```
571 | yarn generate:cardano
572 | ```
573 |
574 | or if you're using npm
575 |
576 | ```
577 | npm run generate:cardano
578 | ```
579 |
580 | ## running the standalone cardano util
581 |
582 | If you have an existing set of generated images and data, **and** you have a configured `Cardano/cardano_config.js` file, you can run the cardano conversion script with:
583 |
584 | ```
585 | node utils/cardano.js
586 | ```
587 |
588 | # GIF compatibility
589 |
590 | This tool currently does not support gif layers or outputting gifs. If you want to GIF generative tool, check out
591 | [Jalagar's GIF Engine](https://github.com/jalagar/Generative_Gif_Engine). It supports most of the same functionality
592 | as this repo (if-then, z-index, layering, Tezos, Solana, legendary replace). If you have any questions or issues,
593 | please create a issue/discussion on Jalagar's repo.
594 |
595 | # incompatibilities
596 |
597 | ⚠️ This was forked originally from hashlips 1.0.6 and may have different syntax/options. Be sure to read this readme for how to use each feature in _this_ branch.
598 |
599 | ### Example:
600 |
601 | For example, if you are using a `Backgrounds` layer and would prefer to remove that as a trait from the generated json,
602 | First, generate the images and json as usual, then, running the remove trait util
603 |
604 | ```
605 |
606 | node utils/removeTrait.js "Background"
607 |
608 | ```
609 |
610 | Will remove the background trait from all metadata files.
611 |
612 | # Breaking Changes
613 |
614 | 1. `extraMetadata` in prior version of this repo/branch, `extraMetadata` was used for attributes, it has been renamed `extraAttributes`
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 | # Basic Setup
624 |
625 | This is fork/combination of the original hashlips generator, for basic configuration
626 | Check the [Basic Configuration readme](BASIC-README.md)
627 |
628 | ```
629 |
630 | ```
631 |
--------------------------------------------------------------------------------
/Solana/solana_config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * If you are exporting your project for Solana:
3 | * 1. Read the Readme section for more info
4 | * 2. Enter your metadata information in this file, more on the Slana Metadata
5 | * standards here, https://docs.metaplex.com/programs/token-metadata/token-standard
6 | * 3. Run the generate for Solana script, yarn generate:solana (or npm run generate:solana)
7 | * 4. If you forgot to do step 3, do step 3 OR run the solana util
8 | * `node utils/metaplex.js`
9 | *
10 | * Credits:
11 | * Original Metaplex.js util by https://github.com/DawidAbram
12 | */
13 |
14 | const NFTName = "NameOfNFT"; //This is the name there will be showen on your NFTs !!! Name can at max be 32 characters !!!
15 | const symbol = "PRJSMBL"; // !!! Symbol can at max be 10 characters !!!
16 |
17 | const baseUriPrefix = ""; // OPTIONAL, if you need to prefix your image#.png with a baseURI
18 | const description = "Default Solana Description";
19 | const external_url = ""; // add optional external URL here, e.g, https://0n10nDivision.com
20 |
21 | const royaltyFee = 200; // This is 2% royalty fee
22 |
23 | /**
24 | * Array of Creators.
25 | * If there is more than one creator, add additional objects with address and share properties.
26 | */
27 | const creators = [
28 | {
29 | address: "WALLET_ADDRESS", // Wallet address for royalties
30 | share: 100, // Amount of shares for this wallet, can be more than one, all have to add up to 100 together !!! And a maximum of 4 creators !!!
31 | },
32 | // uncomment and edit for additional creator.
33 | // {
34 | // address: "second wallet address here",
35 | // share: 100,
36 | // },
37 | ];
38 |
39 | /**
40 | * Only change this if you need to generate data for video/VR/3d content
41 | */
42 | const propertyCategory = "image";
43 |
44 | module.exports = {
45 | symbol,
46 | NFTName,
47 | description,
48 | royaltyFee,
49 | creators,
50 | external_url,
51 | baseUriPrefix,
52 | propertyCategory,
53 | };
54 |
--------------------------------------------------------------------------------
/Tezos/tezos_config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Config for generating NFTs that supports tezos standard
3 | */
4 |
5 | const path = require("path");
6 | const isLocal = typeof process.pkg === "undefined";
7 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
8 |
9 | const { format } = require(path.join(basePath, "src/config.js"));
10 |
11 | const NFTName = "NameOfNFT"; //This is the name there will be showen on your NFTs !!! Name can at max be 32 characters !!!
12 | const collectionName = "PROJECT_NAME"; //This is used if mutiple collection is needed
13 | const collectionFamily = "PROJECT_FAMILY"; // Many projects can belong to one family
14 | const symbol = "PRJSMBL"; // !!! Symbol can at max be 10 characters !!!
15 |
16 | const baseUriPrefix = ""; // OPTIONAL, if you need to prefix your image#.png with a baseURI
17 | const baseDisplayUri = "BASE_DISPLAY_URI";
18 | const baseThumbnailUri = "BASE_THUMBNAIL_URI";
19 |
20 | const description = "Default Solana Description";
21 | const external_url = ""; // add optional external URL here, e.g, https://0n10nDivision.com
22 |
23 | /**
24 | * How to write the % here ?
25 | * Royalties and royalty-splits should be defined in the token-metadata with the following format:
26 |
27 | ```json
28 | {
29 | [...],
30 | "royalties": {
31 | "decimals": 3,
32 | "shares": {
33 | "tz1h3rQ8wBxFd8L9B3d7Jhaawu6Z568XU3xY": 50,
34 | "tz1eY5Aqa1kXDFoiebL28emyXFoneAoVg1zh": 25
35 | }
36 | },
37 | [...]
38 | }
39 | ```
40 | > This example defines two royalty recipients with `tz1h3rQ8wBxFd8L9B3d7Jhaawu6Z568XU3xY` @ 5% and `tz1eY5Aqa1kXDFoiebL28emyXFoneAoVg1zh` @ 2.5%.
41 | > The `"decimals"` field defines the position of the decimal point: `305` with `4` decimals would mean `305 * 10^-4 = 0.0305 = 3.05%`.
42 |
43 | */
44 |
45 | const royalties = {
46 | tz1UxnruUqq2demYbAHsHkZ2VV95PV8MXVGq: 100, // 100 * 10 ^ -3 * 100 = 10%
47 | tz1WNKahMHz1bkuAZrsvtmjBhh4GJzw8YcUZ: 100, // 100 * 10 ^ -3 * 100 = 10%
48 | };
49 | const isBooleanAmount = true;
50 | const shouldPreferSymbol = false;
51 | const decimals = 0;
52 | const rights = "All right reserved.";
53 |
54 | const creators = ["@vivekascoder"];
55 |
56 | const reduceByFraction = (obj, fraction) => ({
57 | width: parseInt(obj.width * fraction),
58 | height: parseInt(obj.height * fraction),
59 | });
60 |
61 | const size = {
62 | artifactUri: format,
63 | displayUri: reduceByFraction(format, 0.75),
64 | thumbnailUri: reduceByFraction(format, 0.5),
65 | };
66 |
67 | /**
68 | * Only change this if you need to generate data for video/VR/3d content
69 | */
70 | const propertyCategory = "image";
71 |
72 | module.exports = {
73 | symbol,
74 | NFTName,
75 | collectionName,
76 | collectionFamily,
77 | description,
78 | creators,
79 | external_url,
80 | baseUriPrefix,
81 | propertyCategory,
82 |
83 | rights,
84 | isBooleanAmount,
85 | shouldPreferSymbol,
86 | decimals,
87 | size,
88 | baseDisplayUri,
89 | baseThumbnailUri,
90 | royalties,
91 | };
92 |
--------------------------------------------------------------------------------
/Tezos/updateinfo.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const isLocal = typeof process.pkg === "undefined";
5 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
6 | const tezosConfig = require(path.join(basePath, "/Tezos/tezos_config.js"));
7 |
8 | const fs = require("fs");
9 |
10 | console.log(path.join(basePath, "/src/config.js"));
11 | const { baseUri, description } = require(path.join(basePath, "/src/config.js"));
12 |
13 | // read json data
14 | let rawdata = fs.readFileSync(`${basePath}/build/tezos/json/_metadata.json`);
15 | let data = JSON.parse(rawdata);
16 |
17 | /**
18 | * loop over each loaded item, modify the data, and overwrite
19 | * the existing files.
20 | *
21 | * uses item.edition to ensure the proper number is used
22 | * insead of the loop index as images may have a different order.
23 | */
24 | const stringifySize = (obj) => {
25 | return `${obj.width}x${obj.height}`;
26 | };
27 | data.forEach((item) => {
28 | // item.image = `${baseUri}/${item.edition}.png`;
29 | item.artifactUri = `${baseUri}/${item.edition}.png`;
30 | item.displayUri = `${tezosConfig.baseDisplayUri}/${item.edition}.png`;
31 | item.thumbnailUri = `${tezosConfig.baseThumbnailUri}/${item.edition}.png`;
32 | item.description = description;
33 |
34 | item.formats = [
35 | {
36 | mimeType: "image/png",
37 | uri: `${baseUri}/${item.edition}.png`,
38 | dimensions: {
39 | value: stringifySize(tezosConfig.size.artifactUri),
40 | unit: "px",
41 | },
42 | },
43 | {
44 | mimeType: "image/png",
45 | uri: `${tezosConfig.baseDisplayUri}/${item.edition}.png`,
46 | dimensions: {
47 | value: stringifySize(tezosConfig.size.displayUri),
48 | unit: "px",
49 | },
50 | },
51 | {
52 | mimeType: "image/png",
53 | uri: `${tezosConfig.baseThumbnailUri}/${item.edition}.png`,
54 | dimensions: {
55 | value: stringifySize(tezosConfig.size.thumbnailUri),
56 | unit: "px",
57 | },
58 | },
59 | ];
60 | // ✨ if you would like to rename all the names, add a prefix here and
61 | // enable the following line
62 | // item.name = `PREFIX #${item.edition}`;
63 | item.royalties = {
64 | decimals: 3,
65 | shares: tezosConfig.royalties,
66 | };
67 |
68 | fs.writeFileSync(
69 | `${basePath}/build/tezos/json/${item.edition}.json`,
70 | JSON.stringify(item, null, 2)
71 | );
72 | });
73 |
74 | fs.writeFileSync(
75 | `${basePath}/build/json/_metadata.json`,
76 | JSON.stringify(data, null, 2)
77 | );
78 |
79 | console.log(`Updated baseUri for images to ===> ${baseUri}`);
80 | console.log(
81 | `Updated Royalties for images to ===> ${JSON.stringify(
82 | tezosConfig.royalties,
83 | null,
84 | 2
85 | )}`
86 | );
87 | console.log(
88 | `Updated displayUri for images to ===> ${tezosConfig.baseDisplayUri}`
89 | );
90 | console.log(
91 | `Updated thumbnailUri for images to ===> ${tezosConfig.baseThumbnailUri}`
92 | );
93 | console.log(`Updated Description for all to ===> ${description}`);
94 |
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Branching result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/Branching result.png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Branching with nested folders (1) (1) (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/Branching with nested folders (1) (1) (1).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Branching with nested folders (1) (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/Branching with nested folders (1) (1).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Branching with nested folders (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/Branching with nested folders (1).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/Branching with nested folders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/Branching with nested folders.png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/image (1) (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/image (1) (1).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/image (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/image (1).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/image (2).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/image (2).png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/image.png
--------------------------------------------------------------------------------
/documentation/.gitbook/assets/z-index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/documentation/.gitbook/assets/z-index.png
--------------------------------------------------------------------------------
/documentation/README.md:
--------------------------------------------------------------------------------
1 | # Art Engine
2 |
3 | ### Under development: for more documentation, see the readme.md
4 |
5 | ### GitHub Generator Repo: [https://github.com/nftchef/hashlips\_art\_engine/tree/nested-folder-structure](https://github.com/nftchef/hashlips\_art\_engine/tree/nested-folder-structure)
6 |
7 | This art engine is based on and continues to use the interface and setup from the Hashlips Art Engine.
8 |
9 | This art generator (art engine) has a number of advanced features to help build more complicated sets of generated art from transparent .png trait layers. The core feature is to support branching generation (illustrated below) using nested folders to control the if/then flow of how the generator chooses each layer.
10 |
11 | This generator is only for static images.
12 |
13 | 
14 |
15 | 
16 |
17 | \
18 |
--------------------------------------------------------------------------------
/documentation/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [Art Engine](README.md)
4 | * [Quickstart](readme/quickstart.md)
5 | * [Branching (IF/Then)](readme/branching-if-then.md)
6 | * [Extending a collection](readme/extending-a-collection.md)
7 | * [z-index (layer order)](readme/z-index-layer-order.md)
8 | * [Incompaticble/Forced combinations](readme/inclusions-and-exclusions.md)
9 | * [Utils](utils/README.md)
10 | * [Metadata](utils/metadata.md)
11 | * [Rarity](utils/rarity.md)
12 | * [Stats](utils/stats.md)
13 | * [Regenerate](utils/regenerate.md)
14 | * [Replace (for rares)](utils/replace-for-rares.md)
15 |
16 | ## Other Blockchains
17 |
18 | * [Solana](other-blockchains/solana.md)
19 | * [Cardano](other-blockchains/cardano.md)
20 | * [Tezos](other-blockchains/tezos.md)
21 |
--------------------------------------------------------------------------------
/documentation/other-blockchains/cardano.md:
--------------------------------------------------------------------------------
1 | # Cardano
2 |
3 | `yarn generate:cardano`
4 |
5 | details coming soon, see the readme.md
6 |
--------------------------------------------------------------------------------
/documentation/other-blockchains/solana.md:
--------------------------------------------------------------------------------
1 | # Solana
2 |
3 | If you are building for Solana, all the image generation options in config.js are available and are the same.
4 |
5 | ⚠️ Solana should always use a `startIndex` of `0`
6 |
7 | ### To set up your Solana specific metadata
8 |
9 | Configure the `solona_config.js` file located in the `Solana/` folder. Here, enter in all the necessary information for your collection.
10 |
11 | You can run the generator AND output Solana data by running the following command from your terminal
12 |
13 | ```
14 | yarn generate:solana
15 | ```
16 |
17 | If you are using npm,
18 |
19 | ```
20 | npm run generate:solana
21 | ```
22 |
23 | **After running, your Solana ready files will be in `build/solana`**\
24 | \
25 |
26 | If you need to convert existing images/json to solana metadata standards, you can run the util by itself with
27 |
28 | ```
29 | node utils/metaplex.js
30 | ```
31 |
32 | ⚠️ The Solana (metaplex) util only supports images generateed with a `startIndex` of `0` or `1`.
33 |
34 | ##
35 |
36 | \
37 |
--------------------------------------------------------------------------------
/documentation/other-blockchains/tezos.md:
--------------------------------------------------------------------------------
1 | # Tezos
2 |
3 | 🧪 BETA FEATURE
4 |
5 | Since tezos need some extra config information, in order to change config for tezos specific metadata you can look into `./Tezos/tezos_config.js`.
6 |
7 | ### To set up your Tezos specific metadata.
8 |
9 | Configure the `tezos_config.js` file located in the `Tezos/` folder. Here, enter in all the necessary information for your collection.
10 |
11 | You can run the generator AND output Solana data by running the following command from your terminal
12 |
13 | ```
14 | yarn generate:tezos
15 | ```
16 |
17 | If you are using npm,
18 |
19 | ```
20 | npm run generate:tezos
21 | ```
22 |
23 | **After running, your Tezos ready files will be in `build/tezos`**
24 |
25 |
26 | To generate the resized images for thumbnailUri and displayUri run the following command.
27 |
28 | ```
29 | yarn resize
30 | ```
31 |
32 | If you are using npm,
33 |
34 | ```
35 | npm run resize
36 | ```
37 |
38 | Now you'll get two more folders under `build/` directory. Now you can upload all these three folders to IPFS namely `build/displayUri/`, `build/thumbnailUri/` and `build/images`. After that you can update the base IPFS uri for these three folders in `/Tezos/tezos_config.js`
39 |
40 | ```js
41 | const baseUriPrefix = "ipfs://BASE_ARTIFACT_URI";
42 | const baseDisplayUri = "ipfs://BASE_DISPLAY_URI";
43 | const baseThumbnailUri = "ipfs://BASE_THUMBNAIL_URI";
44 | ```
45 |
46 |
47 | Then to update the generated metadata with these base uris run the following command.
48 |
49 | ```
50 | yarn update_info:tezos
51 | ```
52 |
53 | If you are using npm,
54 |
55 | ```
56 | npm run update_info:tezos
57 | ```
58 |
59 | That's it you're ready for launch of your NFT project.
--------------------------------------------------------------------------------
/documentation/readme/branching-if-then.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Understanding and setting up conditional branching with nested folders.
3 | ---
4 |
5 | # Branching (IF/Then)
6 |
7 |  (1) (1).png>)
8 |
9 | 
10 |
11 | ## Branching using Nested Layers
12 |
13 |
14 |
15 | ### Example
16 |
17 | The following example (included in this repository) uses multiple `layer_configurations` in `config.js` to generate male and female characters, as follows.
18 |
19 | ```javascript
20 | const layerConfigurations = [
21 | {
22 | growEditionSizeTo: 2,
23 | layersOrder: [
24 | { name: "Background" },
25 | { name: "Female Hair", trait: "Hair" },
26 | ],
27 | },
28 | {
29 | growEditionSizeTo: 5,
30 | layersOrder: [
31 | { name: "Background" },
32 | { name: "Eyeball" },
33 | { name: "Male Hair", trait: "Hair" },
34 | ],
35 | },
36 | ];
37 | ```
38 |
39 | The Hair layers, exist as their own layers in the `layers` directory and use the `trait` key/property to overwrite the output metadata to always look like, the following, regardless of layer folder it is using–so both Male and Female art have a `Hair` trait.
40 |
41 | ```js
42 | {
43 | "trait_type": "Hair",
44 | "value": "Rainbow Ombre"
45 | }
46 | ```
47 |
48 | ### Nesting structure
49 |
50 | In this modified repository, nesting subdirectories is supported and each directory **can** have it's own rarity weight WITH nested weights inside for individual PNG's.
51 |
52 | 
53 |
54 | For the example above, `Female Hair` can be read as:
55 |
56 | > Female Hair layer is required from config -> Randomly select either `common` or `rare` with a respective chance of `70% / 30%`. If Common is chosen, randomly pick between Dark Long (20% chance) or Dark Short (20%)
57 |
58 | ### Advanced options
59 |
60 | #### Required files
61 |
62 | Additionally, `png` files that ommit a rarity weight **will be included** always and are considered "required".
63 |
64 | This means, that if you need multiple images to construct a single "trait", e.g., lines layer and fill layer, you could do the following:
65 |
66 | ```
67 | HAIR
68 | |-- Special Hair#10
69 | |----- 1-line-layer.png
70 | |----- 2-fill-layer.png
71 | ```
72 |
73 | Where the containing folder will define the traits _rarity_ and in the event that it is selected as part of the randomization, BOTH nested images will be included in the final result, in alphabetical oder–hence the 1, 2, numbering.
74 |
--------------------------------------------------------------------------------
/documentation/readme/extending-a-collection.md:
--------------------------------------------------------------------------------
1 | # Extending a collection
2 |
3 | In some cases, you may want to continue building a collection after the initial generation and start the token number at 10,001, for example. If, while extending the collection you also need to taking into consideration the original collection and **prevent duplicates,** please follow the steps below:
4 |
5 |
6 |
7 | ### The \_dna.json file is required
8 |
9 | The generator automatically outputs a `_dna.json` file every time it is run. this file is **required to extend a collection.** If you did not save it, you will not be able to continue generation while taking into consideration the DNA from a previous generation.\
10 | \
11 | ⚠️ The \_dna.json file, along with the entire build directory is deleted and overwritten each time the generator is run. **Make sure you rename the folder and save it someplace safe!**
12 |
13 |
14 |
15 | Rename and save the DNA file from the first build file. Here, I have renamed the build folder to `_series1` and the DNA file to `_series1_dna.json`
16 |
17 | .png>)
18 |
19 | Once you have the DNA file saved, you can configure the config.js as normal, setting the start index to the number you like.
20 |
21 | 
22 |
23 | Once configured, run the generator with the `--continue` flag and pass in the (relative) path to the series 1 dna file.
24 |
25 | ```
26 | yarn generate --continue ./_series1/_dna.json
27 |
28 | // or with npm
29 | // npm run generate --continue ./s1/_dna.json
30 | ```
31 |
32 | When the generation is finished, the resulting DNA file will contain all DNA used in _both_ (all) generated builds.
33 |
34 | .png>)
35 |
36 | **Be sure to save the build file by renaming it.**
37 |
--------------------------------------------------------------------------------
/documentation/readme/inclusions-and-exclusions.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Override randomness and force inclusion or exclusion of trait combinations
3 | ---
4 |
5 | # Prevent or enforce trait combinations
6 |
7 | **For edge cases only** Nested Folders (conditional branching) should always be used first and can solve 90% of "if this, then _not_ that" use cases. However, for the 10% of valid edge cases where you need to either prevent art from being used together or force art to be used together, you can use the following `incompatible` or `forcedCombination` objects in config.js
8 |
9 | ## Flagging Incompatible layers
10 |
11 | To flag certain images that _should never be used with_ another image, for this, you can use the incompatible configuration in `config.js`
12 |
13 | To set incompatible items, in the `incompatible` object, use the layer/images `cleanName` (the name without rarity weight, or .png extension) as a key, and create an array of incompatible layer names (again, clean names). Layers that have space or hyphens in their names should be wrapped in quotes
14 |
15 | ⚠️ NOTE: Names are expected to be unique. if you have multiple files with the same name, you may accidentally exclude those images.
16 |
17 | ```js
18 | const incompatible = {
19 | //key: [array of names]
20 | Red: ["Dark Long"],
21 | "Name with spaces": ["buzz","rare-Pink-Pompadour" ]
22 | // conditional #weighted directory incompatible with another # weighted directory example
23 | White: ["rare-Pink-Pompadour"],
24 | };
25 | ```
26 |
27 | ⚠️ NOTE: This relies on the layer order to set incompatible DNA sets. For example the key should be the image/layer that comes first (from top to bottom) in the layerConfiguration. in other words, IF the item (KEY) is chosen, then, the generator will know not to pick any of the items in the `[Array]` that it lists.
28 |
29 | ## Forced Combinations
30 |
31 | 
32 |
33 | When you need to force/require two images to work together that are in different root-layer-folders (the layer config), you can use the `forcedCombinations` object in `config.js`.
34 |
35 | ```js
36 | const forcedCombinations = {
37 | floral: ["MetallicShades", "Golden Sakura"],
38 | };
39 | ```
40 |
41 | Using the _clean Name_ (file or folder name without weight and .png extension), specify the key (if names have spaces, use quotes `"file name" :`)
42 |
43 | Then, create an array of names that should be required.
44 |
45 | Note: the layer order matters here. The key (name on the left) should be a file withing a layer that comes first in the `layersOrder` configuration, then, files that are required (in the array), should be files in layers _afterward_.
46 |
47 | ### Force combination with special items
48 | If you would like to have a trait that ONLY appears in forced combinations, and is never picked randomly:
49 |
50 | Set the folder/image weight to `#0` in your layers folder,
51 | then configure the forced combinations as usual.
52 |
53 | ## using with z-index
54 |
55 | Incompatible and forced combinations run _after_ the z-index modifies the layer ordering and can lead to unexpexted output when setting up the `key:[values]` pairs. The `key` should always be the item that comes first, after the z-index has been applied.
56 |
--------------------------------------------------------------------------------
/documentation/readme/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quickstart
2 |
3 | It is recommended to start by running the generator using the sample art to be sure everything is installed correctly without any node errors.
4 |
5 | ### Prerequisites
6 |
7 | [Node.js ](https://nodejs.org)v14 or higher (16 is recommended)
8 |
9 | NPM or Yarn
10 |
11 |
12 |
13 | ### Installation
14 |
15 | ```
16 | // yarn
17 | yarn
18 |
19 | // npm
20 | npm install
21 | ```
22 |
23 | 💡Note for M1 Macs. You may run into node-gpy or node-canvas errors during installation. If you do, first delete the yarn.lock and/or package.lock files, then try installing agin. For Canvas issues when running the generator, run `yarn add canvas` or `npm install --save-dev canvas` to install the m1 version.
24 |
25 |
26 |
27 | ### Running the generator
28 |
29 | The generator by default will build from the `layers` directory. To build the sample art, run
30 |
31 | ```javascript
32 | // yarn
33 | yarn generate
34 |
35 | // npm
36 | npm run generate
37 | ```
38 |
39 | If you see a new `build/` directory with images, json, and \_dna.json 🎉 congrats! everything is working!!
40 |
41 |
42 |
43 | ## Basic Setup
44 |
45 | To use your own artwork, replace everything with your own folders inside the `layers` folder. Then, configure the order of your layers and the number you would like to create in `./src/config.js`
46 |
47 | Each `{}` _Object_ should be the name of your layer folder, matched _exactly_
48 |
49 | ```
50 | const layerConfigurations = [
51 | {
52 | growEditionSizeTo: 10,
53 | namePrefix: "Series 2", // Use to add a name to Metadata `name:`
54 | layersOrder: [
55 | { name: "Background" },
56 | { name: "Head" },
57 | { name: "Body" },
58 | { name: "Clothes" },
59 | { name: "Eye" },
60 | ],
61 | },
62 | ```
63 |
64 | ## Configuration Options
65 |
66 | ### Output JPEG
67 |
68 | `const outputJPEG`
69 |
70 | By default, the generator outputs PNG images. setting to `true` will generate JPEG's.
71 |
72 | ### Start Index
73 |
74 | ```
75 | const startIndex = 0;
76 | ```
77 |
78 | By default, the generator will generate images starting at 0. Use this option to change the number the generator starts from.
79 |
80 | ⚠️ Be sure it matches your smart contract!
81 |
82 | ### Format (output size)
83 |
84 | ```javascript
85 | const format = {
86 | width: 512,
87 | height: 512,
88 | smoothing: true, // set to false when up-scaling pixel art.
89 | };
90 | ```
91 |
92 | Set the width and height of the generated image. Be sure that this matches the aspect ratio of the input layers from the `layers/` folder. For the highest quality results, the output should match the input file size to avoid scaling.
93 |
94 | #### `smoothing`
95 |
96 | When the input art is small (pixel art for example) and you would like to scale it while retaining the sharpness, turn this off with, `smoothing: false`
97 |
98 | ###
99 |
--------------------------------------------------------------------------------
/documentation/readme/z-index-layer-order.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Fine grain control over the stacking order of your artwork layers
3 | ---
4 |
5 | # z-index (layer order)
6 |
7 | ## Controlling stacking order
8 |
9 | 
10 |
11 | ### Basic organization
12 |
13 | Generally, layer order is defined by using `layersOrder` in config.js. However, when using nested folders, layer order defaults to alphanumeric sorting (0-9,a-z)
14 |
15 | To manually define stacking order for sublayers (nested folders) use the `z#,` (`z` followed by a positive or negative number, followed bu a `,` comma) prefix for folder names and/or file names.
16 |
17 | Example folders _inside_ a root folder.
18 |
19 | ⚠️ **z-index does not work on "Root Layers", the layers listed in the `layersOrder` inside config.js**
20 |
21 | ⚠️ **If you are not seeing your negative z-index layers, be sure you do not have a background layer covering them! you may need to set backgrounds to a lower negative number.**
22 |
23 | ```
24 | |- z-1,under layer
25 | |-normal layer/
26 | |-normal layer two/
27 | |-- z3,OptionAlways On Top#50.png
28 | |-z2,layer two/
29 | ```
30 |
31 |  (1).png>)
32 |
33 | Folders and files without a `z#,` prefix default to a z-index of `0`
34 |
35 | If a file has a `z#,` prefix, and its parent folder(s) does as well, the **files z-index will be used,** allowing you to overwrite and define an individual's layer order.
36 |
37 | Layer order z-indexes are "global" meaning the index is relative to ALL OTHER z-indices defined in every folder/file
38 |
39 | ### Multiple layers for a single trait
40 |
41 | In the case where a single trait needs to have one part of the image on top, and another layered below (see the Hair Example in the illustated diagram at the top of this document), use weighted subfolders with required folders inside.
42 |
43 | The result will be the Hair, Front.png being stacked _normally,_ while Back.png will be send below all other layers (higher that negative one).
44 |
45 | .png>)
46 |
47 | ## Advanced Usage
48 |
49 | There are many cases where simple layers result in a tangle of options and incompatible layers Nested Layers and z-Index features of this application have all the answers that are needed.
50 |
51 | Let's set up a simple scenario that will serve as the basis for understanding.
52 |
53 | #### The Example Project Concept
54 |
55 | Let's say we are building a basic 4-layers:
56 |
57 | 1. Background
58 | 2. Body
59 | 3. Hair
60 | 4. (optional) Hat
61 |
62 | The Body has 2 basic options - each with 2 skin tones. So, in total, we have 4 possible Body options:
63 |
64 | 1. `Female 1`
65 | 2. `Female 2`
66 | 3. `Male 1`
67 | 4. `Male 2`
68 |
69 | Futhermore, let's assume we have multiple hairstyles. The hairstyles are not shared between male/female, but are shared between both skin tones (eg, `Male Hair 1` can be on `Male 1` or `Male 2`, but can't be on any female)
70 |
71 | Lastly, certain hats are only compatible with certain hairstyles. For this scenario, let's assume:
72 |
73 | * `Male Hat 1` and `Male Hat 2` work with `Male Hair 1`
74 | * `Male Hat 2` works with `Male Hair 2`
75 | * There should be a chance for NO hat to be selected on either hair
76 |
77 | For the sake of brevity, we will not consider hat options for the Females
78 |
79 | #### Jump to the End Result
80 |
81 | For those that like seeing the end result first, here is the final tree structure that supports this scenario. We will dig in section by section to further understand.
82 |
83 | **NOTE:** The filenames and directories used here are not using "friendly" names and will either need to be overridden or changed to make the Metadata JSON files look right.
84 |
85 | #### Contents of `layers`
86 |
87 | ```
88 | .
89 | ├── background
90 | │ ├── bg1#10.png
91 | │ └── bg2#10.png
92 | └── type
93 | ├── female#10
94 | │ ├── female.png
95 | │ └── hair
96 | │ └── TRUNCATED_FOR_TUTORIAL
97 | └── male#10
98 | ├── male-1#10.png
99 | ├── male-2#10.png
100 | └── hair
101 | ├── hair-male-1#10
102 | │ ├── hair-male-1.png
103 | │ └── hat
104 | │ ├── NONE#10.png
105 | │ ├── hat-male-1#10.png
106 | │ └── hat-male-2#10.png
107 | ├── hair-male-2#10
108 | │ └── hat
109 | │ ├── NONE#10.png
110 | │ └── hat-male-3#10.png
111 | └── hair-male-3#10
112 | ```
113 |
114 | #### Value of `layerConfigurations`
115 |
116 | ```javascript
117 | const layerConfigurations = [
118 | {
119 | growEditionSizeTo: 10,
120 | layersOrder: [
121 | {name: "background"},
122 | {name: "types"},
123 | ],
124 | },
125 | ]
126 | ```
127 |
128 | **NOTE:** At the time of writing, the `trait` option is not supported on the root folder (eg, `types`). It will cause all nested traits to roll up to a single trait name, which isn't desirable.
129 |
130 | #### Breaking It Down
131 |
132 | Inside of any given folder, the following actions are taken:
133 |
134 | 1. Look at all weighted (suffix `#xx`) items (folders AND files) - Make a selection based on weight then recursively, if applicable, dive down any nested structures.
135 | 2. Any unweighted items are processed. In the case of png files, all of them are applied in alphabetical order. In the case of folders, they are recursed, also in alpha-order.
136 |
137 | In practice with this example, the following is the order of processing inside of `types`:
138 |
139 | 1. Select Male/Female (50/50)
140 | 2. Assuming Male, then choose between `male-1` and `male-2` (50/50)
141 | 3. Assuming `male-2`, that layer is selected
142 | 4. Next, open `hair` and select from one of 3 weighted hairs
143 | 5. Assuming `hair-male-2`, that layer is selected
144 | 6. Next, open `hats` (the one inside `hair-male-2#10` folder) and select from none or `hat-male-3` (50/50)
145 | 7. Assuming `hat-male-3`, that layer is selected.
146 |
147 | The final result of this is a male, with hair 2 and hat 3.
148 |
149 | #### But there is a problem with the metadata
150 |
151 | If you look at that metadata, you will see something like this:
152 |
153 | ```json
154 | "attributes": [
155 | {
156 | "trait_type": "Background",
157 | "value": "bg1"
158 | },
159 | {
160 | "trait_type": "type",
161 | "value": "male-1"
162 | },
163 | {
164 | "trait_type": "hair",
165 | "value": "hair-male-2"
166 | },
167 | {
168 | "trait_type": "hat",
169 | "value": "hat-male-3"
170 | },
171 | ],
172 | ```
173 |
174 | There are two problems here:
175 |
176 | 1. The `trait_type` should probably be `Hair` instead of `hair`
177 | 2. The `value` should be `Cool Hat` instead of `hat-male-3`
178 |
179 | The `trait_type` issue can also be solved in one of two ways:
180 |
181 | 1. Change the file and folder names to be exactly what you want to be used for traits and values
182 | 2. You can use `sublayerOptions` to modify the names like below:
183 |
184 | ```js
185 | const layerConfigurations = [
186 | {
187 | growEditionSizeTo: 20,
188 | layersOrder: [
189 | {name: "background", trait: "Background"},
190 | {
191 | name: "type",
192 | sublayerOptions: {
193 | "hair": {trait: "Hair"},
194 | "hat": {trait: "Hat"}
195 | }
196 | },
197 | ],
198 | },
199 | ]
200 | ```
201 |
202 | **NOTE:** Due to the issue of `trait` name changes not being supported at the root of nested folders, it will be best to change the folder name from `type` to `Type`, assuming you want it to be capitalized.
203 |
204 | The `value` issue can be solved one of two ways:
205 |
206 | 1. Change the file and folder names to be exactly what you want to be used for traits and values
207 | 2. Add `traitValueOverrides` like below
208 |
209 | ```js
210 | const traitValueOverrides = {
211 | "male-1": "Male Skin1",
212 | "male-2": "Male Skin2",
213 | "hat-male-3": "Cool Hat",
214 | "hair-male-2": "Mohawk",
215 | [etc, etc etc]
216 | };
217 | ```
218 |
219 | #### Adding a bit more complexity using z-index
220 |
221 | This system is great when items are generally stacked on top of each other in a linear fashion. But let's say, `hat-male-3` is actually made up of two layers. One that needs to go on top of the hair and one that needs to go behind the base body.
222 |
223 | The good news is that z-index overrides are extremely powerful. You can prepend `z[offset],` on any file or folder to change how it stacks up. Numbers can be either positive or negative and are expressed like:
224 |
225 | * `z2,sometrait`
226 | * `z-1,someothertrait`
227 |
228 | Note that Z-Index items can still have
229 |
230 | Let's look at the final structure first
231 |
232 | ```
233 | .
234 | ├── background
235 | │ ├── bg1#10.png
236 | │ └── bg2#10.png
237 | └── type
238 | ├── z1,female#10
239 | │ ├── female.png
240 | │ └── hair
241 | │ └── TRUNCATED_FOR_TUTORIAL
242 | └── z1,male#10
243 | ├── male-1#10.png
244 | ├── male-2#10.png
245 | └── hair
246 | ├── hair-male-1#10
247 | │ ├── hair-male-1.png
248 | │ └── hat
249 | │ ├── NONE#10.png
250 | │ ├── hat-male-1#10.png
251 | │ └── hat-male-2#10.png
252 | ├── hair-male-2#10
253 | │ └── hat
254 | │ ├── NONE#10.png
255 | │ └── hat-male-3#10
256 | │ └── hat-male-3-front.png
257 | │ └── z0,hat-male-3-back.png
258 | └── hair-male-3#10
259 | ```
260 |
261 | Z-index is global, and defaults to `0` at the root of a given nested tree. In this case, `type` is `0` unless otherwise specified. The layers are then added where the deeper layers are on top of the more shallow layers (eg type -> hair -> hat).
262 |
263 | If any folder has a z-index specified, all of the children folders/files will inherit that unless explicitly set. For example, if you change the name of `hair` to `z-1,hair`, the hair and the hat will be inheriting `z-1`
264 |
265 | By setting the root folders `male#10` and `female#10` to `z1,male#10` and `z1,female#10`, we have "lifted" the base model from the background and can slip in the `hat-male-3-back.png` by modifying that to `z0,hat-male-3-back.png`.
266 |
267 | **HOWEVER,** If you run this you will see that the back part of the hat will be behind the background. This can be solved by either moving everything up, or the background back.
268 |
269 | To move the background back, simply change prepend a negative z-index like `z-5,` to the name of the background files - eg, `z-5bg#10.png`
270 |
271 | I choose moving everything up. So the final folder structure is:
272 |
273 | ```
274 | .
275 | ├── background
276 | │ ├── bg1#10.png
277 | │ └── bg2#10.png
278 | └── type
279 | ├── z2,female#10
280 | │ ├── female.png
281 | │ └── hair
282 | │ └── TRUNCATED_FOR_TUTORIAL
283 | └── z2,male#10
284 | ├── male-1#10.png
285 | ├── male-2#10.png
286 | └── hair
287 | ├── hair-male-1#10
288 | │ ├── hair-male-1.png
289 | │ └── hat
290 | │ ├── NONE#10.png
291 | │ ├── hat-male-1#10.png
292 | │ └── hat-male-2#10.png
293 | ├── hair-male-2#10
294 | │ └── hat
295 | │ ├── NONE#10.png
296 | │ └── hat-male-3#10
297 | │ └── hat-male-3-front.png
298 | │ └── z1,hat-male-3-back.png
299 | └── hair-male-3#10
300 | ```
301 |
302 | ## Important things to remember / Best Practices:
303 |
304 | * ALL weighted items go into the selection. If you always want something included, make sure there is no weight.
305 | * Depending on your folder structure and how you want the metadata to name the `trait_type`, you may need to toggle `const useRootTraitType = true;` to `false`
306 | * Even though z-index information was the last included in here, being explicit about z-index might be considered a best practice as it is self-documenting
307 | * As you are developing out your layers, if the taxonomy is not clear from the beginning, it could be wise to do a wide range of z-index so that you can easily slip items into layers between others.
308 | * Layer order z-indexes are "global" meaning the index is relative to ALL OTHER z-indices defined in every folder/file
309 |
--------------------------------------------------------------------------------
/documentation/utils/README.md:
--------------------------------------------------------------------------------
1 | # Utils
2 |
3 |
--------------------------------------------------------------------------------
/documentation/utils/metadata.md:
--------------------------------------------------------------------------------
1 | # Metadata
2 |
3 | ## Metadata Utility scripts
4 |
5 | Available util scripts
6 |
7 | * Update Metadata
8 | * Regenreate \_metadata.json
9 | * Remove trait
10 | * Provenance Hash generation
11 |
12 | ## Update metadata
13 |
14 | ```bash
15 | yarn update:metadata
16 |
17 | // if using npm
18 | // npm run update:metadata
19 | ```
20 |
21 | Be sure to back up a copy of the build directory before running any scripts. The update metadata script will regenerate all the .json files in the /json directory, including `_metadata.json`.
22 |
23 | This script will overwrite the metadata fileds in each .json file with the options in `config.js` including
24 |
25 | * Name (optional)
26 | * description
27 | * baseUri
28 |
29 | With optional flags, it can also be used to **clean** the metadata by removing a field or removing a single trait from the attributes.
30 |
31 | ## Options
32 |
33 | For a list of all available flags, run
34 |
35 | ```bash
36 | node utils/updateInfo.js --help
37 |
38 | ```
39 |
40 | ⚠️ NOTE: This script relies on the "edition" property to reliably set the numbers and file names.
41 |
42 | ### Set a new "name" `--name `
43 |
44 | Run the command and pass a new name prefix in using the `--name` flag.
45 |
46 | ⚠️ Currently this only supports renaming ALL files using the same prefix. It does not work for multiple layer configurtions that use different name prefix's.
47 |
48 | Example:
49 |
50 | ```bash
51 | yarn update:metadata -n "Frosty Fish"
52 | ```
53 |
54 | The result would be
55 |
56 | ```
57 | "name": "Frosty Fish #4"
58 | ...
59 | ```
60 |
61 | ### Remove field `--skip `
62 |
63 | Used when you need to remove one of the top level fields from the output metadata, e.g., `"dna"`, `"edition"`, `"date"`.
64 |
65 | ```bash
66 | yarn update:metadata --skip "dna"
67 |
68 | // if using npm
69 | // npm run update:metadata --skip "dna"
70 | ```
71 |
72 | Only a single filed is removable per run of the script.
73 |
74 | ### Remove Trait `--removeTrait `
75 |
76 | Used when you need to completely remove a attribute `trait_type` from all files, e.g., `trait_type: "Body"`.
77 |
78 | ```bash
79 | yarn update:metadata --removeTrait "Body"
80 |
81 | // if using npm
82 | // npm run update:metadata --removeTrait "Body"
83 | ```
84 |
85 | Only a single attribute is removable per run of the script.
86 |
--------------------------------------------------------------------------------
/documentation/utils/rarity.md:
--------------------------------------------------------------------------------
1 | # Rarity
2 |
3 | .png>)
4 |
5 | Calculate the count and percentage of each trait and value that _was_ generated, and output `build/_rarity.csv` And, rank the tokens generated in rarity-order; output to `build/_ranking.csv`
6 |
7 | Running the rarity script will print out a the data to the terminal as well as gener
8 |
9 | #### Prerequisites:
10 |
11 | - Art must be generated
12 | - `_metadata.json` must be in `./build/json/`
13 |
14 | ### Usage
15 |
16 | ```
17 | yarn rarity
18 | ```
19 |
20 | #### if using npm
21 |
22 | ```
23 | npm run rarity
24 | ```
25 |
26 | ### Using the CSV file
27 |
28 | Open or import the .csv file into your preferred spreadsheet application (e.g., excel, google sheets) to view
29 |
--------------------------------------------------------------------------------
/documentation/utils/regenerate.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: >-
3 | How to use the regenerate utility to replace a single edition with a new
4 | randomly generated edition.
5 | ---
6 |
7 | # Regenerate
8 |
9 | Usage: `node utils/regenerate.js [options] `
10 |
11 | Options: `-d, --debug` display some debugging
12 |
13 |
14 |
15 | In the event that the generator outputs a small number of images that you do not like and you would like to _try again_, the `regenerate` utility will take all previously used \_dna into consideration and run the generator once to output a new image and metadata.
16 |
17 | ⚠️ This script modifies the `_dna.json` file used in a number of other utility scripts. After modifying `_dna.json` with this regenration script, you will lose the dna data for the item you are replacing. This is permanent.
18 |
19 | ### Example
20 |
21 | To regenerate/recreate item number 1, which will:\
22 | Replace: `build/images/1.png` + \`build/json/1.json\`\
23 | Modify: `build/json/_metadata.json` _+ `build/_dna.json`_\
24 | _``_
25 |
26 | ```
27 | node utils/regenerate.js -d 1
28 | ```
29 |
30 | 
31 |
32 |
--------------------------------------------------------------------------------
/documentation/utils/replace-for-rares.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: >-
3 | Randomly Insert Rare items - Replace Util If you would like to manually add
4 | 'hand drawn' or unique versions into the pool of generated items, this utility
5 | takes a source folder (of your new artwork)
6 | ---
7 |
8 | # Replace (for rares)
9 |
10 | ### Requirements
11 |
12 | * create a source directory with a `/images` and `/json` folder e.g. `/ultraRares`
13 | * Name images sequentially from 1.png/jpeg (order does not matter) and place them in the images folder.
14 | * Put matching, sequential JSON files in the `json` folder
15 |
16 | ⚠️ This step in the collection-building process (adding rares) should be done BEFORE generating a provenance hash. Each new, replaced image generates a new hash and is inserted into the metadata used for generating the provenance hash.
17 |
18 | ⚠️ This util requires the `build` directory to be complete (after generation)
19 |
20 | ⚠️ This script **DOES NOT MODIFY THE ORIGINAL \_dna.json** file. If using other utils like `rebuildAll` , The files replaced with this util will not be rebuilt.
21 |
22 | example:
23 |
24 | ```
25 | ├── ultraRares
26 | │ ├── images
27 | │ │ ├── 1.png
28 | │ │ └── 2.png
29 | │ └── json
30 | │ ├── 1.json
31 | │ └── 2.json
32 | ```
33 |
34 | **You must have matching JSON files for each of your images.**
35 |
36 | ### Setting up the JSON.
37 |
38 | Because this script randomizes which tokens to replace/place, _it is important_ to update the metadata properly with the resulting tokenId #.
39 |
40 | _**Everywhere**_** you need the edition number in the metadata should use the `##` identifier.**
41 |
42 | ```json
43 | "edition": "##",
44 | ```
45 |
46 | **Don't forget the image URI!**
47 |
48 | ```json
49 | "name": "## super rare sunburn ",
50 | "image": "ipfs://NewUriToReplace/##.png",
51 | "edition": "##",
52 | ```
53 |
54 | _if you need `#num` with the `#` in the name for example, use a different symbol other than `##`. see the --replacementSymbol flag below_
55 |
56 |
57 |
58 | ### Metadata
59 |
60 | ```
61 | {
62 | "name": "Legendary",
63 | "description": "any description...",
64 | "image": "ipfs://NewUriToReplace/##.png",
65 | "attributes": [
66 | {
67 | "trait_type": "Head",
68 | "value": "Sunburn"
69 | },
70 | {
71 | "trait_type": "Clothes",
72 | "value": "Shiny jacket"
73 | },
74 | {
75 | "trait_type": "Shirt Accessories",
76 | "value": "GOLDEN NECKLACE"
77 | }
78 | ],
79 | }
80 | ```
81 |
82 | 📝 Note: Not all fields are required in the ultra-rare metadata. Refer to the erc721 NFT metadata standards to see what fields you can include, [https://docs.opensea.io/docs/metadata-standards](https://docs.opensea.io/docs/metadata-standards)
83 |
84 | ### Running
85 |
86 | Run the script with the following command, passing in the source directory name, (relative to the current working dir)
87 |
88 | ```
89 | node utils/replace.js [Source Directory]
90 | ```
91 |
92 | example
93 |
94 | ```
95 | node utils/replace.js ./ultraRares
96 | ```
97 |
98 | ### Flags
99 |
100 | #### `--help`
101 |
102 | Outputs command help.
103 |
104 | #### `--Debug`
105 |
106 | `-d` outputs additional logging information
107 |
108 |
109 |
110 | **`--sneak`**
111 |
112 | `-s`outputs logging for which items were replaced. By default, the tokens are randomly inserted without reporting for fairness. Use for QA.
113 |
114 | `--replacementSymbol`
115 |
116 | If you need the output data to have `#12` for example, with the leading #, the default `##` in the metadata is a problem. use this flag in combination with a different symbol in the metadata json files to replace the passed-in symbol with the appropriate edition number
117 |
118 | ```
119 | node index.js ./ultraRares -r '@@'
120 | ```
121 |
122 | This will replace all instances of `@@` with the item number
123 |
124 | #### `--identifier`
125 |
126 | `-i` Change the default object identifier/location for the edition/id number. defaults to "edition". This is used when the metadata object does not have "edition" in the top level, but may have it nested in "properties", for example, you can use the following to locate the correct item in \_metadata.json
127 |
128 | ```
129 | node utils/replace.js ./ultraRares -i properties.edition
130 | ```
131 |
132 |
--------------------------------------------------------------------------------
/documentation/utils/stats.md:
--------------------------------------------------------------------------------
1 | # Stats
2 |
3 | ### 🧪In development
4 |
5 | Similar to rarity, with the main difference being `stats` will output the number of times an image file was used (not the ending attribute trait\_type & value) which gives you visibility into how the weight of each file affected the current run of the generator. \
6 | \
7 | This is very useful if you have a low weight set for a rare image and you want to be sure it is being used (not, zero)
8 |
9 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const isLocal = typeof process.pkg === "undefined";
5 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
6 |
7 | const fs = require("fs");
8 | const { Command } = require("commander");
9 | const program = new Command();
10 | const chalk = require("chalk");
11 |
12 | const { startCreating, buildSetup } = require(path.join(
13 | basePath,
14 | "/src/main.js"
15 | ));
16 |
17 | program
18 | .name("generate")
19 |
20 | .option("-c, --continue ", "Continues generatino using a _dna.json file")
21 | .action((options) => {
22 | console.log(chalk.green("genator started"), options.continue);
23 | options.continue
24 | ? console.log(
25 | chalk.bgCyanBright("\n continuing generation using _dna.json file \n")
26 | )
27 | : null;
28 | buildSetup();
29 | let dna = null;
30 | if (options.continue) {
31 | const storedGenomes = JSON.parse(fs.readFileSync(options.continue));
32 | dna = new Set(storedGenomes);
33 | console.log({ dna });
34 | }
35 |
36 | startCreating(dna);
37 | });
38 |
39 | program.parse();
40 |
--------------------------------------------------------------------------------
/layers/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/.DS_Store
--------------------------------------------------------------------------------
/layers/Accessory/Helmet#10/Helmet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Accessory/Helmet#10/Helmet.png
--------------------------------------------------------------------------------
/layers/Accessory/MetallicShades#1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Accessory/MetallicShades#1.png
--------------------------------------------------------------------------------
/layers/Accessory/NONE#100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Accessory/NONE#100.png
--------------------------------------------------------------------------------
/layers/Accessory/Purple Glasses#25/Glasses front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Accessory/Purple Glasses#25/Glasses front.png
--------------------------------------------------------------------------------
/layers/Accessory/Purple Glasses#25/z-10,Glasses back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Accessory/Purple Glasses#25/z-10,Glasses back.png
--------------------------------------------------------------------------------
/layers/Back Accessory/Backpack#50/1backpack-lines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Back Accessory/Backpack#50/1backpack-lines.png
--------------------------------------------------------------------------------
/layers/Back Accessory/Backpack#50/backpack-FILL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Back Accessory/Backpack#50/backpack-FILL.png
--------------------------------------------------------------------------------
/layers/Back Accessory/rockets#40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Back Accessory/rockets#40.png
--------------------------------------------------------------------------------
/layers/Background/coulds#5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Background/coulds#5.png
--------------------------------------------------------------------------------
/layers/Background/stars#5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Background/stars#5.png
--------------------------------------------------------------------------------
/layers/Background/swirl#5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Background/swirl#5.png
--------------------------------------------------------------------------------
/layers/Clothes/Shirt three#44/3shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/Shirt three#44/3shadow.png
--------------------------------------------------------------------------------
/layers/Clothes/Shirt three#44/shirt-FILL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/Shirt three#44/shirt-FILL.png
--------------------------------------------------------------------------------
/layers/Clothes/floral#30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/floral#30.png
--------------------------------------------------------------------------------
/layers/Clothes/gray tee#30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/gray tee#30.png
--------------------------------------------------------------------------------
/layers/Clothes/shirt one #3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/shirt one #3.png
--------------------------------------------------------------------------------
/layers/Clothes/shirt two#3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Clothes/shirt two#3.png
--------------------------------------------------------------------------------
/layers/Eyes/z1,eye#50/happy#50/eye2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Eyes/z1,eye#50/happy#50/eye2.png
--------------------------------------------------------------------------------
/layers/Eyes/z1,eye#50/normal#50/eye1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Eyes/z1,eye#50/normal#50/eye1.png
--------------------------------------------------------------------------------
/layers/Eyes/z1,star eyes#30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Eyes/z1,star eyes#30.png
--------------------------------------------------------------------------------
/layers/Hair/z1,Hair#20/bluehair#50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Hair/z1,Hair#20/bluehair#50.png
--------------------------------------------------------------------------------
/layers/Hair/z1,Hair#20/purplehair#50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Hair/z1,Hair#20/purplehair#50.png
--------------------------------------------------------------------------------
/layers/Head/faceA#3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Head/faceA#3.png
--------------------------------------------------------------------------------
/layers/Head/faceB#3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Head/faceB#3.png
--------------------------------------------------------------------------------
/layers/Shirt Accessories/Golden Sakura#1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Shirt Accessories/Golden Sakura#1.png
--------------------------------------------------------------------------------
/layers/Shirt Accessories/gold chain#40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Shirt Accessories/gold chain#40.png
--------------------------------------------------------------------------------
/layers/Shirt Accessories/nametag#40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/layers/Shirt Accessories/nametag#40.png
--------------------------------------------------------------------------------
/modules/HashlipsGiffer.js:
--------------------------------------------------------------------------------
1 | const GifEncoder = require("gif-encoder-2");
2 | const { writeFile } = require("fs");
3 |
4 | class HashLipsGiffer {
5 | constructor(_canvas, _ctx, _fileName, _repeat, _quality, _delay) {
6 | this.canvas = _canvas;
7 | this.ctx = _ctx;
8 | this.fileName = _fileName;
9 | this.repeat = _repeat;
10 | this.quality = _quality;
11 | this.delay = _delay;
12 | this.initGifEncoder();
13 | }
14 |
15 | initGifEncoder = () => {
16 | this.gifEncoder = new GifEncoder(this.canvas.width, this.canvas.height);
17 | this.gifEncoder.setQuality(this.quality);
18 | this.gifEncoder.setRepeat(this.repeat);
19 | this.gifEncoder.setDelay(this.delay);
20 | };
21 |
22 | start = () => {
23 | this.gifEncoder.start();
24 | };
25 |
26 | add = () => {
27 | this.gifEncoder.addFrame(this.ctx);
28 | };
29 |
30 | stop = () => {
31 | this.gifEncoder.finish();
32 | const buffer = this.gifEncoder.out.getData();
33 | writeFile(this.fileName, buffer, (error) => {});
34 | console.log(`Created gif at ${this.fileName}`);
35 | };
36 | }
37 |
38 | module.exports = HashLipsGiffer;
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nftchef-art-engine",
3 | "version": "2.2.0",
4 | "description": "NFTChef Fork - Hard fork from HashLips Art Engine, is a tool used to create multiple different instances of artworks based on provided layers. Most config is the same as Hashlips",
5 | "main": "index.js",
6 | "bin": "index.js",
7 | "engines": {
8 | "node": ">=14"
9 | },
10 | "pkg": {
11 | "assets": [
12 | "layers/**/*",
13 | "node_modules/**/*",
14 | "src/**/*"
15 | ]
16 | },
17 | "scripts": {
18 | "build": "node index.js",
19 | "generate": "node index.js",
20 | "generate:solana": "node index.js && node utils/metaplex.js",
21 | "generate:cardano": "node index.js && node utils/cardano.js",
22 | "generate:tezos": "node index.js && node utils/tezos.js",
23 | "preview": "node utils/createPreviewCollage.js",
24 | "gif": "node utils/preview_gif.js",
25 | "update:metadata": "node utils/updateInfo.js",
26 | "update_info:tezos": "node tezos/updateInfo.js",
27 | "resize": "node utils/resize.js",
28 | "rarity": "node utils/rarity.js",
29 | "preview": "node utils/createPreviewCollage.js"
30 | },
31 | "author": "Daniel Eugene Botha (HashLips), NFTChef",
32 | "license": "MIT",
33 | "dependencies": {
34 | "canvas": "^2.8.0",
35 | "chalk": "^4.1.2",
36 | "commander": "^8.2.0",
37 | "csv-writer": "^1.6.0",
38 | "eslint-plugin-node": "^11.1.0",
39 | "keccak256": "^1.0.3",
40 | "sharp": "^0.30.2",
41 | "gif-encoder-2": "^1.0.5"
42 | }
43 | }
--------------------------------------------------------------------------------
/src/blendMode.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const MODE = {
4 | sourceOver: "source-over",
5 | sourceIn: "source-in",
6 | sourceOut: "source-out",
7 | sourceAtop: "source-atop",
8 | destinationOver: "destination-over",
9 | destinationIn: "destination-in",
10 | destinationOut: "destination-out",
11 | destinationAtop: "destination-atop",
12 | lighter: "lighter",
13 | copy: "copy",
14 | xor: "xor",
15 | multiply: "multiply",
16 | screen: "screen",
17 | overlay: "overlay",
18 | darken: "darken",
19 | lighten: "lighten",
20 | colorDodge: "color-dodge",
21 | colorBurn: "color-burn",
22 | hardLight: "hard-light",
23 | softLight: "soft-light",
24 | difference: "difference",
25 | exclusion: "exclusion",
26 | hue: "hue",
27 | saturation: "saturation",
28 | color: "color",
29 | luminosity: "luminosity",
30 | };
31 |
32 | module.exports = {
33 | MODE,
34 | };
35 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const isLocal = typeof process.pkg === "undefined";
5 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
6 |
7 | // see src/blendMode.js for available blend modes
8 | // documentation: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
9 | const { MODE } = require(path.join(basePath, "src/blendMode.js"));
10 |
11 | const buildDir = path.join(basePath, "/build");
12 | const layersDir = path.join(basePath, "/layers");
13 |
14 | /*********************
15 | * General Generator Options
16 | ***********************/
17 |
18 | const description =
19 | "This is the description of your NFT project, remember to replace this";
20 | const baseUri = "ipfs://NewUriToReplace";
21 |
22 | const outputJPEG = false; // if false, the generator outputs png's
23 |
24 | /**
25 | * Set your tokenID index start number.
26 | * ⚠️ Be sure it matches your smart contract!
27 | */
28 | const startIndex = 0;
29 |
30 | const format = {
31 | width: 512,
32 | height: 512,
33 | smoothing: true, // set to false when up-scaling pixel art.
34 | };
35 |
36 | const background = {
37 | generate: true,
38 | brightness: "80%",
39 | };
40 |
41 | const layerConfigurations = [
42 | {
43 | growEditionSizeTo: 10,
44 | namePrefix: "Series 2", // Use to add a name to Metadata `name:`
45 | layersOrder: [
46 | { name: "Background" },
47 | {
48 | name: "Back Accessory",
49 | // options: {
50 | // bypassDNA: true,
51 | // },
52 | },
53 | { name: "Head" },
54 | { name: "Clothes" },
55 | { name: "Eyes" },
56 | { name: "Hair" },
57 | { name: "Accessory" },
58 | { name: "Shirt Accessories" },
59 | ],
60 | },
61 | // {
62 | // growEditionSizeTo: 10,
63 | // namePrefix: "Lion",
64 | // resetNameIndex: true, // this will start the Lion count at #1 instead of #6
65 | // layersOrder: [
66 | // { name: "Background" },
67 | // { name: "Hats" },
68 | // { name: "Male Hair" },
69 | // ],
70 | // },
71 | ];
72 |
73 | /**
74 | * Set to true for when using multiple layersOrder configuration
75 | * and you would like to shuffle all the artwork together
76 | */
77 | const shuffleLayerConfigurations = false;
78 |
79 | const debugLogs = true;
80 |
81 | /*********************
82 | * Advanced Generator Options
83 | ***********************/
84 |
85 | // if you use an empty/transparent file, set the name here.
86 | const emptyLayerName = "NONE";
87 |
88 | /**
89 | * Incompatible items can be added to this object by a files cleanName
90 | * This works in layer order, meaning, you need to define the layer that comes
91 | * first as the Key, and the incompatible items that _may_ come after.
92 | * The current version requires all layers to have unique names, or you may
93 | * accidentally set incompatibilities for the _wrong_ item.
94 | */
95 | const incompatible = {
96 | // Red: ["Dark Long"],
97 | // // directory incompatible with directory example
98 | // White: ["rare-Pink-Pompadour"],
99 | };
100 |
101 | /**
102 | * Require combinations of files when constructing DNA, this bypasses the
103 | * randomization and weights.
104 | *
105 | * The layer order matters here, the key (left side) is an item within
106 | * the layer that comes first in the stack.
107 | * the items in the array are "required" items that should be pulled from folders
108 | * further in the stack
109 | */
110 | const forcedCombinations = {
111 | // floral: ["MetallicShades", "Golden Sakura"],
112 | };
113 |
114 | /**
115 | * In the event that a filename cannot be the trait value name, for example when
116 | * multiple items should have the same value, specify
117 | * clean-filename: trait-value override pairs. Wrap filenames with spaces in quotes.
118 | */
119 | const traitValueOverrides = {
120 | Helmet: "Space Helmet",
121 | "gold chain": "GOLDEN NECKLACE",
122 | };
123 |
124 | const extraMetadata = {};
125 |
126 | const extraAttributes = () => [
127 | // Optionally, if you need to overwrite one of your layers attributes.
128 | // You can include the same name as the layer, here, and it will overwrite
129 | //
130 | // {
131 | // trait_type: "Bottom lid",
132 | // value: ` Bottom lid # ${Math.random() * 100}`,
133 | // },
134 | // {
135 | // display_type: "boost_number",
136 | // trait_type: "Aqua Power",
137 | // value: Math.random() * 100,
138 | // },
139 | // {
140 | // display_type: "boost_number",
141 | // trait_type: "Health",
142 | // value: Math.random() * 100,
143 | // },
144 | // {
145 | // display_type: "boost_number",
146 | // trait_type: "Mana",
147 | // value: Math.floor(Math.random() * 100),
148 | // },
149 | ];
150 |
151 | // Outputs an Keccack256 hash for the image. Required for provenance hash
152 | const hashImages = true;
153 |
154 | const rarityDelimiter = "#";
155 |
156 | const uniqueDnaTorrance = 10000;
157 |
158 | /**
159 | * Set to true to always use the root folder as trait_type
160 | * Set to false to use weighted parent folders as trait_type
161 | * Default is true.
162 | */
163 | const useRootTraitType = true;
164 |
165 | const preview = {
166 | thumbPerRow: 5,
167 | thumbWidth: 50,
168 | imageRatio: format.width / format.height,
169 | imageName: "preview.png",
170 | };
171 |
172 | const preview_gif = {
173 | numberOfImages: 5,
174 | order: "ASC", // ASC, DESC, MIXED
175 | repeat: 0,
176 | quality: 100,
177 | delay: 500,
178 | imageName: "preview.gif",
179 | };
180 |
181 | module.exports = {
182 | background,
183 | baseUri,
184 | buildDir,
185 | debugLogs,
186 | description,
187 | emptyLayerName,
188 | extraAttributes,
189 | extraMetadata,
190 | forcedCombinations,
191 | format,
192 | hashImages,
193 | incompatible,
194 | layerConfigurations,
195 | layersDir,
196 | outputJPEG,
197 | preview,
198 | preview_gif,
199 | rarityDelimiter,
200 | shuffleLayerConfigurations,
201 | startIndex,
202 | traitValueOverrides,
203 | uniqueDnaTorrance,
204 | useRootTraitType,
205 | };
206 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const isLocal = typeof process.pkg === "undefined";
5 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
6 | const fs = require("fs");
7 | const keccak256 = require("keccak256");
8 | const chalk = require("chalk");
9 |
10 | const { createCanvas, loadImage } = require(path.join(
11 | basePath,
12 | "/node_modules/canvas"
13 | ));
14 |
15 | console.log(path.join(basePath, "/src/config.js"));
16 | const {
17 | background,
18 | baseUri,
19 | buildDir,
20 | debugLogs,
21 | description,
22 | emptyLayerName,
23 | extraAttributes,
24 | extraMetadata,
25 | forcedCombinations,
26 | format,
27 | hashImages,
28 | incompatible,
29 | layerConfigurations,
30 | layersDir,
31 | outputJPEG,
32 | rarityDelimiter,
33 | shuffleLayerConfigurations,
34 | startIndex,
35 | traitValueOverrides,
36 | uniqueDnaTorrance,
37 | useRootTraitType,
38 | } = require(path.join(basePath, "/src/config.js"));
39 | const canvas = createCanvas(format.width, format.height);
40 | const ctxMain = canvas.getContext("2d");
41 | ctxMain.imageSmoothingEnabled = format.smoothing;
42 |
43 | let metadataList = [];
44 | let attributesList = [];
45 |
46 | // when generating a random background used to add to DNA
47 | let generatedBackground;
48 |
49 | let dnaList = new Set(); // internal+external: list of all files. used for regeneration etc
50 | let uniqueDNAList = new Set(); // internal: post-filtered dna set for bypassDNA etc.
51 | const DNA_DELIMITER = "*";
52 |
53 | const zflag = /(z-?\d*,)/;
54 |
55 | const buildSetup = () => {
56 | if (fs.existsSync(buildDir)) {
57 | fs.rmdirSync(buildDir, { recursive: true });
58 | }
59 | fs.mkdirSync(buildDir);
60 | fs.mkdirSync(path.join(buildDir, "/json"));
61 | fs.mkdirSync(path.join(buildDir, "/images"));
62 | };
63 |
64 | const getRarityWeight = (_path) => {
65 | // check if there is an extension, if not, consider it a directory
66 | const exp = new RegExp(`${rarityDelimiter}(\\d*)`, "g");
67 | const weight = exp.exec(_path);
68 | const weightNumber = weight ? Number(weight[1]) : -1;
69 |
70 | if (weightNumber < 0 || isNaN(weightNumber)) {
71 | return "required";
72 | }
73 | return weightNumber;
74 | };
75 |
76 | const cleanDna = (_str) => {
77 | var dna = _str.split(":").shift();
78 | return dna;
79 | };
80 |
81 | const cleanName = (_str) => {
82 | const hasZ = zflag.test(_str);
83 |
84 | const zRemoved = _str.replace(zflag, "");
85 |
86 | const extension = /\.[0-9a-zA-Z]+$/;
87 | const hasExtension = extension.test(zRemoved);
88 | let nameWithoutExtension = hasExtension ? zRemoved.slice(0, -4) : zRemoved;
89 | var nameWithoutWeight = nameWithoutExtension.split(rarityDelimiter).shift();
90 | return nameWithoutWeight;
91 | };
92 |
93 | const parseQueryString = (filename, layer, sublayer) => {
94 | const query = /\?(.*)\./;
95 | const querystring = query.exec(filename);
96 | if (!querystring) {
97 | return getElementOptions(layer, sublayer);
98 | }
99 |
100 | const layerstyles = querystring[1].split("&").reduce((r, setting) => {
101 | const keyPairs = setting.split("=");
102 | return { ...r, [keyPairs[0]]: keyPairs[1] };
103 | }, []);
104 |
105 | return {
106 | blendmode: layerstyles.blend
107 | ? layerstyles.blend
108 | : getElementOptions(layer, sublayer).blendmode,
109 | opacity: layerstyles.opacity
110 | ? layerstyles.opacity / 100
111 | : getElementOptions(layer, sublayer).opacity,
112 | };
113 | };
114 |
115 | /**
116 | * Given some input, creates a sha256 hash.
117 | * @param {Object} input
118 | */
119 | const hash = (input) => {
120 | const hashable = typeof input === "string" ? JSON.stringify(input) : input;
121 | return keccak256(hashable).toString("hex");
122 | };
123 |
124 | /**
125 | * Get't the layer options from the parent, or grandparent layer if
126 | * defined, otherwise, sets default options.
127 | *
128 | * @param {Object} layer the parent layer object
129 | * @param {String} sublayer Clean name of the current layer
130 | * @returns {blendmode, opacity} options object
131 | */
132 | const getElementOptions = (layer, sublayer) => {
133 | let blendmode = "source-over";
134 | let opacity = 1;
135 | if (layer.sublayerOptions?.[sublayer]) {
136 | const options = layer.sublayerOptions[sublayer];
137 |
138 | options.bypassDNA !== undefined ? (bypassDNA = options.bypassDNA) : null;
139 | options.blend !== undefined ? (blendmode = options.blend) : null;
140 | options.opacity !== undefined ? (opacity = options.opacity) : null;
141 | } else {
142 | // inherit parent blend mode
143 | blendmode = layer.blend != undefined ? layer.blend : "source-over";
144 | opacity = layer.opacity != undefined ? layer.opacity : 1;
145 | }
146 | return { blendmode, opacity };
147 | };
148 |
149 | const parseZIndex = (str) => {
150 | const z = zflag.exec(str);
151 | return z ? parseInt(z[0].match(/-?\d+/)[0]) : null;
152 | };
153 |
154 | const getElements = (path, layer) => {
155 | return fs
156 | .readdirSync(path)
157 | .filter((item) => {
158 | const invalid = /(\.ini)/g;
159 | return !/(^|\/)\.[^\/\.]/g.test(item) && !invalid.test(item);
160 | })
161 | .map((i, index) => {
162 | const name = cleanName(i);
163 | const extension = /\.[0-9a-zA-Z]+$/;
164 | const sublayer = !extension.test(i);
165 | const weight = getRarityWeight(i);
166 |
167 | const { blendmode, opacity } = parseQueryString(i, layer, name);
168 | //pass along the zflag to any children
169 | const zindex = zflag.exec(i)
170 | ? zflag.exec(i)[0]
171 | : layer.zindex
172 | ? layer.zindex
173 | : "";
174 |
175 | const element = {
176 | sublayer,
177 | weight,
178 | blendmode,
179 | opacity,
180 | id: index,
181 | name,
182 | filename: i,
183 | path: `${path}${i}`,
184 | zindex,
185 | };
186 |
187 | if (sublayer) {
188 | element.path = `${path}${i}`;
189 | const subPath = `${path}${i}/`;
190 | const sublayer = { ...layer, blend: blendmode, opacity, zindex };
191 | element.elements = getElements(subPath, sublayer);
192 | }
193 |
194 | // Set trait type on layers for metadata
195 | const lineage = path.split("/");
196 | let typeAncestor;
197 |
198 | if (weight !== "required") {
199 | typeAncestor = element.sublayer ? 3 : 2;
200 | }
201 | if (weight === "required") {
202 | typeAncestor = element.sublayer ? 1 : 3;
203 | }
204 | // we need to check if the parent is required, or if it's a prop-folder
205 | if (
206 | useRootTraitType &&
207 | lineage[lineage.length - typeAncestor].includes(rarityDelimiter)
208 | ) {
209 | typeAncestor += 1;
210 | }
211 |
212 | const parentName = cleanName(lineage[lineage.length - typeAncestor]);
213 |
214 | element.trait = layer.sublayerOptions?.[parentName]
215 | ? layer.sublayerOptions[parentName].trait
216 | : layer.trait !== undefined
217 | ? layer.trait
218 | : parentName;
219 |
220 | const rawTrait = getTraitValueFromPath(element, lineage);
221 | const trait = processTraitOverrides(rawTrait);
222 | element.traitValue = trait;
223 |
224 | return element;
225 | });
226 | };
227 |
228 | const getTraitValueFromPath = (element, lineage) => {
229 | // If the element is a required png. then, the trait property = the parent path
230 | // if the element is a non-required png. black%50.png, then element.name is the value and the parent Dir is the prop
231 | if (element.weight !== "required") {
232 | return element.name;
233 | } else if (element.weight === "required") {
234 | // if the element is a png that is required, get the traitValue from the parent Dir
235 | return element.sublayer ? true : cleanName(lineage[lineage.length - 2]);
236 | }
237 | };
238 |
239 | /**
240 | * Checks the override object for trait overrides
241 | * @param {String} trait The default trait value from the path-name
242 | * @returns String trait of either overridden value of raw default.
243 | */
244 | const processTraitOverrides = (trait) => {
245 | return traitValueOverrides[trait] ? traitValueOverrides[trait] : trait;
246 | };
247 |
248 | const layersSetup = (layersOrder) => {
249 | const layers = layersOrder.map((layerObj, index) => {
250 | return {
251 | id: index,
252 | name: layerObj.name,
253 | blendmode:
254 | layerObj["blend"] != undefined ? layerObj["blend"] : "source-over",
255 | opacity: layerObj["opacity"] != undefined ? layerObj["opacity"] : 1,
256 | elements: getElements(`${layersDir}/${layerObj.name}/`, layerObj),
257 | ...(layerObj.display_type !== undefined && {
258 | display_type: layerObj.display_type,
259 | }),
260 | bypassDNA:
261 | layerObj.options?.["bypassDNA"] !== undefined
262 | ? layerObj.options?.["bypassDNA"]
263 | : false,
264 | };
265 | });
266 |
267 | return layers;
268 | };
269 |
270 | const saveImage = (_editionCount, _buildDir, _canvas) => {
271 | fs.writeFileSync(
272 | `${_buildDir}/images/${_editionCount}${outputJPEG ? ".jpg" : ".png"}`,
273 | _canvas.toBuffer(`${outputJPEG ? "image/jpeg" : "image/png"}`)
274 | );
275 | };
276 |
277 | const genColor = () => {
278 | let hue = Math.floor(Math.random() * 360);
279 | let pastel = `hsl(${hue}, 100%, ${background.brightness})`;
280 | // store the background color in the dna
281 | generatedBackground = pastel; //TODO: storing in a global var is brittle. could be improved.
282 | return pastel;
283 | };
284 |
285 | const drawBackground = (canvasContext, background) => {
286 | canvasContext.fillStyle = background.HSL ?? genColor();
287 |
288 | canvasContext.fillRect(0, 0, format.width, format.height);
289 | };
290 |
291 | const addMetadata = (_dna, _edition, _prefixData) => {
292 | let dateTime = Date.now();
293 | const { _prefix, _offset, _imageHash } = _prefixData;
294 |
295 | const combinedAttrs = [...attributesList, ...extraAttributes()];
296 | const cleanedAttrs = combinedAttrs.reduce((acc, current) => {
297 | const x = acc.find((item) => item.trait_type === current.trait_type);
298 | if (!x) {
299 | return acc.concat([current]);
300 | } else {
301 | return acc;
302 | }
303 | }, []);
304 |
305 | let tempMetadata = {
306 | name: `${_prefix ? _prefix + " " : ""}#${_edition - _offset}`,
307 | description: description,
308 | image: `${baseUri}/${_edition}${outputJPEG ? ".jpg" : ".png"}`,
309 | ...(hashImages === true && { imageHash: _imageHash }),
310 | edition: _edition,
311 | date: dateTime,
312 | ...extraMetadata,
313 | attributes: cleanedAttrs,
314 | compiler: "HashLips Art Engine - NFTChef fork",
315 | };
316 | metadataList.push(tempMetadata);
317 | attributesList = [];
318 | return tempMetadata;
319 | };
320 |
321 | const addAttributes = (_element) => {
322 | let selectedElement = _element.layer;
323 | const layerAttributes = {
324 | trait_type: _element.layer.trait,
325 | value: selectedElement.traitValue,
326 | ...(_element.layer.display_type !== undefined && {
327 | display_type: _element.layer.display_type,
328 | }),
329 | };
330 | if (
331 | attributesList.some(
332 | (attr) => attr.trait_type === layerAttributes.trait_type
333 | )
334 | )
335 | return;
336 | attributesList.push(layerAttributes);
337 | };
338 |
339 | const loadLayerImg = async (_layer) => {
340 | return new Promise(async (resolve) => {
341 | // selected elements is an array.
342 | const image = await loadImage(`${_layer.path}`).catch((err) =>
343 | console.log(chalk.redBright(`failed to load ${_layer.path}`, err))
344 | );
345 | resolve({ layer: _layer, loadedImage: image });
346 | });
347 | };
348 |
349 | const drawElement = (_renderObject) => {
350 | const layerCanvas = createCanvas(format.width, format.height);
351 | const layerctx = layerCanvas.getContext("2d");
352 | layerctx.imageSmoothingEnabled = format.smoothing;
353 |
354 | layerctx.drawImage(
355 | _renderObject.loadedImage,
356 | 0,
357 | 0,
358 | format.width,
359 | format.height
360 | );
361 |
362 | addAttributes(_renderObject);
363 | return layerCanvas;
364 | };
365 |
366 | const constructLayerToDna = (_dna = [], _layers = []) => {
367 | const dna = _dna.split(DNA_DELIMITER);
368 | let mappedDnaToLayers = _layers.map((layer, index) => {
369 | let selectedElements = [];
370 | const layerImages = dna.filter(
371 | (element) => element.split(".")[0] == layer.id
372 | );
373 | layerImages.forEach((img) => {
374 | const indexAddress = cleanDna(img);
375 |
376 | //
377 |
378 | const indices = indexAddress.toString().split(".");
379 | // const firstAddress = indices.shift();
380 | const lastAddress = indices.pop(); // 1
381 | // recursively go through each index to get the nested item
382 | let parentElement = indices.reduce((r, nestedIndex) => {
383 | if (!r[nestedIndex]) {
384 | throw new Error("wtf");
385 | }
386 | return r[nestedIndex].elements;
387 | }, _layers); //returns string, need to return
388 |
389 | selectedElements.push(parentElement[lastAddress]);
390 | });
391 | // If there is more than one item whose root address indicies match the layer ID,
392 | // continue to loop through them an return an array of selectedElements
393 |
394 | return {
395 | name: layer.name,
396 | blendmode: layer.blendmode,
397 | opacity: layer.opacity,
398 | selectedElements: selectedElements,
399 | ...(layer.display_type !== undefined && {
400 | display_type: layer.display_type,
401 | }),
402 | };
403 | });
404 | return mappedDnaToLayers;
405 | };
406 |
407 | /**
408 | * In some cases a DNA string may contain optional query parameters for options
409 | * such as bypassing the DNA isUnique check, this function filters out those
410 | * items without modifying the stored DNA.
411 | *
412 | * @param {String} _dna New DNA string
413 | * @returns new DNA string with any items that should be filtered, removed.
414 | */
415 | const filterDNAOptions = (_dna) => {
416 | const filteredDNA = _dna.split(DNA_DELIMITER).filter((element) => {
417 | const query = /(\?.*$)/;
418 | const querystring = query.exec(element);
419 | if (!querystring) {
420 | return true;
421 | }
422 | // convert the items in the query string to an object
423 | const options = querystring[1].split("&").reduce((r, setting) => {
424 | const keyPairs = setting.split("=");
425 | // construct the object → {bypassDNA: bool}
426 | return { ...r, [keyPairs[0].replace("?", "")]: keyPairs[1] };
427 | }, []);
428 | // currently, there is only support for the bypassDNA option,
429 | // when bypassDNA is true, return false to omit from .filter
430 | return options.bypassDNA === "true" ? false : true;
431 | });
432 |
433 | return filteredDNA.join(DNA_DELIMITER);
434 | };
435 |
436 | /**
437 | * Cleaning function for DNA strings. When DNA strings include an option, it
438 | * is added to the filename with a ?setting=value query string. It needs to be
439 | * removed to properly access the file name before Drawing.
440 | *
441 | * @param {String} _dna The entire newDNA string
442 | * @returns Cleaned DNA string without querystring parameters.
443 | */
444 | const removeQueryStrings = (_dna) => {
445 | const query = /(\?.*$)/;
446 | return _dna.replace(query, "");
447 | };
448 |
449 | /**
450 | * determine if the sanitized/filtered DNA string is unique or not by comparing
451 | * it to the set of all previously generated permutations.
452 | *
453 | * @param {String} _dna string
454 | * @returns isUnique is true if uniqueDNAList does NOT contain a match,
455 | * false if uniqueDANList.has() is true
456 | */
457 | const isDnaUnique = (_dna = []) => {
458 | const filtered = filterDNAOptions(_dna);
459 | return !uniqueDNAList.has(filterDNAOptions(_dna));
460 | };
461 |
462 | // expecting to return an array of strings for each _layer_ that is picked,
463 | // should be a flattened list of all things that are picked randomly AND required
464 | /**
465 | *
466 | * @param {Object} layer The main layer, defined in config.layerConfigurations
467 | * @param {Array} dnaSequence Strings of layer to object mappings to nesting structure
468 | * @param {Number*} parentId nested parentID, used during recursive calls for sublayers
469 | * @param {Array*} incompatibleDNA Used to store incompatible layer names while building DNA
470 | * @param {Array*} forcedDNA Used to store forced layer selection combinations names while building DNA
471 | * @param {Int} zIndex Used in the dna string to define a layers stacking order
472 | * from the top down
473 | * @returns Array DNA sequence
474 | */
475 | function pickRandomElement(
476 | layer,
477 | dnaSequence,
478 | parentId,
479 | incompatibleDNA,
480 | forcedDNA,
481 | bypassDNA,
482 | zIndex
483 | ) {
484 | let totalWeight = 0;
485 | // Does this layer include a forcedDNA item? ya? just return it.
486 | const forcedPick = layer.elements.find((element) =>
487 | forcedDNA.includes(element.name)
488 | );
489 | if (forcedPick) {
490 | debugLogs
491 | ? console.log(chalk.yellowBright(`Force picking ${forcedPick.name}/n`))
492 | : null;
493 | if (forcedPick.sublayer) {
494 | return dnaSequence.concat(
495 | pickRandomElement(
496 | forcedPick,
497 | dnaSequence,
498 | `${parentId}.${forcedPick.id}`,
499 | incompatibleDNA,
500 | forcedDNA,
501 | bypassDNA,
502 | zIndex
503 | )
504 | );
505 | }
506 | let dnaString = `${parentId}.${forcedPick.id}:${forcedPick.zindex}${forcedPick.filename}${bypassDNA}`;
507 | return dnaSequence.push(dnaString);
508 | }
509 |
510 | if (incompatibleDNA.includes(layer.name) && layer.sublayer) {
511 | debugLogs
512 | ? console.log(
513 | `Skipping incompatible sublayer directory, ${layer.name}`,
514 | layer.name
515 | )
516 | : null;
517 | return dnaSequence;
518 | }
519 |
520 | const compatibleLayers = layer.elements.filter(
521 | (layer) => !incompatibleDNA.includes(layer.name)
522 | );
523 | if (compatibleLayers.length === 0) {
524 | debugLogs
525 | ? console.log(
526 | chalk.yellow(
527 | "No compatible layers in the directory, skipping",
528 | layer.name
529 | )
530 | )
531 | : null;
532 | return dnaSequence;
533 | }
534 |
535 | compatibleLayers.forEach((element) => {
536 | // If there is no weight, it's required, always include it
537 | // If directory has %, that is % chance to enter the dir
538 | if (element.weight == "required" && !element.sublayer) {
539 | let dnaString = `${parentId}.${element.id}:${element.zindex}${element.filename}${bypassDNA}`;
540 | dnaSequence.unshift(dnaString);
541 | return;
542 | }
543 | // when the current directory is a required folder
544 | // and the element in the loop is another folder
545 | if (element.weight == "required" && element.sublayer) {
546 | const next = pickRandomElement(
547 | element,
548 | dnaSequence,
549 | `${parentId}.${element.id}`,
550 | incompatibleDNA,
551 | forcedDNA,
552 | bypassDNA,
553 | zIndex
554 | );
555 | }
556 | if (element.weight !== "required") {
557 | totalWeight += element.weight;
558 | }
559 | });
560 | // if the entire directory should be ignored…
561 |
562 | // number between 0 - totalWeight
563 | const currentLayers = compatibleLayers.filter((l) => l.weight !== "required");
564 |
565 | let random = Math.floor(Math.random() * totalWeight);
566 |
567 | for (var i = 0; i < currentLayers.length; i++) {
568 | // subtract the current weight from the random weight until we reach a sub zero value.
569 | // Check if the picked image is in the incompatible list
570 | random -= currentLayers[i].weight;
571 |
572 | // e.g., directory, or, all files within a directory
573 | if (random < 0) {
574 | // Check for incompatible layer configurations and only add incompatibilities IF
575 | // chosing _this_ layer.
576 | if (incompatible[currentLayers[i].name]) {
577 | debugLogs
578 | ? console.log(
579 | `Adding the following to incompatible list`,
580 | ...incompatible[currentLayers[i].name]
581 | )
582 | : null;
583 | incompatibleDNA.push(...incompatible[currentLayers[i].name]);
584 | }
585 | // Similar to incompaticle, check for forced combos
586 | if (forcedCombinations[currentLayers[i].name]) {
587 | debugLogs
588 | ? console.log(
589 | chalk.bgYellowBright.black(
590 | `\nSetting up the folling forced combinations for ${currentLayers[i].name}: `,
591 | ...forcedCombinations[currentLayers[i].name]
592 | )
593 | )
594 | : null;
595 | forcedDNA.push(...forcedCombinations[currentLayers[i].name]);
596 | }
597 | // if there's a sublayer, we need to concat the sublayers parent ID to the DNA srting
598 | // and recursively pick nested required and random elements
599 | if (currentLayers[i].sublayer) {
600 | return dnaSequence.concat(
601 | pickRandomElement(
602 | currentLayers[i],
603 | dnaSequence,
604 | `${parentId}.${currentLayers[i].id}`,
605 | incompatibleDNA,
606 | forcedDNA,
607 | bypassDNA,
608 | zIndex
609 | )
610 | );
611 | }
612 |
613 | // none/empty layer handler
614 | if (currentLayers[i].name === emptyLayerName) {
615 | return dnaSequence;
616 | }
617 | let dnaString = `${parentId}.${currentLayers[i].id}:${currentLayers[i].zindex}${currentLayers[i].filename}${bypassDNA}`;
618 | return dnaSequence.push(dnaString);
619 | }
620 | }
621 | }
622 |
623 | /**
624 | * given the nesting structure is complicated and messy, the most reliable way to sort
625 | * is based on the number of nested indecies.
626 | * This sorts layers stacking the most deeply nested grandchildren above their
627 | * immediate ancestors
628 | * @param {[String]} layers array of dna string sequences
629 | */
630 | const sortLayers = (layers) => {
631 | const nestedsort = layers.sort((a, b) => {
632 | const addressA = a.split(":")[0];
633 | const addressB = b.split(":")[0];
634 | return addressA.length - addressB.length;
635 | });
636 |
637 | let stack = { front: [], normal: [], end: [] };
638 | stack = nestedsort.reduce((acc, layer) => {
639 | const zindex = parseZIndex(layer);
640 | if (!zindex)
641 | return { ...acc, normal: [...(acc.normal ? acc.normal : []), layer] };
642 | // move negative z into `front`
643 | if (zindex < 0)
644 | return { ...acc, front: [...(acc.front ? acc.front : []), layer] };
645 | // move positive z into `end`
646 | if (zindex > 0)
647 | return { ...acc, end: [...(acc.end ? acc.end : []), layer] };
648 | // make sure front and end are sorted
649 | // contat everything back to an ordered array
650 | }, stack);
651 |
652 | // sort the normal array
653 | stack.normal.sort();
654 |
655 | return sortByZ(stack.front).concat(stack.normal).concat(sortByZ(stack.end));
656 | };
657 |
658 | /** File String sort by zFlag */
659 | function sortByZ(dnastrings) {
660 | return dnastrings.sort((a, b) => {
661 | const indexA = parseZIndex(a);
662 | const indexB = parseZIndex(b);
663 | return indexA - indexB;
664 | });
665 | }
666 |
667 | /**
668 | * Sorting by index based on the layer.z property
669 | * @param {Array } layers selected Image layer objects array
670 | */
671 | function sortZIndex(layers) {
672 | return layers.sort((a, b) => {
673 | const indexA = parseZIndex(a.zindex);
674 | const indexB = parseZIndex(b.zindex);
675 | return indexA - indexB;
676 | });
677 | }
678 |
679 | const createDna = (_layers) => {
680 | let dnaSequence = [];
681 | let incompatibleDNA = [];
682 | let forcedDNA = [];
683 |
684 | _layers.forEach((layer) => {
685 | const layerSequence = [];
686 | pickRandomElement(
687 | layer,
688 | layerSequence,
689 | layer.id,
690 | incompatibleDNA,
691 | forcedDNA,
692 | layer.bypassDNA ? "?bypassDNA=true" : "",
693 | layer.zindex ? layer.zIndex : ""
694 | );
695 | const sortedLayers = sortLayers(layerSequence);
696 | dnaSequence = [...dnaSequence, [sortedLayers]];
697 | });
698 | const zSortDNA = sortByZ(dnaSequence.flat(2));
699 | const dnaStrand = zSortDNA.join(DNA_DELIMITER);
700 |
701 | return dnaStrand;
702 | };
703 |
704 | const writeMetaData = (_data) => {
705 | fs.writeFileSync(`${buildDir}/json/_metadata.json`, _data);
706 | };
707 |
708 | const writeDnaLog = (_data) => {
709 | fs.writeFileSync(`${buildDir}/_dna.json`, _data);
710 | };
711 |
712 | const saveMetaDataSingleFile = (_editionCount, _buildDir) => {
713 | let metadata = metadataList.find((meta) => meta.edition == _editionCount);
714 | debugLogs
715 | ? console.log(
716 | `Writing metadata for ${_editionCount}: ${JSON.stringify(metadata)}`
717 | )
718 | : null;
719 | fs.writeFileSync(
720 | `${_buildDir}/json/${_editionCount}.json`,
721 | JSON.stringify(metadata, null, 2)
722 | );
723 | };
724 |
725 | function shuffle(array) {
726 | let currentIndex = array.length,
727 | randomIndex;
728 | while (currentIndex != 0) {
729 | randomIndex = Math.floor(Math.random() * currentIndex);
730 | currentIndex--;
731 | [array[currentIndex], array[randomIndex]] = [
732 | array[randomIndex],
733 | array[currentIndex],
734 | ];
735 | }
736 | return array;
737 | }
738 |
739 | /**
740 | * Paints the given renderOjects to the main canvas context.
741 | *
742 | * @param {Array} renderObjectArray Array of render elements to draw to canvas
743 | * @param {Object} layerData data passed from the current iteration of the loop or configured dna-set
744 | *
745 | */
746 | const paintLayers = (canvasContext, renderObjectArray, layerData) => {
747 | debugLogs ? console.log("\nClearing canvas") : null;
748 | canvasContext.clearRect(0, 0, format.width, format.height);
749 |
750 | const { abstractedIndexes, _background } = layerData;
751 |
752 | renderObjectArray.forEach((renderObject) => {
753 | // one main canvas
754 | // each render Object should be a solo canvas
755 | // append them all to main canbas
756 | canvasContext.globalAlpha = renderObject.layer.opacity;
757 | canvasContext.globalCompositeOperation = renderObject.layer.blendmode;
758 | canvasContext.drawImage(
759 | drawElement(renderObject),
760 | 0,
761 | 0,
762 | format.width,
763 | format.height
764 | );
765 | });
766 |
767 | if (_background.generate) {
768 | canvasContext.globalCompositeOperation = "destination-over";
769 | drawBackground(canvasContext, background);
770 | }
771 | debugLogs
772 | ? console.log("Editions left to create: ", abstractedIndexes)
773 | : null;
774 | };
775 |
776 | const postProcessMetadata = (layerData) => {
777 | const { abstractedIndexes, layerConfigIndex } = layerData;
778 | // Metadata options
779 | const savedFile = fs.readFileSync(
780 | `${buildDir}/images/${abstractedIndexes[0]}${outputJPEG ? ".jpg" : ".png"}`
781 | );
782 | const _imageHash = hash(savedFile);
783 |
784 | // if there's a prefix for the current configIndex, then
785 | // start count back at 1 for the name, only.
786 | const _prefix = layerConfigurations[layerConfigIndex].namePrefix
787 | ? layerConfigurations[layerConfigIndex].namePrefix
788 | : null;
789 | // if resetNameIndex is turned on, calculate the offset and send it
790 | // with the prefix
791 | let _offset = 0;
792 | if (layerConfigurations[layerConfigIndex].resetNameIndex) {
793 | _offset = layerConfigurations[layerConfigIndex - 1].growEditionSizeTo;
794 | }
795 |
796 | return {
797 | _imageHash,
798 | _prefix,
799 | _offset,
800 | };
801 | };
802 |
803 | const outputFiles = (
804 | abstractedIndexes,
805 | layerData,
806 | _buildDir = buildDir,
807 | _canvas = canvas
808 | ) => {
809 | const { newDna, layerConfigIndex } = layerData;
810 | // Save the canvas buffer to file
811 | saveImage(abstractedIndexes[0], _buildDir, _canvas);
812 |
813 | const { _imageHash, _prefix, _offset } = postProcessMetadata(layerData);
814 |
815 | addMetadata(newDna, abstractedIndexes[0], {
816 | _prefix,
817 | _offset,
818 | _imageHash,
819 | });
820 |
821 | saveMetaDataSingleFile(abstractedIndexes[0], _buildDir);
822 | console.log(chalk.cyan(`Created edition: ${abstractedIndexes[0]}`));
823 | };
824 |
825 | const startCreating = async (storedDNA) => {
826 | if (storedDNA) {
827 | console.log(`using stored dna of ${storedDNA.size}`);
828 | dnaList = storedDNA;
829 | dnaList.forEach((dna) => {
830 | const editionExp = /\d+\//;
831 | const dnaWithoutEditionNum = dna.replace(editionExp, "");
832 | uniqueDNAList.add(filterDNAOptions(dnaWithoutEditionNum));
833 | });
834 | }
835 | let layerConfigIndex = 0;
836 | let editionCount = 1; //used for the growEditionSize while loop, not edition number
837 | let failedCount = 0;
838 | let abstractedIndexes = [];
839 | for (
840 | let i = startIndex;
841 | i <=
842 | startIndex +
843 | layerConfigurations[layerConfigurations.length - 1].growEditionSizeTo -
844 | 1;
845 | i++
846 | ) {
847 | abstractedIndexes.push(i);
848 | }
849 | if (shuffleLayerConfigurations) {
850 | abstractedIndexes = shuffle(abstractedIndexes);
851 | }
852 | debugLogs
853 | ? console.log("Editions left to create: ", abstractedIndexes)
854 | : null;
855 | while (layerConfigIndex < layerConfigurations.length) {
856 | const layers = layersSetup(
857 | layerConfigurations[layerConfigIndex].layersOrder
858 | );
859 | while (
860 | editionCount <= layerConfigurations[layerConfigIndex].growEditionSizeTo
861 | ) {
862 | let newDna = createDna(layers);
863 | if (isDnaUnique(newDna)) {
864 | let results = constructLayerToDna(newDna, layers);
865 | debugLogs ? console.log("DNA:", newDna.split(DNA_DELIMITER)) : null;
866 | let loadedElements = [];
867 | // reduce the stacked and nested layer into a single array
868 | const allImages = results.reduce((images, layer) => {
869 | return [...images, ...layer.selectedElements];
870 | }, []);
871 | sortZIndex(allImages).forEach((layer) => {
872 | loadedElements.push(loadLayerImg(layer));
873 | });
874 |
875 | await Promise.all(loadedElements).then((renderObjectArray) => {
876 | const layerData = {
877 | newDna,
878 | layerConfigIndex,
879 | abstractedIndexes,
880 | _background: background,
881 | };
882 | paintLayers(ctxMain, renderObjectArray, layerData);
883 | outputFiles(abstractedIndexes, layerData);
884 | });
885 |
886 | // prepend the same output num (abstractedIndexes[0])
887 | // to the DNA as the saved files.
888 | dnaList.add(
889 | `${abstractedIndexes[0]}/${newDna}${
890 | generatedBackground ? "___" + generatedBackground : ""
891 | }`
892 | );
893 | uniqueDNAList.add(filterDNAOptions(newDna));
894 | editionCount++;
895 | abstractedIndexes.shift();
896 | } else {
897 | console.log(chalk.bgRed("DNA exists!"));
898 | failedCount++;
899 | if (failedCount >= uniqueDnaTorrance) {
900 | console.log(
901 | `You need more layers or elements to grow your edition to ${layerConfigurations[layerConfigIndex].growEditionSizeTo} artworks!`
902 | );
903 | process.exit();
904 | }
905 | }
906 | }
907 | layerConfigIndex++;
908 | }
909 | writeMetaData(JSON.stringify(metadataList, null, 2));
910 | writeDnaLog(JSON.stringify([...dnaList], null, 2));
911 | };
912 |
913 | module.exports = {
914 | addAttributes,
915 | addMetadata,
916 | buildSetup,
917 | constructLayerToDna,
918 | cleanName,
919 | createDna,
920 | DNA_DELIMITER,
921 | getElements,
922 | hash,
923 | isDnaUnique,
924 | layersSetup,
925 | loadLayerImg,
926 | outputFiles,
927 | paintLayers,
928 | parseQueryString,
929 | postProcessMetadata,
930 | sortZIndex,
931 | startCreating,
932 | writeMetaData,
933 | };
934 |
--------------------------------------------------------------------------------
/ultraRares/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/ultraRares/images/1.png
--------------------------------------------------------------------------------
/ultraRares/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nftchef/art-engine/884bc739678ee07e0d14297738d015e712bbac66/ultraRares/images/2.png
--------------------------------------------------------------------------------
/ultraRares/json/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "dna": "efefc005307ac4d9469ac42f566f471d7589c47de2808ebd6d7c9b06bba1a177",
3 | "name": "## super rare sunburn ",
4 | "description": "This is the description of your NFT project, remember to replace this",
5 | "image": "ipfs://NewUriToReplace/##.png",
6 | "edition": "##",
7 | "date": 1635397198441,
8 | "attributes": [
9 |
10 | {
11 | "trait_type": "Head",
12 | "value": "Sunburn"
13 | },
14 | {
15 | "trait_type": "Clothes",
16 | "value": "Shiny jacket"
17 | },
18 | {
19 | "trait_type": "Shirt Accessories",
20 | "value": "GOLDEN NECKLACE"
21 | }
22 | ],
23 | "properties": {
24 | "something": "Some string with ## In the middle for trickiness",
25 | "Edition": "##"
26 | },
27 | "compiler": "HashLips Art Engine - NFTChef fork"
28 | }
--------------------------------------------------------------------------------
/ultraRares/json/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "dna": "efefc005307ac4d9469ac42f566f471d7589c47de2808ebd6d7c9b06bba1a177",
3 | "name": "## super rare sunburn ",
4 | "description": "This is the description of your NFT project, remember to replace this",
5 | "image": "ipfs://NewUriToReplace/##.png",
6 | "imageHash": "This is automatically replaced",
7 | "edition": "##",
8 | "date": 1635397198441,
9 | "attributes": [
10 |
11 | {
12 | "trait_type": "Head",
13 | "value": "Sunburn"
14 | },
15 | {
16 | "trait_type": "Clothes",
17 | "value": "Shiny jacket"
18 | },
19 | {
20 | "trait_type": "Shirt Accessories",
21 | "value": "GOLDEN NECKLACE"
22 | }
23 | ],
24 | "properties": {
25 | "something": "Some string with ## In the middle for trickiness",
26 | "Edition": "##"
27 | },
28 | "compiler": "HashLips Art Engine - NFTChef fork"
29 | }
--------------------------------------------------------------------------------
/utils/cardano.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /**
3 | * Cardono util is build to conform to the specifications and workflow
4 | * for NFTMaker Pro.
5 | *
6 | * The policy_id, image location, and other values are left in the
7 | * placeholder form, e.g.,
8 | * The actual values are replaced dynamically by NFTmakerPro.
9 | *
10 | *
11 | */
12 | const fs = require("fs");
13 | const path = require("path");
14 | const isLocal = typeof process.pkg === "undefined";
15 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
16 | const chalk = require("chalk");
17 |
18 | // const imagesDir = `${basePath}/build/images`;
19 | const jsonDir = `${basePath}/build/json`;
20 |
21 | const metadataBuildPath = `${basePath}/build/cardano`;
22 | const metadataConfigPath = `${basePath}/build/cardano`;
23 |
24 | const setup = () => {
25 | if (fs.existsSync(metadataBuildPath)) {
26 | fs.rmSync(metadataBuildPath, {
27 | recursive: true,
28 | });
29 | }
30 | fs.mkdirSync(metadataBuildPath);
31 | fs.mkdirSync(path.join(metadataBuildPath, "/metadata"));
32 | };
33 |
34 | const getIndividualJsonFiles = () => {
35 | return fs
36 | .readdirSync(jsonDir)
37 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
38 | };
39 |
40 | setup();
41 | console.log(chalk.cyan.black("Beginning Cardano conversion"));
42 | console.log(
43 | chalk.cyan(`\nExtracting files.\nWriting to folder: ${metadataBuildPath}`)
44 | );
45 |
46 | // Identify json files
47 | const jsonFiles = getIndividualJsonFiles();
48 | console.log(
49 | chalk.cyan(`Found ${jsonFiles.length} json files in "${jsonDir}" to process`)
50 | );
51 |
52 | // Iterate, open and put in metadata list
53 | jsonFiles.forEach((file) => {
54 | let nameWithoutExtension = file.slice(0, -4);
55 | let editionCountFromFileName = Number(nameWithoutExtension);
56 |
57 | const rawData = fs.readFileSync(`${jsonDir}/${file}`);
58 | const jsonData = JSON.parse(rawData);
59 |
60 | // convert the array of attributes into a flat object
61 | // this object is spread into the metadata template
62 | const restructuredAttributes = {};
63 | jsonData.attributes.map((attr) => {
64 | restructuredAttributes[attr.trait_type] = attr.value;
65 | }, []);
66 |
67 | let metadataTemplate = {
68 | 721: {
69 | "": {
70 | "": {
71 | name: jsonData.name,
72 | image: "",
73 | mediaType: "",
74 | description: jsonData.description,
75 | files: [
76 | {
77 | name: "",
78 | mediaType: "",
79 | src: "",
80 | },
81 | ],
82 | ...restructuredAttributes,
83 | },
84 | },
85 | version: "1.0",
86 | },
87 | };
88 | fs.writeFileSync(
89 | path.join(
90 | `${metadataConfigPath}`,
91 | "metadata",
92 | `${editionCountFromFileName}.metadata`
93 | ),
94 | JSON.stringify(metadataTemplate, null, 2)
95 | );
96 | });
97 | console.log(`\nFinished converting json metadata files to Cardano Format.`);
98 | console.log(chalk.green(`\nConversion was finished successfully!\n`));
99 |
--------------------------------------------------------------------------------
/utils/createPreviewCollage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const isLocal = typeof process.pkg === "undefined";
4 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
5 | const fs = require("fs");
6 | const path = require("path");
7 | const { createCanvas, loadImage } = require("canvas");
8 | const buildDir = `${basePath}/build`;
9 |
10 | const { preview } = require(path.join(basePath, "/src/config.js"));
11 |
12 | // read json data
13 | const rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`);
14 | const metadataList = JSON.parse(rawdata);
15 |
16 | const saveProjectPreviewImage = async (_data) => {
17 | // Extract from preview config
18 | const { thumbWidth, thumbPerRow, imageRatio, imageName } = preview;
19 | // Calculate height on the fly
20 | const thumbHeight = thumbWidth * imageRatio;
21 | // Prepare canvas
22 | const previewCanvasWidth = thumbWidth * thumbPerRow;
23 | const previewCanvasHeight =
24 | thumbHeight * Math.trunc(_data.length / thumbPerRow);
25 | // Shout from the mountain tops
26 | console.log(
27 | `Preparing a ${previewCanvasWidth}x${previewCanvasHeight} project preview with ${_data.length} thumbnails.`
28 | );
29 |
30 | // Initiate the canvas now that we have calculated everything
31 | const previewPath = `${buildDir}/${imageName}`;
32 | const previewCanvas = createCanvas(previewCanvasWidth, previewCanvasHeight);
33 | const previewCtx = previewCanvas.getContext("2d");
34 |
35 | // Iterate all NFTs and insert thumbnail into preview image
36 | // Don't want to rely on "edition" for assuming index
37 | for (let index = 0; index < _data.length; index++) {
38 | const nft = _data[index];
39 | await loadImage(`${buildDir}/images/${nft.edition}.png`).then((image) => {
40 | previewCtx.drawImage(
41 | image,
42 | thumbWidth * (index % thumbPerRow),
43 | thumbHeight * Math.trunc(index / thumbPerRow),
44 | thumbWidth,
45 | thumbHeight
46 | );
47 | });
48 | }
49 |
50 | // Write Project Preview to file
51 | fs.writeFileSync(previewPath, previewCanvas.toBuffer("image/png"));
52 | console.log(`Project preview image located at: ${previewPath}`);
53 | };
54 |
55 | saveProjectPreviewImage(metadataList);
56 |
--------------------------------------------------------------------------------
/utils/metaplex.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const path = require("path");
5 | const isLocal = typeof process.pkg === "undefined";
6 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
7 | const chalk = require("chalk");
8 |
9 | const {
10 | creators,
11 | description,
12 | external_url,
13 | NFTName,
14 | royaltyFee,
15 | symbol,
16 | } = require(path.join(basePath, "/Solana/solana_config.js"));
17 | const { startIndex, outputJPEG } = require(path.join(
18 | basePath,
19 | "/src/config.js"
20 | ));
21 | const imagesDir = `${basePath}/build/images`;
22 | const jsonDir = `${basePath}/build/json`;
23 |
24 | const metaplexFilePath = `${basePath}/build/solana`;
25 | const metaplexDir = `${basePath}/build/solana`;
26 |
27 | const setup = () => {
28 | if (fs.existsSync(metaplexFilePath)) {
29 | fs.rmSync(metaplexFilePath, {
30 | recursive: true,
31 | });
32 | }
33 | fs.mkdirSync(metaplexFilePath);
34 | fs.mkdirSync(path.join(metaplexFilePath, "/json"));
35 | if (startIndex != 0) {
36 | fs.mkdirSync(path.join(metaplexFilePath, "/images"));
37 | }
38 | };
39 |
40 | const getIndividualImageFiles = () => {
41 | return fs.readdirSync(imagesDir);
42 | };
43 |
44 | const getIndividualJsonFiles = () => {
45 | return fs
46 | .readdirSync(jsonDir)
47 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
48 | };
49 |
50 | setup();
51 | console.log(chalk.bgGreenBright.black("Beginning Solana/Metaplex conversion"));
52 | console.log(
53 | chalk.green(
54 | `\nExtracting metaplex-ready files.\nWriting to folder: ${metaplexFilePath}`
55 | )
56 | );
57 |
58 | const outputFormat = outputJPEG ? "jpg" : "png";
59 | // copy & rename images IF needed
60 | // Rename all image files to n-1.png (to be zero indexed "start at zero") and store in solana/images
61 | if (startIndex != 0) {
62 | const imageFiles = getIndividualImageFiles();
63 | imageFiles.forEach((file) => {
64 | let nameWithoutExtension = file.slice(0, -4);
65 | let editionCountFromFileName = Number(nameWithoutExtension);
66 | let newEditionCount = editionCountFromFileName - startIndex;
67 | fs.copyFile(
68 | `${imagesDir}/${file}`,
69 | path.join(
70 | `${metaplexDir}`,
71 | "images",
72 | `${newEditionCount}.${outputFormat}`
73 | ),
74 | () => {}
75 | );
76 | });
77 | console.log(`\nFinished converting images to being metaplex-ready.\n`);
78 | }
79 |
80 | // Identify json files
81 | const jsonFiles = getIndividualJsonFiles();
82 | console.log(
83 | chalk.green(`Found ${jsonFiles.length} json files in "${jsonDir}" to process`)
84 | );
85 |
86 | // Iterate, open and put in metadata list
87 | jsonFiles.forEach((file) => {
88 | let nameWithoutExtension = file.slice(0, -4);
89 | let editionCountFromFileName = Number(nameWithoutExtension);
90 | let newEditionCount = editionCountFromFileName - startIndex;
91 |
92 | const rawData = fs.readFileSync(`${jsonDir}/${file}`);
93 | const jsonData = JSON.parse(rawData);
94 |
95 | let tempMetadata = {
96 | name: NFTName + " " + jsonData.name,
97 | symbol: symbol,
98 | description: description,
99 | seller_fee_basis_points: royaltyFee,
100 | image: `${newEditionCount}.${outputFormat}`,
101 | ...(external_url !== "" && { external_url }),
102 | attributes: jsonData.attributes,
103 | properties: {
104 | edition: jsonData.edition,
105 | files: [
106 | {
107 | uri: `${newEditionCount}.${outputFormat}`,
108 | type: `image/${outputFormat}`,
109 | },
110 | ],
111 | category: "image",
112 | creators: creators,
113 | compiler: "HashLips Art Engine - NFTChef fork | qualifieddevs.io",
114 | },
115 | };
116 | fs.writeFileSync(
117 | path.join(`${metaplexDir}`, "json", `${newEditionCount}.json`),
118 | JSON.stringify(tempMetadata, null, 2)
119 | );
120 | });
121 | console.log(
122 | `\nFinished converting json metadata files to being metaplex-ready.`
123 | );
124 | console.log(chalk.green(`\nConversion was finished successfully!\n`));
125 |
--------------------------------------------------------------------------------
/utils/preview_gif.js:
--------------------------------------------------------------------------------
1 | const basePath = process.cwd();
2 | const fs = require("fs");
3 | const { createCanvas, loadImage } = require("canvas");
4 | const buildDir = `${basePath}/build`;
5 | const imageDir = `${buildDir}/images`;
6 | const { format, preview_gif } = require(`${basePath}/src/config.js`);
7 | const canvas = createCanvas(format.width, format.height);
8 | const ctx = canvas.getContext("2d");
9 |
10 | const HashlipsGiffer = require(`${basePath}/modules/HashlipsGiffer.js`);
11 | let hashlipsGiffer = null;
12 |
13 | const loadImg = async (_img) => {
14 | return new Promise(async (resolve) => {
15 | const loadedImage = await loadImage(`${_img}`);
16 | resolve({ loadedImage: loadedImage });
17 | });
18 | };
19 |
20 | // read image paths
21 | const imageList = [];
22 | const rawdata = fs.readdirSync(imageDir).forEach((file) => {
23 | imageList.push(loadImg(`${imageDir}/${file}`));
24 | });
25 |
26 | const saveProjectPreviewGIF = async (_data) => {
27 | // Extract from preview config
28 | const { numberOfImages, order, repeat, quality, delay, imageName } =
29 | preview_gif;
30 | // Extract from format config
31 | const { width, height } = format;
32 | // Prepare canvas
33 | const previewCanvasWidth = width;
34 | const previewCanvasHeight = height;
35 |
36 | if (_data.length < numberOfImages) {
37 | console.log(
38 | `You do not have enough images to create a gif with ${numberOfImages} images.`
39 | );
40 | } else {
41 | // Shout from the mountain tops
42 | console.log(
43 | `Preparing a ${previewCanvasWidth}x${previewCanvasHeight} project preview with ${_data.length} images.`
44 | );
45 | const previewPath = `${buildDir}/${imageName}`;
46 |
47 | ctx.clearRect(0, 0, width, height);
48 |
49 | hashlipsGiffer = new HashlipsGiffer(
50 | canvas,
51 | ctx,
52 | `${previewPath}`,
53 | repeat,
54 | quality,
55 | delay
56 | );
57 | hashlipsGiffer.start();
58 |
59 | await Promise.all(_data).then((renderObjectArray) => {
60 | // Determin the order of the Images before creating the gif
61 | if (order == "ASC") {
62 | // Do nothing
63 | } else if (order == "DESC") {
64 | renderObjectArray.reverse();
65 | } else if (order == "MIXED") {
66 | renderObjectArray = renderObjectArray.sort(() => Math.random() - 0.5);
67 | }
68 |
69 | // Reduce the size of the array of Images to the desired amount
70 | if (parseInt(numberOfImages) > 0) {
71 | renderObjectArray = renderObjectArray.slice(0, numberOfImages);
72 | }
73 |
74 | renderObjectArray.forEach((renderObject, index) => {
75 | ctx.globalAlpha = 1;
76 | ctx.globalCompositeOperation = "source-over";
77 | ctx.drawImage(
78 | renderObject.loadedImage,
79 | 0,
80 | 0,
81 | previewCanvasWidth,
82 | previewCanvasHeight
83 | );
84 | hashlipsGiffer.add();
85 | });
86 | });
87 | hashlipsGiffer.stop();
88 | }
89 | };
90 |
91 | saveProjectPreviewGIF(imageList);
92 |
--------------------------------------------------------------------------------
/utils/provenance.js:
--------------------------------------------------------------------------------
1 | const keccak256 = require("keccak256");
2 | const fs = require("fs");
3 | const chalk = require("chalk");
4 | const path = require("path");
5 | const isLocal = typeof process.pkg === "undefined";
6 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
7 |
8 | const { buildDir } = require(path.join(basePath, "/src/config.js"));
9 | // Read files from the build folder defined in config.
10 | const metadata = JSON.parse(
11 | fs.readFileSync(path.join(buildDir, `/json/_metadata.json`), "utf-8")
12 | );
13 |
14 | const accumulatedHashString = metadata.reduce((acc, item) => {
15 | return acc.concat(item.imageHash);
16 | }, []);
17 |
18 | const provenance = keccak256(accumulatedHashString.join("")).toString("hex");
19 |
20 | fs.writeFileSync(
21 | `${buildDir}/_provenance.json`,
22 | JSON.stringify(
23 | { provenance, concatenatedHashString: accumulatedHashString.join("") },
24 | null,
25 | "\t"
26 | )
27 | );
28 |
29 | console.log(`\nProvenance Hash Save in !\n${buildDir}/_provenance.json\n`);
30 | console.log(chalk.greenBright.bold(`${provenance} \n`));
31 |
--------------------------------------------------------------------------------
/utils/rarity.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const createCsvWriter = require("csv-writer").createObjectCsvWriter;
5 |
6 | const isLocal = typeof process.pkg === "undefined";
7 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
8 | const fs = require("fs");
9 | // const layersDir = `${basePath}/layers`;
10 | const layersDir = path.join(basePath, "../", "genkiFiles");
11 |
12 | const {
13 | layerConfigurations,
14 | extraAttributes,
15 | rarityDelimiter,
16 | } = require(path.join(basePath, "/src/config.js"));
17 |
18 | const { getElements, cleanName } = require("../src/main.js");
19 | const metadataPath = path.join(basePath, "/build/json/_metadata.json");
20 |
21 | function calculate(options = {}) {
22 | let rarity = {};
23 | let totals = {};
24 | let attributeCounts = {};
25 |
26 | const dataset = JSON.parse(fs.readFileSync(metadataPath)); // filter out .DS_Store
27 | // .filter((item) => {
28 | // return !/(^|\/)\.[^\/\.]/g.test(item);
29 | // });
30 | dataset.forEach((metadata) => {
31 | // const readData = fs.readFileSync(path.join(basePath, inputdir, file));
32 | // const metadata = JSON.parse(readData);
33 | // Push the attributes to the main counter and increment
34 | metadata.attributes = metadata.attributes.filter(
35 | (attr) => attr.value !== ""
36 | );
37 |
38 | // add a count to the attribue counts
39 | attributeCounts[metadata.attributes.length] = attributeCounts[
40 | metadata.attributes.length
41 | ]
42 | ? attributeCounts[metadata.attributes.length] + 1
43 | : 1;
44 |
45 | metadata.attributes.forEach((attribute) => {
46 | rarity = {
47 | ...rarity,
48 | [attribute.trait_type]: {
49 | ...rarity[attribute.trait_type],
50 | [attribute.value]: {
51 | count: rarity[attribute.trait_type]
52 | ? rarity[attribute.trait_type][attribute.value]
53 | ? rarity[attribute.trait_type][attribute.value].count + 1
54 | : 1
55 | : 1,
56 | },
57 | },
58 | };
59 |
60 | totals = {
61 | ...totals,
62 | [attribute.trait_type]: totals[attribute.trait_type]
63 | ? (totals[attribute.trait_type] += 1)
64 | : 1,
65 | };
66 | });
67 | });
68 |
69 | // loop again to write percentages based on occurrences/ total supply
70 | for (const category in rarity) {
71 | for (const element in rarity[category]) {
72 | rarity[category][element].percentage = (
73 | (rarity[category][element].count / dataset.length) *
74 | 100
75 | ).toFixed(4);
76 | }
77 | }
78 |
79 | // sort everything alphabetically (could be refactored)
80 | for (let subitem in rarity) {
81 | rarity[subitem] = Object.keys(rarity[subitem])
82 | .sort()
83 | .reduce((obj, key) => {
84 | obj[key] = rarity[subitem][key];
85 | return obj;
86 | }, {});
87 | }
88 | const ordered = Object.keys(rarity)
89 | .sort()
90 | .reduce((obj, key) => {
91 | obj[key] = rarity[key];
92 | return obj;
93 | }, {});
94 |
95 | // append attribute count as a trait
96 | ordered["Attribute Count"] = {};
97 |
98 | for (const key in attributeCounts) {
99 | console.log(`attributeCounts ${key}`);
100 | ordered["Attribute Count"][`${key} Attributes`] = {
101 | count: attributeCounts[key],
102 | percentage: (attributeCounts[key] / dataset.length).toFixed(4) * 100,
103 | };
104 | }
105 |
106 | // TODO: Calculate rarity score by looping through the set again
107 | console.log({ count: dataset.length });
108 |
109 | const tokenRarities = [];
110 |
111 | dataset.forEach((metadata) => {
112 | metadata.attributes = metadata.attributes.filter(
113 | (attr) => attr.value !== ""
114 | );
115 |
116 | // look up each one in the rarity data, and sum it
117 | const raritySum = metadata.attributes.reduce((sum, attribute) => {
118 | return (
119 | sum + Number(ordered[attribute.trait_type][attribute.value].percentage)
120 | );
121 | }, 0);
122 |
123 | tokenRarities.push({ name: metadata.name, raritySum });
124 | });
125 |
126 | tokenRarities.sort((a, b) => {
127 | return a.raritySum - b.raritySum;
128 | });
129 |
130 | console.log(ordered);
131 | console.log(attributeCounts);
132 | outputRarityCSV(ordered);
133 |
134 | // console.log(tokenRarities);
135 | console.table(tokenRarities);
136 | options.outputRanking ? outputRankingCSV(tokenRarities) : null;
137 | }
138 |
139 | /**
140 | * converts the sorted rarity data objects into the csv output we are looking for
141 | * @param {Array} rarityData all calculated usages and percentages
142 | */
143 | async function outputRarityCSV(rarityData) {
144 | const csvWriter = createCsvWriter({
145 | path: path.join(basePath, "build/_rarity.csv"),
146 | header: [
147 | { id: "name", title: "Attribute" },
148 | { id: "count", title: "Count" },
149 | { id: "percentage", title: "Percentage" },
150 | ],
151 | });
152 | // loop through the
153 | for (const trait in rarityData) {
154 | await csvWriter.writeRecords([
155 | { name: "" },
156 | {
157 | name: trait,
158 | },
159 | ]);
160 | console.log({ trait });
161 | const rows = [];
162 | for (const [key, value] of Object.entries(rarityData[trait])) {
163 | rows.push({
164 | name: key,
165 | count: rarityData[trait][key].count,
166 | percentage: rarityData[trait][key].percentage,
167 | });
168 | }
169 | await csvWriter.writeRecords(rows);
170 | console.log(rows);
171 | }
172 | }
173 |
174 | /**
175 | * outputs a csv of ordered and ranked tokens by rarity score.
176 | * @param {Array[Objects]} ranking sorted ranking data
177 | */
178 | function outputRankingCSV(ranking) {
179 | const csvWriter = createCsvWriter({
180 | path: path.join(basePath, "build/_ranking.csv"),
181 | header: [
182 | { id: "name", title: "NAME" },
183 | { id: "raritySum", title: "Rarity Sum" },
184 | ],
185 | });
186 | csvWriter.writeRecords(ranking);
187 | }
188 |
189 | calculate({ outputRanking: true });
190 |
--------------------------------------------------------------------------------
/utils/rebuildAll.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * Utility for regenerating the same output using the DNA file to
5 | * redraw each previously generated image.
6 | *
7 | * Optionally, you can reconfigure backgrounds,
8 | * turn off layers, e.g. backgrounds for transparent vertions
9 | * using --omit
10 |
11 | */
12 |
13 | const isLocal = typeof process.pkg === "undefined";
14 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
15 | const fs = require("fs");
16 | const path = require("path");
17 | const { Command } = require("commander");
18 | const program = new Command();
19 | const chalk = require("chalk");
20 |
21 | const { createCanvas } = require("canvas");
22 |
23 | const {
24 | format,
25 | layerConfigurations,
26 | background,
27 | outputJPEG,
28 | } = require("../src/config");
29 | const {
30 | addMetadata,
31 | constructLayerToDna,
32 | DNA_DELIMITER,
33 | layersSetup,
34 | loadLayerImg,
35 | outputFiles,
36 | paintLayers,
37 | sortZIndex,
38 | writeMetaData,
39 | } = require("../src/main");
40 |
41 | const dnaFilePath = `${basePath}/build/_dna.json`;
42 | const outputDir = `${basePath}/build/rebuilt`;
43 |
44 | const setup = () => {
45 | if (fs.existsSync(outputDir)) {
46 | fs.rmSync(outputDir, {
47 | recursive: true,
48 | });
49 | }
50 | fs.mkdirSync(outputDir);
51 | fs.mkdirSync(path.join(outputDir, "/json"));
52 | fs.mkdirSync(path.join(outputDir, "/images"));
53 | // fs.mkdirSync(path.join(metadataBuildPath, "/json"));
54 | };
55 |
56 | function parseEditionNumFromDNA(dnaStrand) {
57 | // clean dna of edition num
58 | const editionExp = /\d+\//;
59 | return Number(editionExp.exec(dnaStrand)[0].replace("/", ""));
60 | }
61 |
62 | function regenerateSingleMetadataFile() {
63 | const metadata = [];
64 | const metadatafiles = fs.readdirSync(path.join(outputDir, "/json"));
65 |
66 | console.log("\nBuilding _metadata.json");
67 |
68 | metadatafiles.forEach((file) => {
69 | const data = fs.readFileSync(path.join(outputDir, "/json", file));
70 | metadata.push(JSON.parse(data));
71 | });
72 |
73 | fs.writeFileSync(
74 | path.join(outputDir, "json", "_metadata.json"),
75 | JSON.stringify(metadata, null, 2)
76 | );
77 | }
78 | /**
79 | * Randomly selects a number within the range of built images.
80 | * Since images and json files in the build folder are assumed to be identical,
81 | * we index of the length of the images directory.
82 | *
83 | * @param {String} image incomong filename
84 | * @param {Number} randomID new index to replace existing image/json files
85 | * @param {String} sourcePath path to source files
86 | * @param {Object} options command options object
87 | */
88 |
89 | /**
90 | * TODO: Add layer config index to dna so we can reconstruct
91 | * This currently only supports a single layer config
92 | */
93 |
94 | const regenerate = async (dnaData, options) => {
95 | /**
96 | * TODO:
97 | * The Dna needs to store background generation color
98 | * if it is to be re constructed properly
99 | */
100 | const canvas = createCanvas(format.width, format.height);
101 | const ctxMain = canvas.getContext("2d");
102 | let layerConfigIndex = 0;
103 | let abstractedIndexes = [];
104 | let drawIndex = 0;
105 | for (let i = 0; i <= dnaData.length - 1; i++) {
106 | // set abstractedIndexes from DNA
107 |
108 | const edition = parseEditionNumFromDNA(dnaData[i]);
109 | abstractedIndexes.push(edition);
110 | }
111 |
112 | const layers = layersSetup(layerConfigurations[layerConfigIndex].layersOrder);
113 | console.log({ drawIndex, len: dnaData.length });
114 | while (drawIndex < dnaData.length) {
115 | const dnaStrand = dnaData[drawIndex];
116 | let loadedElements = [];
117 | // loop over the dna data, check if it is an array or a string, if string, make arayy
118 | options.debug && options.verbose
119 | ? console.log("dna strand type", typeof dnaStrand)
120 | : null;
121 | options.debug && options.verbose
122 | ? console.log(`DNA for index ${drawIndex}: \n`, dnaStrand)
123 | : null;
124 |
125 | // clean dna of edition num
126 | const editionExp = /\d+\//;
127 |
128 | let images =
129 | typeof dnaStrand === "object"
130 | ? dnaStrand.replace(editionExp, "").join(DNA_DELIMITER)
131 | : dnaStrand.replace(editionExp, "");
132 |
133 | options.debug ? console.log("Rebuilding DNA:", images) : null;
134 | if (options.omit) {
135 | const dnaImages = images.split(DNA_DELIMITER);
136 | // remove every item whose address index matches the omitIndex
137 | let elementsToDelete = [];
138 | dnaImages.forEach((element, index) => {
139 | if (element.startsWith(`${options.omit}.`)) {
140 | elementsToDelete.push(index);
141 | }
142 | });
143 | const removedDnaImages = dnaImages.filter(
144 | (el, index) => !elementsToDelete.includes(index)
145 | );
146 |
147 | images = removedDnaImages.join(DNA_DELIMITER);
148 | }
149 |
150 | let results = constructLayerToDna(images, layers);
151 |
152 | // then, draw each layer using the address lookup
153 | // reduce the stacked and nested layer into a single array
154 | const allImages = results.reduce((images, layer) => {
155 | return [...images, ...layer.selectedElements];
156 | }, []);
157 |
158 | // sort by z-index.
159 | sortZIndex(allImages).forEach((layer) => {
160 | loadedElements.push(loadLayerImg(layer));
161 | });
162 |
163 | await Promise.all(loadedElements).then(async (renderObjectArray) => {
164 | // has background information?
165 | const bgHSL = dnaStrand.match(/(___.*)/);
166 | const generateBG = eval(options.background?.replace(/\s+/g, ""));
167 | if (generateBG != false && bgHSL) {
168 | background.HSL = bgHSL[0].replace("___", "");
169 | }
170 | if (!generateBG) {
171 | background.generate = false;
172 | }
173 | const layerData = {
174 | dnaStrand,
175 | layerConfigIndex,
176 | abstractedIndexes,
177 | _background: background,
178 | };
179 | paintLayers(ctxMain, renderObjectArray, layerData);
180 |
181 | outputFiles(abstractedIndexes, layerData, outputDir, canvas);
182 | drawIndex++;
183 | abstractedIndexes.shift();
184 | });
185 | }
186 | };
187 |
188 | program
189 | .option("-o, --omit ", "omit any given layer by layer index")
190 | .option(
191 | "-i, --startIndex ",
192 | "Then num.to start Output naming at, default is 0"
193 | )
194 |
195 | .option(
196 | "-b, --background ",
197 | "override the config generate background bool"
198 | )
199 | .option("-s, --source ", "Optional source path of _dna.json")
200 | .option("-d, --debug", "display additional logging")
201 | .option("-v, --verbose", "display even more additional logging")
202 | .action(async (options, command) => {
203 | const dnaData = options.source
204 | ? require(path.join(basePath, options.source))
205 | : require(dnaFilePath);
206 |
207 | options.debug && options.verbose
208 | ? console.log("Loaed DNA data\n", dnaData)
209 | : null;
210 |
211 | console.log(chalk.greenBright.inverse(`\nRegenerating images..`));
212 | options.omit
213 | ? console.log(`omitting layer at index ${options.omit}`)
214 | : null;
215 | options.background
216 | ? console.log(`Generate backgrounds ${options.background}`)
217 | : null;
218 | options.startIndex
219 | ? console.log(`Outputting files starting at index ${options.startIndex}`)
220 | : null;
221 |
222 | setup();
223 | await regenerate(dnaData, options);
224 | regenerateSingleMetadataFile();
225 | console.log(chalk.green("DONE"));
226 | });
227 |
228 | program.parse();
229 |
--------------------------------------------------------------------------------
/utils/regenerate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /**
3 | * The regeneration util uses the output _dna.json file to "continue" the same
4 | * uniqueness check the main generator uses when running the inital generation.
5 | *
6 | * This util takes an id number and generates an _additional_ unique DNA sequence,
7 | * and replaces the existing image and json files of the same id.
8 | *
9 | * It is assumed that the item is being regenerated because of an issue with
10 | * the DNA (picked traits), and that DNA is left in the _dna.json file so
11 | * (while changes are low) that item is not recreated again.
12 | */
13 |
14 | const isLocal = typeof process.pkg === "undefined";
15 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
16 | const fs = require("fs");
17 | const path = require("path");
18 | const { Command } = require("commander");
19 | const program = new Command();
20 | const { createCanvas } = require("canvas");
21 |
22 | const chalk = require("chalk");
23 |
24 | const jsonDir = `${basePath}/build/json`;
25 | const imageDir = `${basePath}/build/images`;
26 | const dnaFilePath = `${basePath}/build/_dna.json`;
27 | const metadataFilePath = `${basePath}/build/json/_metadata.json`;
28 |
29 | const {
30 | format,
31 | background,
32 | uniqueDnaTorrance,
33 | layerConfigurations,
34 | outputJPEG,
35 | startIndex,
36 | } = require(path.join(basePath, "/src/config.js"));
37 |
38 | const {
39 | createDna,
40 | DNA_DELIMITER,
41 | isDnaUnique,
42 | paintLayers,
43 | layersSetup,
44 | constructLayerToDna,
45 | loadLayerImg,
46 | addMetadata,
47 | postProcessMetadata,
48 | } = require(path.join(basePath, "/src/main.js"));
49 |
50 | let failedCount = 0;
51 | let attributesList = [];
52 | const canvas = createCanvas(format.width, format.height);
53 | const ctxMain = canvas.getContext("2d");
54 |
55 | const getDNA = () => {
56 | const flat = JSON.parse(fs.readFileSync(dnaFilePath));
57 | return flat.map((dnaStrand) => dnaStrand.split(DNA_DELIMITER));
58 | // .filter((item) => /^[0-9]{1,6}.json/g.test(item));
59 | };
60 |
61 | const createItem = (layers) => {
62 | let newDna = createDna(layers);
63 | const existingDna = getDNA();
64 | if (isDnaUnique(existingDna, newDna)) {
65 | return { newDna, layerImages: constructLayerToDna(newDna, layers) };
66 | } else {
67 | failedCount++;
68 | createItem(layers);
69 | if (failedCount >= uniqueDnaTorrance) {
70 | console.log(
71 | chalk.redBright(
72 | `You need more layers or elements to create a new, unique item`
73 | )
74 | );
75 | process.exit();
76 | }
77 | }
78 | };
79 |
80 | const outputFiles = (_id, layerData, options) => {
81 | const { newDna, abstractedIndexes } = layerData;
82 |
83 | // Save the image
84 | fs.writeFileSync(
85 | `${imageDir}/${_id}${outputJPEG ? ".jpg" : ".png"}`,
86 | canvas.toBuffer(`${outputJPEG ? "image/jpeg" : "image/png"}`)
87 | );
88 |
89 | const { _imageHash, _prefix, _offset } = postProcessMetadata(layerData);
90 |
91 | const metadata = addMetadata(newDna, abstractedIndexes[0], {
92 | _prefix,
93 | _offset,
94 | _imageHash,
95 | });
96 |
97 | options.debug ? console.log({ metadata }) : null;
98 | // save the metadata json
99 | fs.writeFileSync(`${jsonDir}/${_id}.json`, JSON.stringify(metadata, null, 2));
100 | console.log(chalk.bgGreenBright.black(`Recreated item: ${_id}`));
101 | //TODO: update and output _metadata.json
102 |
103 | const originalMetadata = JSON.parse(fs.readFileSync(metadataFilePath));
104 | const updatedMetadata = [...originalMetadata];
105 | const editionIndex = _id - startIndex;
106 | updatedMetadata[editionIndex] = metadata;
107 | fs.writeFileSync(metadataFilePath, JSON.stringify(updatedMetadata, null, 2));
108 | };
109 |
110 | const regenerateItem = (_id, options) => {
111 | // get the dna lists
112 | // FIgure out which layer config set it's from
113 | const layerEdition = layerConfigurations.reduce((acc, config) => {
114 | return [...acc, config.growEditionSizeTo];
115 | }, []);
116 | const layerConfigIndex = layerEdition.findIndex(
117 | (editionCount) => _id <= editionCount
118 | );
119 |
120 | const layers = layersSetup(layerConfigurations[layerConfigIndex].layersOrder);
121 |
122 | const { newDna, layerImages } = createItem(layers);
123 | options.debug ? console.log({ newDna }) : null;
124 |
125 | // regenerate an image using main functions
126 | const allImages = layerImages.reduce((images, layer) => {
127 | return [...images, ...layer.selectedElements];
128 | }, []);
129 |
130 | const loadedElements = allImages.reduce((acc, layer) => {
131 | return [...acc, loadLayerImg(layer)];
132 | }, []);
133 |
134 | Promise.all(loadedElements).then((renderObjectArray) => {
135 | const layerData = {
136 | newDna,
137 | layerConfigIndex,
138 | abstractedIndexes: [_id],
139 | _background: background,
140 | };
141 | // paint layers to global canvas context.. no return value
142 | paintLayers(ctxMain, renderObjectArray, layerData);
143 | outputFiles(_id, layerData, options);
144 |
145 | // update the _dna.json
146 | const existingDna = getDNA();
147 | const existingDnaFlat = existingDna.map((dna) => dna.join(DNA_DELIMITER));
148 |
149 | const updatedDnaList = [...existingDnaFlat];
150 | // find the correct entry and update it
151 | const dnaIndex = _id - startIndex;
152 | updatedDnaList[dnaIndex] = newDna;
153 |
154 | options.debug
155 | ? console.log(
156 | chalk.redBright(`replacing old DNA:\n`, existingDnaFlat[dnaIndex])
157 | )
158 | : null;
159 | options.debug
160 | ? console.log(
161 | chalk.greenBright(`\nWith new DNA:\n`, updatedDnaList[dnaIndex])
162 | )
163 | : null;
164 |
165 | fs.writeFileSync(
166 | path.join(dnaFilePath),
167 | JSON.stringify(updatedDnaList, null, 2)
168 | );
169 | });
170 | };
171 |
172 | program
173 | .argument("")
174 | .option("-d, --debug", "display some debugging")
175 | .action((id, options, command) => {
176 | options.debug
177 | ? console.log(chalk.greenBright.inverse(`Regemerating #${id}`))
178 | : null;
179 |
180 | regenerateItem(id, options);
181 | });
182 |
183 | program.parse();
184 |
--------------------------------------------------------------------------------
/utils/regenerateMetadata.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const isLocal = typeof process.pkg === "undefined";
4 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
5 | const fs = require("fs");
6 | const path = require("path");
7 | const jsonDir = `${basePath}/build/json`;
8 | const metadataFilePath = `${basePath}/build/json/_metadata.json`;
9 |
10 | const getIndividualJsonFiles = () => {
11 | return fs
12 | .readdirSync(jsonDir)
13 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
14 | };
15 |
16 | // Identify json files
17 | const jsonFiles = getIndividualJsonFiles();
18 | console.log(`Found ${jsonFiles.length} json files in "${jsonDir}" to process`);
19 |
20 | // Iterate, open and put in metadata list
21 | const metadata = jsonFiles
22 | .map((file) => {
23 | const rawdata = fs.readFileSync(`${jsonDir}/${file}`);
24 | return JSON.parse(rawdata);
25 | })
26 | .sort((a, b) => parseInt(a.edition) - parseInt(b.edition));
27 |
28 | console.log(
29 | `Extracted and sorted metadata files. Writing to file: ${metadataFilePath}`
30 | );
31 | fs.writeFileSync(metadataFilePath, JSON.stringify(metadata, null, 2));
32 |
--------------------------------------------------------------------------------
/utils/removeTrait.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const isLocal = typeof process.pkg === "undefined";
4 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
5 | const fs = require("fs");
6 | const path = require("path");
7 | const { Command } = require("commander");
8 | const program = new Command();
9 |
10 | const chalk = require("chalk");
11 | const jsonDir = `${basePath}/build/json`;
12 |
13 | const getIndividualJsonFiles = () => {
14 | return fs
15 | .readdirSync(jsonDir)
16 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
17 | };
18 |
19 | program
20 | .argument("")
21 | .option("-d, --debug", "display some debugging")
22 | .action((trait, options, command) => {
23 | const jsonFiles = getIndividualJsonFiles();
24 | options.debug
25 | ? console.log(
26 | `Found ${jsonFiles.length} json files in "${jsonDir}" to process`
27 | )
28 | : null;
29 |
30 | console.log(chalk.greenBright.inverse(`Removing ${trait}`));
31 | jsonFiles.forEach((filename) => {
32 | // read the contents
33 | options.debug ? console.log(`removing ${trait} from ${filename}`) : null;
34 | const contents = JSON.parse(fs.readFileSync(`${jsonDir}/${filename}`));
35 |
36 | const hasTrait = contents.attributes.some(
37 | (attr) => attr.trait_type === trait
38 | );
39 |
40 | if (!hasTrait) {
41 | console.log(chalk.yellow(`"${trait}" not found in ${filename}`));
42 | }
43 | // remove the trait from attributes
44 |
45 | contents.attributes = contents.attributes.filter(
46 | (traits) => traits.trait_type !== trait
47 | );
48 |
49 | // write the file
50 | fs.writeFileSync(
51 | `${jsonDir}/${filename}`,
52 | JSON.stringify(contents, null, 2)
53 | );
54 |
55 | options.debug
56 | ? console.log(
57 | hasTrait ? chalk.greenBright("Removed \n") : "…skipped \n"
58 | )
59 | : null;
60 | });
61 | });
62 |
63 | program.parse();
64 |
--------------------------------------------------------------------------------
/utils/replace.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * This utility tool is designed specifically for the scenario in which you
5 | * would like to replace one or many tokens with one off, non-generated items,
6 | * (or any image/metadata combo that does NOT conflict with the generators permutation DNA checks)
7 |
8 | */
9 |
10 | const isLocal = typeof process.pkg === "undefined";
11 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
12 | const fs = require("fs");
13 | const path = require("path");
14 | const { Command } = require("commander");
15 | const program = new Command();
16 | const chalk = require("chalk");
17 | const keccak256 = require("keccak256");
18 |
19 | const builtImageDir = `${basePath}/build/images`;
20 | const builtJsonDir = `${basePath}/build/json`;
21 |
22 | const getIndividualJsonFiles = (sourcePath) => {
23 | return fs
24 | .readdirSync(sourcePath)
25 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
26 | };
27 |
28 | const getIndividualImageFiles = (sourcePath) => {
29 | return fs.readdirSync(sourcePath);
30 | };
31 |
32 | /**
33 | * Given some input, creates a sha256 hash.
34 | * @param {Object} input
35 | */
36 | const hash = (input) => {
37 | const hashable = typeof input === Buffer ? input : JSON.stringify(input);
38 | return keccak256(hashable).toString("hex");
39 | };
40 |
41 | /**
42 | * Resolve an objects nested path from string
43 | * @param {String} path string path to object
44 | * @param {Object} obj Optional to directly return the value
45 | * @returns
46 | */
47 | function resolveNested(stringpath, obj) {
48 | return stringpath
49 | .split(".") // split string based on `.`
50 | .reduce(function (o, k) {
51 | return o && o[k]; // get inner property if `o` is defined else get `o` and return
52 | }, obj); // set initial value as object
53 | }
54 |
55 | /**
56 | * Randomly selects a number within the range of built images.
57 | * Since images and json files in the build folder are assumed to be identical,
58 | * we index of the length of the images directory.
59 | *
60 | * @param {String} image incomong filename
61 | * @param {Number} randomID new index to replace existing image/json files
62 | * @param {String} sourcePath path to source files
63 | * @param {Object} options command options object
64 | */
65 | const replace = (image, randomID, sourcePath, options) => {
66 | options.sneak
67 | ? console.log(chalk.cyan(`Randomly replacing ${image} -> ${randomID} `))
68 | : null;
69 | // console.log({ image, index, sourcePath });
70 | const imageNum = image.substr(0, image.lastIndexOf(".")) || image;
71 | const imageExtension = image.split(".").pop();
72 |
73 | // read the data, replace the numbers
74 | const currentImage = fs.readFileSync(path.join(sourcePath, "images", image));
75 | try {
76 | const currentData = fs.readFileSync(
77 | path.join(sourcePath, "json", `${imageNum}.json`)
78 | );
79 |
80 | const newMetadata = JSON.parse(currentData);
81 | // hash the image
82 | const imageHash = hash(currentImage);
83 | newMetadata.imageHash = imageHash;
84 |
85 | // replace all ## with proper edition number
86 | const symbol = options.replacementSymbol
87 | ? new RegExp(options.replacementSymbol, "gm")
88 | : /##/gm;
89 |
90 | options.debug ? console.log({ symbol }) : null;
91 | const updatedMetadata = JSON.stringify(newMetadata, null, 2).replace(
92 | symbol,
93 | randomID
94 | );
95 |
96 | options.debug
97 | ? console.log(`Generating hash from ${image}`, imageHash)
98 | : null;
99 |
100 | const globalMetadata = JSON.parse(
101 | fs.readFileSync(path.join(builtJsonDir, "_metadata.json"))
102 | );
103 |
104 | // update the object in the globalFile,
105 | const updateIndex = globalMetadata.findIndex((item) => {
106 | const globalIndex = options.identifier
107 | ? resolveNested(options.identifier, item)
108 | : item.edition;
109 | return globalIndex === randomID;
110 | });
111 | if (updateIndex < 0) {
112 | throw new Error(
113 | `Could not find the identifier, "${
114 | options.identifier ? options.identifier : "edition"
115 | }" in _metadata.json. Check that it is correct and try again.`
116 | );
117 | }
118 | options.debug
119 | ? console.log(`updating _metadata.json index [${updateIndex}]`)
120 | : null;
121 |
122 | const updatedGlobalMetadata = globalMetadata;
123 | // set the new data in the _metadata.json
124 | updatedGlobalMetadata[updateIndex] = JSON.parse(updatedMetadata);
125 | // everything looks good to write files.
126 | // overwrite the build json file
127 | fs.writeFileSync(
128 | path.join(builtJsonDir, `${randomID}.json`),
129 | updatedMetadata
130 | );
131 | // overwrite the build image file
132 | fs.writeFileSync(
133 | path.join(builtImageDir, `${randomID}.${imageExtension}`),
134 | currentImage
135 | );
136 |
137 | // overwrite the build image file
138 | fs.writeFileSync(
139 | path.join(builtJsonDir, "_metadata.json"),
140 | JSON.stringify(updatedGlobalMetadata, null, 2)
141 | );
142 | } catch (error) {
143 | console.error(error);
144 | throw new Error(`Image ${imageNum} is missing a matching JSON file`);
145 | }
146 | };
147 |
148 | program
149 | .argument("")
150 | .option("-d, --debug", "display additional logging")
151 | .option("-s, --sneak", "output the random ID's that are being replaced")
152 | .option(
153 | "-r, --replacementSymbol ",
154 | "The character used as a placeholder for edition numbers"
155 | )
156 | .option(
157 | "-i, --identifier ",
158 | 'Change the default object identifier/location for the edition/id number. defaults to "edition"'
159 | )
160 | .action((source, options, command) => {
161 | // get source to replace from
162 | // replaceFrom source/ -> destination
163 |
164 | // get source to replace to, image + json
165 | const imageSource = path.join(basePath, source, `/images`);
166 | const dataSource = path.join(basePath, source, `/json`);
167 | const imageFiles = getIndividualImageFiles(imageSource);
168 | const dataFiles = getIndividualJsonFiles(dataSource);
169 | // global variable to keep track of which ID's have been used.
170 | const randomIDs = new Set();
171 |
172 | console.log(
173 | chalk.greenBright.inverse(`\nPulling images and data from ${source}`)
174 | );
175 | options.debug
176 | ? console.log(
177 | `\tFound ${imageFiles.length} images in "${imageSource}"
178 | and
179 | ${dataFiles.length} in ${dataSource}`
180 | )
181 | : null;
182 |
183 | // Main functions in trycatch block for cleaner error logging if throwing errors.
184 | // try {
185 | if (imageFiles.length !== dataFiles.length) {
186 | throw new Error(
187 | "Number of images and number of metadata JSON files do not match. \n Are you Missing one?"
188 | );
189 | }
190 | // get the length of images in the build folder
191 | const totalCount = fs.readdirSync(builtImageDir).length;
192 | while (randomIDs.size < imageFiles.length) {
193 | randomIDs.add(Math.floor(Math.random() * (totalCount - 1 + 1) + 1));
194 | }
195 | const randomIDArray = Array.from(randomIDs);
196 |
197 | // randomly choose a number
198 | imageFiles.forEach((image, index) =>
199 | replace(image, randomIDArray[index], path.join(basePath, source), options)
200 | );
201 | // if image is missing accompanying json, throw error.
202 | console.log(
203 | chalk.green(
204 | `\nSuccessfully inserted ${chalk.bgGreenBright.black(
205 | imageFiles.length
206 | )} Images and Data Files into the build directories\n`
207 | )
208 | );
209 | // } catch (error) {
210 | // console.error(chalk.bgRedBright.black(error));
211 | // }
212 |
213 | // side effects?
214 | // does it affect rarity data util?
215 | // provenance hash?
216 |
217 | // jsonFiles.forEach((filename) => {
218 | // // read the contents
219 | // options.debug ? console.log(`removing ${trait} from ${filename}`) : null;
220 | // const contents = JSON.parse(fs.readFileSync(`${jsonDir}/${filename}`));
221 |
222 | // const hasTrait = contents.attributes.some(
223 | // (attr) => attr.trait_type === trait
224 | // );
225 |
226 | // if (!hasTrait) {
227 | // console.log(chalk.yellow(`"${trait}" not found in ${filename}`));
228 | // }
229 | // // remove the trait from attributes
230 |
231 | // contents.attributes = contents.attributes.filter(
232 | // (traits) => traits.trait_type !== trait
233 | // );
234 |
235 | // // write the file
236 | // fs.writeFileSync(
237 | // `${jsonDir}/${filename}`,
238 | // JSON.stringify(contents, null, 2)
239 | // );
240 |
241 | // options.debug
242 | // ? console.log(
243 | // hasTrait ? chalk.greenBright("Removed \n") : "…skipped \n"
244 | // )
245 | // : null;
246 | // });
247 | });
248 |
249 | program.parse();
250 |
--------------------------------------------------------------------------------
/utils/resize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script helps to resize your images in the
3 | * `build/images` folder for the `displayUri` and
4 | * `thumbnailUri` in Tezos metadata.
5 | */
6 |
7 | const sharp = require("sharp");
8 | const fs = require("fs");
9 | const path = require("path");
10 |
11 | const isLocal = typeof process.pkg === "undefined";
12 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
13 | const imagesDir = `${basePath}/build/images`;
14 | const tezosConfig = require(`${basePath}/Tezos/tezos_config.js`);
15 |
16 | const resizeImagePath = {
17 | displayUri: path.join(basePath, "build/displayUri/"),
18 | thumbnailUri: path.join(basePath, "build/thumbnailUri/"),
19 | };
20 |
21 | function getAllImages(dir) {
22 | if (!fs.existsSync(imagesDir)) {
23 | console.log(`Images folder doesn't exist.`);
24 | return;
25 | }
26 |
27 | const images = fs
28 | .readdirSync(imagesDir)
29 | .filter((item) => {
30 | let extension = path.extname(`${dir}${item}`);
31 | if (extension == ".png" || extension == ".jpg") {
32 | return item;
33 | }
34 | })
35 | .map((i) => ({
36 | filename: i,
37 | path: `${dir}/${i}`,
38 | }));
39 |
40 | return images;
41 | }
42 |
43 | function renderResizedImages(images, path, sizeW, sizeH) {
44 | /**
45 | * images: A list of images.
46 | * path: Path to render the resized images.
47 | * sizeH: Height of resized images.
48 | * sizeW: Width of resized images.
49 | */
50 | if (!fs.existsSync(path)) {
51 | console.log(`Images folder doesn't exist.`);
52 | return;
53 | }
54 | if (!path.endsWith("/")) {
55 | path += `/`;
56 | }
57 |
58 | images.forEach((image) => {
59 | const newPath = `${path}${image.filename}`;
60 | console.log(`Converting ${image.path}`);
61 | sharp(image.path)
62 | .resize(sizeW, sizeH)
63 | .toFile(newPath, (err, info) => {
64 | if (!err) {
65 | console.log(`✅ Rendered ${newPath}.`);
66 | } else {
67 | console.error(`Got error ${err}`);
68 | }
69 | });
70 | });
71 | }
72 |
73 | const createPath = (path) => {
74 | if (!fs.existsSync(path)) {
75 | fs.mkdirSync(path);
76 | return path;
77 | } else {
78 | console.log(`${path} already exists.`);
79 | }
80 | };
81 | console.log(tezosConfig.size);
82 |
83 | function transformForTez(images) {
84 | // Converting for the `displayUri`.
85 | createPath(resizeImagePath.displayUri);
86 | console.log("------------> Display", resizeImagePath.displayUri);
87 | renderResizedImages(
88 | images,
89 | resizeImagePath.displayUri,
90 | tezosConfig.size.displayUri.width,
91 | tezosConfig.size.displayUri.height
92 | );
93 |
94 | createPath(resizeImagePath.thumbnailUri);
95 |
96 | console.log("------------> Thumbnail", resizeImagePath.thumbnailUri);
97 | renderResizedImages(
98 | images,
99 | resizeImagePath.thumbnailUri,
100 | tezosConfig.size.thumbnailUri.width,
101 | tezosConfig.size.thumbnailUri.height
102 | );
103 | console.log(`Done!`);
104 | }
105 |
106 | const images = getAllImages(imagesDir);
107 | console.log(`Images list`);
108 | console.table(images);
109 | transformForTez(images);
110 |
--------------------------------------------------------------------------------
/utils/tezos.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const path = require("path");
5 | const isLocal = typeof process.pkg === "undefined";
6 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
7 | const chalk = require("chalk");
8 |
9 | const tezosConfig = require(path.join(basePath, "/Tezos/tezos_config.js"));
10 |
11 | const jsonDir = `${basePath}/build/json`;
12 |
13 | const metadataBuildPath = `${basePath}/build/tezos`;
14 | const metadataConfigPath = `${basePath}/build/tezos`;
15 |
16 | const setup = () => {
17 | if (fs.existsSync(metadataBuildPath)) {
18 | fs.rmSync(metadataBuildPath, {
19 | recursive: true,
20 | });
21 | }
22 | fs.mkdirSync(metadataBuildPath);
23 | fs.mkdirSync(path.join(metadataBuildPath, "/json"));
24 | };
25 |
26 | const getIndividualJsonFiles = () => {
27 | return fs
28 | .readdirSync(jsonDir)
29 | .filter((item) => /^[0-9]{1,6}.json/g.test(item));
30 | };
31 |
32 | setup();
33 | console.log(chalk.cyan.black("Beginning Tezos conversion"));
34 | console.log(
35 | chalk.cyan(`\nExtracting files.\nWriting to folder: ${metadataBuildPath}`)
36 | );
37 |
38 | // Iterate, open and put in metadata list
39 | const jsonFiles = getIndividualJsonFiles();
40 |
41 | const stringifySize = (obj) => {
42 | return `${obj.width}x${obj.height}`;
43 | };
44 | const metadatas = [];
45 | jsonFiles.forEach((file) => {
46 | let nameWithoutExtension = file.slice(0, -4);
47 | let editionCountFromFileName = Number(nameWithoutExtension);
48 |
49 | const rawData = fs.readFileSync(`${jsonDir}/${file}`);
50 | const jsonData = JSON.parse(rawData);
51 |
52 | const restructuredAttributes = jsonData.attributes.reduce(
53 | (properties, attr) => {
54 | return [...properties, { name: attr.trait_type, value: attr.value }];
55 | },
56 | []
57 | );
58 |
59 | let tempMetadata = {
60 | edition: jsonData.edition,
61 | name: jsonData.name,
62 | description: jsonData.description,
63 | artifactUri: jsonData.image,
64 | displayUri: `${tezosConfig.baseDisplayUri}/${jsonData.edition}.png`,
65 | thumbnailUri: `${tezosConfig.baseThumbnailUri}/${jsonData.edition}.png`,
66 | decimals: tezosConfig.decimals,
67 | creators: tezosConfig.creators,
68 | isBooleanAmount: tezosConfig.isBooleanAmount,
69 | symbol: tezosConfig.symbol,
70 | rights: tezosConfig.rights,
71 | shouldPreferSymbol: tezosConfig.shouldPreferSymbol,
72 |
73 | attributes: [...restructuredAttributes],
74 |
75 | // Defining formats
76 | formats: [
77 | {
78 | mimeType: "image/png",
79 | uri: jsonData.image,
80 | dimensions: {
81 | value: stringifySize(tezosConfig.size.artifactUri),
82 | unit: "px",
83 | },
84 | },
85 | {
86 | mimeType: "image/png",
87 | uri: `${tezosConfig.baseDisplayUri}/${jsonData.edition}.png`,
88 | dimensions: {
89 | value: stringifySize(tezosConfig.size.displayUri),
90 | unit: "px",
91 | },
92 | },
93 | {
94 | mimeType: "image/png",
95 | uri: `${tezosConfig.baseThumbnailUri}/${jsonData.edition}.png`,
96 | dimensions: {
97 | value: stringifySize(tezosConfig.size.thumbnailUri),
98 | unit: "px",
99 | },
100 | },
101 | ],
102 |
103 | royalties: {
104 | decimals: 3,
105 | shares: tezosConfig.royalties,
106 | },
107 | };
108 | fs.writeFileSync(
109 | path.join(
110 | `${metadataConfigPath}`,
111 | "json",
112 | `${editionCountFromFileName}.json`
113 | ),
114 | JSON.stringify(tempMetadata, null, 2)
115 | );
116 | metadatas.push(tempMetadata);
117 | });
118 |
119 | fs.writeFileSync(
120 | path.join(`${metadataConfigPath}`, "json", `_metadata.json`),
121 | JSON.stringify(metadatas, null, 2)
122 | );
123 | console.log(`\nFinished converting json metadata files to Tezos Format.`);
124 | console.log(chalk.green(`\nConversion was finished successfully!\n`));
125 |
--------------------------------------------------------------------------------
/utils/updateInfo.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * Regenerate all the metadata from a build using the generated json files
5 | * for attribute and edition numbers, while using the _updated_ genrealo
6 | * metadata config from config.js to update the general fields.
7 | *
8 | * Options:
9 | * -r, --removeTrait , remove all instances of an attribute
10 | * -s, --skip , Remove/skip adding a general metadata filed, e.g, dna
11 | *
12 | */
13 |
14 | const path = require("path");
15 | const isLocal = typeof process.pkg === "undefined";
16 | const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);
17 | const fs = require("fs");
18 |
19 | console.log(path.join(basePath, "/src/config.js"));
20 | const { baseUri, description } = require(path.join(basePath, "/src/config.js"));
21 |
22 | const { Command } = require("commander");
23 | const program = new Command();
24 | const chalk = require("chalk");
25 |
26 | // read json data
27 | let rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`);
28 | let data = JSON.parse(rawdata);
29 |
30 | program
31 | .option(
32 | "-s, --skip ",
33 | " Remove/skip adding a general metadata filed, e.g, dna"
34 | )
35 | .option(
36 | "-r, --removeTrait ",
37 | "remove all instances of a trait from attributes"
38 | )
39 | .option("-n, --name ", "Rename the Name prefix for ALL tokens files.")
40 | .action((options) => {
41 | if (options) {
42 | console.log("Running with options", { options });
43 | }
44 | if (options.skip == "edition") {
45 | const error = new Error(
46 | "\nRemoving the edition field it not allowed in this script\n"
47 | );
48 | console.error(chalk.red(error));
49 | return false;
50 | }
51 | /**
52 | * loop over each loaded item, modify the data, and overwrite
53 | * the existing files.
54 | *
55 | * uses item.edition to ensure the proper number is used
56 | * insead of the loop index as images may have a different order.
57 | */
58 | data.forEach((item) => {
59 | item.image = `${baseUri}/${item.edition}.png`;
60 | item.description = description;
61 |
62 | if (options.name) {
63 | console.log(chalk.yellow(`Renaming token to ${options.name}`));
64 | item.name = `${options.name} #${item.edition}`;
65 | }
66 |
67 | if (options.skip) {
68 | console.log(
69 | chalk.yellow(`Skipping ${options.skip}: ${item[options.skip]}`)
70 | );
71 | delete item[options.skip];
72 | }
73 |
74 | if (options.removeTrait) {
75 | console.log(chalk.redBright(`Removing ${options.removeTrait}`));
76 | console.log({ item: item.attributes });
77 | item.attributes = item.attributes.filter(
78 | (trait) => trait.trait_type !== options.removeTrait
79 | );
80 | }
81 |
82 | fs.writeFileSync(
83 | `${basePath}/build/json/${item.edition}.json`,
84 | JSON.stringify(item, null, 2)
85 | );
86 | });
87 |
88 | fs.writeFileSync(
89 | `${basePath}/build/json/_metadata.json`,
90 | JSON.stringify(data, null, 2)
91 | );
92 | console.log(`\nUpdated baseUri for images to ===> ${baseUri}\n`);
93 | console.log(`Updated Description for all to ===> ${description}\n`);
94 | });
95 |
96 | program.parse();
97 |
--------------------------------------------------------------------------------