diff --git a/README.md b/README.md index 2d71d3cd2..359349236 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,11 @@ You can mix up the `layerConfigurations` order on how the images are saved by se 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. -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. +If you want to play around with different blending modes, you can add a `blend: MODE.colorBurn` field to the layersOrder `options` object. + +If you need a layers to have a different opacity then you can add the `opacity: 0.7` field to the layersOrder `options` object as well. + +To use a different metadata attribute name you can add the `displayName: "Awesome Eye Color"` to the `options` object. All options are optional and can be addes on the same layer if you want to. Here is an example on how you can play around with both filter fields: @@ -118,11 +122,18 @@ const layerConfigurations = [ layersOrder: [ { name: "Background" }, { name: "Eyeball" }, - { name: "Eye color", blend: MODE.colorBurn }, + { + name: "Eye color", + options: { + blend: MODE.destinationIn, + opcacity: 0.2, + displayName: "Awesome Eye Color", + }, + }, { name: "Iris" }, { name: "Shine" }, - { name: "Bottom lid", blend: MODE.overlay, opacity: 0.7 }, - { name: "Top lid", opacity: 0.7 }, + { name: "Bottom lid", options: { blend: MODE.overlay, opacity: 0.7 } }, + { name: "Top lid" }, ], }, ]; @@ -214,12 +225,12 @@ That's it, you're done. ## Utils -### Updating baseUri for IPFS +### Updating baseUri for IPFS and description -You might possibly want to update the baseUri after you have ran your collection. To update the baseUri simply run: +You might possibly want to update the baseUri and description after you have ran your collection. To update the baseUri and description simply run: ```sh -node utils/updateBaseUri.js +npm run update_info ``` ### Generate a preview image @@ -227,15 +238,26 @@ node utils/updateBaseUri.js Create a preview image collage of your collection, run: ```sh -node utils/createPreviewCollage.js +npm run preview ``` -### Re-generate the \_metadata.json file +### Generate pixelated images from collection -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: +In order to convert images into pixelated images you would need a list of images that you want to convert. So run the generator first. + +Then simply run this command: ```sh -node utils/regenerateMetadata.js +npm run pixelate +``` + +All your images will be outputted in the `/build/pixel_images` directory. +If you want to change the ratio of the pixelation then you can update the ratio property on the `pixelFormat` object in the `src/config.js` file. The lower the number on the left, the more pixelated the image will be. + +```js +const pixelFormat = { + ratio: 5 / 128, +}; ``` ### Printing rarity data (Experimental feature) @@ -243,7 +265,7 @@ node utils/regenerateMetadata.js To see the percentages of each attribute across your collection, run: ```sh -node utils/rarityData.js +npm run rarity ``` The output will look something like this: diff --git a/src/blendMode.js b/constants/blend_mode.js similarity index 100% rename from src/blendMode.js rename to constants/blend_mode.js diff --git a/package.json b/package.json index d77d45204..f436ee5fc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ }, "scripts": { "build": "node index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "generate": "node index.js", + "rarity": "node utils/rarity.js", + "preview": "node utils/preview.js", + "pixelate": "node utils/pixelate.js", + "update_info": "node utils/update_info.js" }, "author": "Daniel Eugene Botha (HashLips)", "license": "MIT", diff --git a/src/config.js b/src/config.js index 37fb4ef7a..b139dbc77 100644 --- a/src/config.js +++ b/src/config.js @@ -3,14 +3,14 @@ const path = require("path"); const isLocal = typeof process.pkg === "undefined"; const basePath = isLocal ? process.cwd() : path.dirname(process.execPath); -const { MODE } = require(path.join(basePath, "src/blendMode.js")); +const { MODE } = require(path.join(basePath, "constants/blend_mode.js")); const description = "This is the description of your NFT project, remember to replace this"; const baseUri = "ipfs://NewUriToReplace"; const layerConfigurations = [ { - growEditionSizeTo: 10, + growEditionSizeTo: 5, layersOrder: [ { name: "Background" }, { name: "Eyeball" }, @@ -32,9 +32,28 @@ const format = { height: 512, }; +const text = { + only: false, + color: "#ffffff", + size: 20, + xGap: 40, + yGap: 40, + align: "left", + baseline: "top", + weight: "regular", + family: "Courier", + spacer: " => ", +}; + +const pixelFormat = { + ratio: 2 / 128, +}; + const background = { generate: true, brightness: "80%", + static: false, + default: "#000000", }; const extraMetadata = {}; @@ -62,4 +81,6 @@ module.exports = { shuffleLayerConfigurations, debugLogs, extraMetadata, + pixelFormat, + text, }; diff --git a/src/main.js b/src/main.js index 7cc6de6e7..403b9f046 100644 --- a/src/main.js +++ b/src/main.js @@ -11,7 +11,6 @@ const { createCanvas, loadImage } = require(path.join( )); const buildDir = path.join(basePath, "/build"); const layersDir = path.join(basePath, "/layers"); -console.log(path.join(basePath, "/src/config.js")); const { format, baseUri, @@ -23,13 +22,14 @@ const { shuffleLayerConfigurations, debugLogs, extraMetadata, + text, } = require(path.join(basePath, "/src/config.js")); const canvas = createCanvas(format.width, format.height); const ctx = canvas.getContext("2d"); var metadataList = []; var attributesList = []; var dnaList = new Set(); -const DNA_DELIMITER = '-'; +const DNA_DELIMITER = "-"; const buildSetup = () => { if (fs.existsSync(buildDir)) { @@ -80,11 +80,19 @@ const getElements = (path) => { const layersSetup = (layersOrder) => { const layers = layersOrder.map((layerObj, index) => ({ id: index, - name: layerObj.name, elements: getElements(`${layersDir}/${layerObj.name}/`), - blendMode: - layerObj["blend"] != undefined ? layerObj["blend"] : "source-over", - opacity: layerObj["opacity"] != undefined ? layerObj["opacity"] : 1, + name: + layerObj.options?.["displayName"] != undefined + ? layerObj.options?.["displayName"] + : layerObj.name, + blend: + layerObj.options?.["blend"] != undefined + ? layerObj.options?.["blend"] + : "source-over", + opacity: + layerObj.options?.["opacity"] != undefined + ? layerObj.options?.["opacity"] + : 1, })); return layers; }; @@ -103,7 +111,7 @@ const genColor = () => { }; const drawBackground = () => { - ctx.fillStyle = genColor(); + ctx.fillStyle = background.static ? background.default : genColor(); ctx.fillRect(0, 0, format.width, format.height); }; @@ -139,21 +147,43 @@ const loadLayerImg = async (_layer) => { }); }; -const drawElement = (_renderObject) => { +const addText = (_sig, x, y, size) => { + ctx.fillStyle = text.color; + ctx.font = `${text.weight} ${size}pt ${text.family}`; + ctx.textBaseline = text.baseline; + ctx.textAlign = text.align; + ctx.fillText(_sig, x, y); +}; + +const drawElement = (_renderObject, _index, _layersLen) => { ctx.globalAlpha = _renderObject.layer.opacity; - ctx.globalCompositeOperation = _renderObject.layer.blendMode; - ctx.drawImage(_renderObject.loadedImage, 0, 0, format.width, format.height); + ctx.globalCompositeOperation = _renderObject.layer.blend; + text.only + ? addText( + `${_renderObject.layer.name}${text.spacer}${_renderObject.layer.selectedElement.name}`, + text.xGap, + text.yGap * (_index + 1), + text.size + ) + : ctx.drawImage( + _renderObject.loadedImage, + 0, + 0, + format.width, + format.height + ); + addAttributes(_renderObject); }; -const constructLayerToDna = (_dna = '', _layers = []) => { +const constructLayerToDna = (_dna = "", _layers = []) => { let mappedDnaToLayers = _layers.map((layer, index) => { let selectedElement = layer.elements.find( (e) => e.id == cleanDna(_dna.split(DNA_DELIMITER)[index]) ); return { name: layer.name, - blendMode: layer.blendMode, + blend: layer.blend, opacity: layer.opacity, selectedElement: selectedElement, }; @@ -161,7 +191,7 @@ const constructLayerToDna = (_dna = '', _layers = []) => { return mappedDnaToLayers; }; -const isDnaUnique = (_DnaList = new Set(), _dna = '') => { +const isDnaUnique = (_DnaList = new Set(), _dna = "") => { return !_DnaList.has(_dna); }; @@ -258,8 +288,12 @@ const startCreating = async () => { if (background.generate) { drawBackground(); } - renderObjectArray.forEach((renderObject) => { - drawElement(renderObject); + renderObjectArray.forEach((renderObject, index) => { + drawElement( + renderObject, + index, + layerConfigurations[layerConfigIndex].layersOrder.length + ); }); debugLogs ? console.log("Editions left to create: ", abstractedIndexes) diff --git a/utils/pixelate.js b/utils/pixelate.js new file mode 100644 index 000000000..0308f4173 --- /dev/null +++ b/utils/pixelate.js @@ -0,0 +1,84 @@ +const fs = require("fs"); +const path = require("path"); +const { createCanvas, loadImage } = require("canvas"); +const isLocal = typeof process.pkg === "undefined"; +const basePath = isLocal ? process.cwd() : path.dirname(process.execPath); +const buildDir = `${basePath}/build/pixel_images`; +const inputDir = `${basePath}/build/images`; +const { format, pixelFormat } = require(path.join(basePath, "/src/config.js")); +const console = require("console"); +const canvas = createCanvas(format.width, format.height); +const ctx = canvas.getContext("2d"); + +const buildSetup = () => { + if (fs.existsSync(buildDir)) { + fs.rmdirSync(buildDir, { recursive: true }); + } + fs.mkdirSync(buildDir); +}; + +const getImages = (_dir) => { + try { + return fs + .readdirSync(_dir) + .filter((item) => { + let extension = path.extname(`${_dir}${item}`); + if (extension == ".png" || extension == ".jpg") { + return item; + } + }) + .map((i) => { + return { + filename: i, + path: `${_dir}/${i}`, + }; + }); + } catch { + return null; + } +}; + +const loadImgData = async (_imgObject) => { + return new Promise(async (resolve) => { + const image = await loadImage(`${_imgObject.path}`); + resolve({ imgObject: _imgObject, loadedImage: image }); + }); +}; + +const draw = (_imgObject) => { + let size = pixelFormat.ratio; + let w = canvas.width * size; + let h = canvas.height * size; + ctx.imageSmoothingEnabled = false; + ctx.drawImage(_imgObject.loadedImage, 0, 0, w, h); + ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height); +}; + +const saveImage = (_loadedImageObject) => { + fs.writeFileSync( + `${buildDir}/${_loadedImageObject.imgObject.filename}`, + canvas.toBuffer("image/png") + ); +}; + +const startCreating = async () => { + const images = getImages(inputDir); + if (images == null) { + console.log("Please generate collection first."); + return; + } + let loadedImageObjects = []; + images.forEach((imgObject) => { + loadedImageObjects.push(loadImgData(imgObject)); + }); + await Promise.all(loadedImageObjects).then((loadedImageObjectArray) => { + loadedImageObjectArray.forEach((loadedImageObject) => { + draw(loadedImageObject); + saveImage(loadedImageObject); + console.log(`Pixelated image: ${loadedImageObject.imgObject.filename}`); + }); + }); +}; + +buildSetup(); +startCreating(); diff --git a/utils/createPreviewCollage.js b/utils/preview.js similarity index 100% rename from utils/createPreviewCollage.js rename to utils/preview.js diff --git a/utils/rarityData.js b/utils/rarity.js similarity index 92% rename from utils/rarityData.js rename to utils/rarity.js index f7d853a32..a17867927 100644 --- a/utils/rarityData.js +++ b/utils/rarity.js @@ -6,7 +6,6 @@ const basePath = isLocal ? process.cwd() : path.dirname(process.execPath); const fs = require("fs"); const layersDir = `${basePath}/layers`; -console.log(path.join(basePath, "/src/config.js")); const { layerConfigurations } = require(path.join(basePath, "/src/config.js")); const { getElements } = require("../src/main.js"); @@ -35,11 +34,14 @@ layerConfigurations.forEach((config) => { }; elementsForLayer.push(rarityDataElement); }); - + let layerName = + layer.options?.["displayName"] != undefined + ? layer.options?.["displayName"] + : layer.name; // don't include duplicate layers if (!rarityData.includes(layer.name)) { // add elements for each layer to chart - rarityData[layer.name] = elementsForLayer; + rarityData[layerName] = elementsForLayer; } }); }); @@ -47,7 +49,6 @@ layerConfigurations.forEach((config) => { // fill up rarity chart with occurrences from metadata data.forEach((element) => { let attributes = element.attributes; - attributes.forEach((attribute) => { let traitType = attribute.trait_type; let value = attribute.value; diff --git a/utils/regenerateMetadata.js b/utils/regenerateMetadata.js deleted file mode 100644 index 1a1563054..000000000 --- a/utils/regenerateMetadata.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; - -const isLocal = typeof process.pkg === "undefined"; -const basePath = isLocal ? process.cwd() : path.dirname(process.execPath); -const fs = require("fs"); -const path = require("path"); -const jsonDir = `${basePath}/build/json`; -const metadataFilePath = `${basePath}/build/json/_metadata.json`; - -const getIndividualJsonFiles = () => { - return fs - .readdirSync(jsonDir) - .filter((item) => /^[0-9]{1,6}.json/g.test(item)); -}; - -// Identify json files -const jsonFiles = getIndividualJsonFiles(); -console.log(`Found ${jsonFiles.length} json files in "${jsonDir}" to process`); - -// Iterate, open and put in metadata list -const metadata = jsonFiles - .map((file) => { - const rawdata = fs.readFileSync(`${jsonDir}/${file}`); - return JSON.parse(rawdata); - }) - .sort((a, b) => parseInt(a.edition) - parseInt(b.edition)); - -console.log( - `Extracted and sorted metadata files. Writing to file: ${metadataFilePath}` -); -fs.writeFileSync(metadataFilePath, JSON.stringify(metadata, null, 2)); diff --git a/utils/updateBaseUri.js b/utils/update_info.js similarity index 79% rename from utils/updateBaseUri.js rename to utils/update_info.js index 5d37318be..da3c17c8e 100644 --- a/utils/updateBaseUri.js +++ b/utils/update_info.js @@ -6,13 +6,14 @@ const basePath = isLocal ? process.cwd() : path.dirname(process.execPath); const fs = require("fs"); console.log(path.join(basePath, "/src/config.js")); -const { baseUri } = require(path.join(basePath, "/src/config.js")); +const { baseUri, description } = require(path.join(basePath, "/src/config.js")); // read json data let rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`); let data = JSON.parse(rawdata); data.forEach((item) => { + item.description = description; item.image = `${baseUri}/${item.edition}.png`; fs.writeFileSync( `${basePath}/build/json/${item.edition}.json`, @@ -26,3 +27,4 @@ fs.writeFileSync( ); console.log(`Updated baseUri for images to ===> ${baseUri}`); +console.log(`Updated description for images to ===> ${description}`);