Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge pull request #13 from caiaga/main #66

Open
wants to merge 17 commits into
base: v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,61 @@
# Welcome to HashLips 👄

Important: There is a new repo for this code.
[https://github.com/HashLips/hashlips_art_engine](https://github.com/HashLips/hashlips_art_engine)

All the code in these repos was created and explained by HashLips on the main YouTube channel.

To find out more please visit:

[📺 YouTube](https://www.youtube.com/channel/UC1LV4_VQGBJHTJjEWUmy8nA)

[👄 Discord](https://discord.com/invite/qh6MWhMJDN)

[💬 Telegram](https://t.me/hashlipsnft)

[🐦 Twitter](https://twitter.com/hashlipsnft)

[ℹ️ Website](https://hashlips.online/HashLips)

# generative-art-opensource
Create generative art by using the canvas api and node js, feel free to contribute to this repo with new ideas.

# Project Setup
- install `node.js` on your local system (https://nodejs.org/en/)
- clone the repository to your local system `[email protected]:HashLips/generative-art-opensource.git`
- run `yarn install` to install dependencies

# How to use
## Run the code
1. Run `node index.js`
2. Open the `./output` folder to find your generated images to use as NFTs, as well as the metadata to use for NFT marketplaces.

## Adjust the provided configuration and resources
### Configuration file
The file `./input/config.js` contains the following properties that can be adjusted to your preference in order to change the behavior of the NFT generation procedure:
- width: - of your image in pixels. Default: `1000px`
- height: - of your image in pixels. Default: `1000px`
- dir: - where image parts are stored. Default: `./input`
- description: - of your generated NFT. Default: `This is an NFT made by the coolest generative code.`
- baseImageUri: - URL base to access your NFTs from. This will be used by platforms to find your image resource. This expects the image to be accessible by it's id like `${baseImageUri}/${id}`.
- startEditionFrom: - number (int) to start naming NFTs from. Default: `1`
- editionSize: - number (int) to end edition at. Default: `10`
- editionDnaPrefix: - value (number or string) that indicates which dna from an edition is used there. I.e. dna `0` from to independent batches in the same edition may differ, and can be differentiated using this. Default: `0`
- rarityWeights: - allows to provide rarity categories and how many of each type to include in an edition. Default: `1 super_rare, 4 rare, 5 original`
- layers: list of layers that should be used to render the image. See next section for detail.

### Image layers
The image layers are different parts that make up a full image by overlaying on top of each other. E.g. in the example input content of this repository we start with the eyeball and layer features like the eye lids or iris on top to create the completed and unique eye, which we can then use as part of our NFT collection.
To ensure uniqueness, we want to add various features and multiple options for each of them in order to allow enough permutations for the amount of unique images we require.

To start, copy the layers/features and their images in a flat hierarchy at a directory of your choice (by default we expect them in `./input/`). The features should contain options for each rarity that is provided via the config file.

After adding the `layers`, adjust them accordingly in the `config.js` by providing the directory path, positioning and sizes.
Use the existing `addLayers` calls as guidance for how to add layers. This can either only use the name of the layer and will use default positioning (x=0, y=0) and sizes (width=configured width, height=configure height), or positioning and sizes can be provided for more flexibility.

### Allowing different rarities for certain rarity/layer combinations
It is possible to provide a percentage at which e.g. a rare item would contain a rare vs. common part in a given layer. This can be done via the `addRarityPercentForLayer` that can be found in the `config.js` as well.
This allows for more fine grained control over how much randomness there should be during the generation process, and allows a combination of common and rare parts.

# Development suggestions
- Preferably use VSCode with the prettifier plugin for a consistent coding style (or equivalent js formatting rules)
194 changes: 135 additions & 59 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,21 @@ const {
baseImageUri,
editionSize,
startEditionFrom,
endEditionAt,
rarityWeights,
} = require("./input/config.js");
const console = require("console");
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
var metadataList = [];
var attributesList = [];
var dnaList = [];

// saves the generated image to the output folder, using the edition count as the name
const saveImage = (_editionCount) => {
fs.writeFileSync(
`./output/${_editionCount}.png`,
canvas.toBuffer("image/png")
);
};

// adds a signature to the top left corner of the canvas
const signImage = (_sig) => {
ctx.fillStyle = "#000000";
ctx.font = "bold 30pt Courier";
Expand All @@ -33,6 +31,7 @@ const signImage = (_sig) => {
ctx.fillText(_sig, 40, 40);
};

// generate a random color hue
const genColor = () => {
let hue = Math.floor(Math.random() * 360);
let pastel = `hsl(${hue}, 100%, 85%)`;
Expand All @@ -44,7 +43,8 @@ const drawBackground = () => {
ctx.fillRect(0, 0, width, height);
};

const addMetadata = (_dna, _edition) => {
// add metadata for individual nft edition
const generateMetadata = (_dna, _edition, _attributesList) => {
let dateTime = Date.now();
let tempMetadata = {
dna: _dna.join(""),
Expand All @@ -53,20 +53,23 @@ const addMetadata = (_dna, _edition) => {
image: `${baseImageUri}/${_edition}`,
edition: _edition,
date: dateTime,
attributes: attributesList,
attributes: _attributesList,
};
metadataList.push(tempMetadata);
attributesList = [];
return tempMetadata;
};

const addAttributes = (_element) => {
// prepare attributes for the given element to be used as metadata
const getAttributeForElement = (_element) => {
let selectedElement = _element.layer.selectedElement;
attributesList.push({
let attribute = {
name: selectedElement.name,
rarity: selectedElement.rarity,
});
};
return attribute;
};

// loads an image from the layer path
// returns the image in a format usable by canvas
const loadLayerImg = async (_layer) => {
return new Promise(async (resolve) => {
const image = await loadImage(`${_layer.selectedElement.path}`);
Expand All @@ -82,92 +85,165 @@ const drawElement = (_element) => {
_element.layer.size.width,
_element.layer.size.height
);
addAttributes(_element);
};

// check the configured layer to find information required for rendering the layer
// this maps the layer information to the generated dna and prepares it for
// drawing on a canvas
const constructLayerToDna = (_dna = [], _layers = [], _rarity) => {
let mappedDnaToLayers = _layers.map((layer, index) => {
let selectedElement = layer.elements[_rarity][_dna[index]];
let selectedElement = layer.elements.find(element => element.id === _dna[index]);
return {
location: layer.location,
position: layer.position,
size: layer.size,
selectedElement: selectedElement,
selectedElement: {...selectedElement, rarity: _rarity },
};
});

return mappedDnaToLayers;
};

const getRarity = (_editionCount) => {
let rarity = "";
rarityWeights.forEach((rarityWeight) => {
if (
_editionCount >= rarityWeight.from &&
_editionCount <= rarityWeight.to
) {
rarity = rarityWeight.value;
}
});
return rarity;
};

// check if the given dna is contained within the given dnaList
// return true if it is, indicating that this dna is already in use and should be recalculated
const isDnaUnique = (_DnaList = [], _dna = []) => {
let foundDna = _DnaList.find((i) => i.join("") === _dna.join(""));
return foundDna == undefined ? true : false;
};

const getRandomRarity = (_rarityOptions) => {
let randomPercent = Math.random() * 100;
let percentCount = 0;

for (let i = 0; i <= _rarityOptions.length; i++) {
percentCount += _rarityOptions[i].percent;
if (percentCount >= randomPercent) {
console.log(`use random rarity ${_rarityOptions[i].id}`)
return _rarityOptions[i].id;
}
}
return _rarityOptions[0].id;
}

// create a dna based on the available layers for the given rarity
// use a random part for each layer
const createDna = (_layers, _rarity) => {
let randNum = [];
let _rarityWeight = rarityWeights.find(rw => rw.value === _rarity);
_layers.forEach((layer) => {
let num = Math.floor(Math.random() * layer.elements[_rarity].length);
randNum.push(num);
let num = Math.floor(Math.random() * layer.elementIdsForRarity[_rarity].length);
if (_rarityWeight && _rarityWeight.layerPercent[layer.id]) {
// if there is a layerPercent defined, we want to identify which dna to actually use here (instead of only picking from the same rarity)
let _rarityForLayer = getRandomRarity(_rarityWeight.layerPercent[layer.id]);
num = Math.floor(Math.random() * layer.elementIdsForRarity[_rarityForLayer].length);
randNum.push(layer.elementIdsForRarity[_rarityForLayer][num]);
} else {
randNum.push(layer.elementIdsForRarity[_rarity][num]);
}
});
return randNum;
};

// holds which rarity should be used for which image in edition
let rarityForEdition;
// get the rarity for the image by edition number that should be generated
const getRarity = (_editionCount) => {
if (!rarityForEdition) {
// prepare array to iterate over
rarityForEdition = [];
rarityWeights.forEach((rarityWeight) => {
for (let i = rarityWeight.from; i <= rarityWeight.to; i++) {
rarityForEdition.push(rarityWeight.value);
}
});
}
return rarityForEdition[editionSize - _editionCount];
};

const writeMetaData = (_data) => {
fs.writeFileSync("./output/_metadata.json", _data);
};

// holds which dna has already been used during generation
let dnaListByRarity = {};
// holds metadata for all NFTs
let metadataList = [];
// Create generative art by using the canvas api
const startCreating = async () => {
console.log('##################');
console.log('# Generative Art');
console.log('# - Create your NFT collection');
console.log('##################');

console.log();
console.log('start creating NFTs.')

// clear meta data from previous run
writeMetaData("");

// prepare dnaList object
rarityWeights.forEach((rarityWeight) => {
dnaListByRarity[rarityWeight.value] = [];
});

// create NFTs from startEditionFrom to editionSize
let editionCount = startEditionFrom;
while (editionCount <= endEditionAt) {
console.log(editionCount);
while (editionCount <= editionSize) {
console.log('-----------------')
console.log('creating NFT %d of %d', editionCount, editionSize);

// get rarity from to config to create NFT as
let rarity = getRarity(editionCount);
console.log(rarity);
console.log('- rarity: ' + rarity);

// calculate the NFT dna by getting a random part for each layer/feature
// based on the ones available for the given rarity to use during generation
let newDna = createDna(layers, rarity);
console.log(dnaList);

if (isDnaUnique(dnaList, newDna)) {
let results = constructLayerToDna(newDna, layers, rarity);
let loadedElements = []; //promise array

results.forEach((layer) => {
loadedElements.push(loadLayerImg(layer));
});

await Promise.all(loadedElements).then((elementArray) => {
ctx.clearRect(0, 0, width, height);
drawBackground();
elementArray.forEach((element) => {
drawElement(element);
});
signImage(`#${editionCount}`);
saveImage(editionCount);
addMetadata(newDna, editionCount);
console.log(`Created edition: ${editionCount} with DNA: ${newDna}`);
});
dnaList.push(newDna);
editionCount++;
} else {
console.log("DNA exists!");
while (!isDnaUnique(dnaListByRarity[rarity], newDna)) {
// recalculate dna as this has been used before.
console.log('found duplicate DNA ' + newDna.join('-') + ', recalculate...');
newDna = createDna(layers, rarity);
}
console.log('- dna: ' + newDna.join('-'));

// propagate information about required layer contained within config into a mapping object
// = prepare for drawing
let results = constructLayerToDna(newDna, layers, rarity);
let loadedElements = [];

// load all images to be used by canvas
results.forEach((layer) => {
loadedElements.push(loadLayerImg(layer));
});

// elements are loaded asynchronously
// -> await for all to be available before drawing the image
await Promise.all(loadedElements).then((elementArray) => {
// create empty image
ctx.clearRect(0, 0, width, height);
// draw a random background color
drawBackground();
// store information about each layer to add it as meta information
let attributesList = [];
// draw each layer
elementArray.forEach((element) => {
drawElement(element);
attributesList.push(getAttributeForElement(element));
});
// add an image signature as the edition count to the top left of the image
signImage(`#${editionCount}`);
// write the image to the output directory
saveImage(editionCount);
let nftMetadata = generateMetadata(newDna, editionCount, attributesList);
metadataList.push(nftMetadata)
console.log('- metadata: ' + JSON.stringify(nftMetadata));
console.log('- edition ' + editionCount + ' created.');
console.log();
});
dnaListByRarity[rarity].push(newDna);
editionCount++;
}
writeMetaData(JSON.stringify(metadataList));
};

startCreating();
// Initiate code
startCreating();
Loading