├── .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 | image 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 | ![10](https://user-images.githubusercontent.com/91582112/138395427-c8642f74-58d1-408b-94d1-7a97dda58d1a.jpg) 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 | ![image](https://user-images.githubusercontent.com/91582112/138395944-31032584-f1e5-4b7e-b8c6-c6ad49f0d2ba.png) 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 | ![Diagram by @juanicarmesi](<../.gitbook/assets/Branching with nested folders.png>) 14 | 15 | ![Example provided by @juanicarmesi](<../.gitbook/assets/Branching result.png>) 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 | ![Diagram provided by @juanicarmesi](<../.gitbook/assets/Branching with nested folders (1) (1) (1).png>) 8 | 9 | ![Diagram provided by @juanicarmesi](<../.gitbook/assets/Branching result.png>) 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 | ![image](https://user-images.githubusercontent.com/2608893/136727619-779221c2-0ec1-42a2-a1c6-144ba4587035.png) 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 | ![](<../../.gitbook/assets/image (2).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 | ![startIndex is the number the first generated images/json will start at. growEditionSizeTo determines how many will be generated.](<../../.gitbook/assets/Screen Shot 2022-02-21 at 9.53.59 AM.png>) 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 | ![](<../../.gitbook/assets/image (1).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 | ![10](https://user-images.githubusercontent.com/91582112/138395427-c8642f74-58d1-408b-94d1-7a97dda58d1a.jpg) 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 | ![Diagram provided by @juanicarmesi ](../.gitbook/assets/z-index.png) 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 | ![Example folder names to illustrate the usage of z#, ](<../.gitbook/assets/image (1) (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 | ![Required png's combined to make a single trait](<../.gitbook/assets/image (1).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 | ![](<../.gitbook/assets/image (2).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 | ![running with the -d debug flag will output the logging above to track what is being changed.](../.gitbook/assets/image.png) 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 | --------------------------------------------------------------------------------