Skip to content

Commit

Permalink
feat: NFT collections can now be generated as sets
Browse files Browse the repository at this point in the history
  • Loading branch information
rvcas committed May 20, 2022
1 parent e8b57cb commit df6c2b8
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 128 deletions.
42 changes: 20 additions & 22 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use config::{Config, File};
use dialoguer::{console::Term, theme::ColorfulTheme, Confirm, Input, Select};
use directories_next::ProjectDirs;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{Map, Value};

use crate::cli::Mode;

Expand All @@ -16,24 +16,27 @@ pub struct AppConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub twitter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub copyright: Option<String>,
pub mode: Mode,
#[serde(default)]
pub start_at_one: bool,
pub amount: usize,
pub tolerance: usize,
pub path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub sets: Option<Vec<SetConfig>>,
pub layers: Vec<LayerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nft_maker: Option<NftMakerLocalConfig>,
pub extra: Option<Map<String, Value>>,
}

#[derive(Deserialize, Serialize, Debug, Default)]
pub struct SetConfig {
pub name: String,
pub amount: usize,
}

#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct LayerConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -171,37 +174,33 @@ impl AppConfig {
None
};

let mut extra = Map::new();

let twitter: String = Input::new()
.with_prompt("enter twitter url")
.allow_empty(true)
.interact_text()?;

let twitter = if !twitter.is_empty() {
Some(twitter)
} else {
None
if !twitter.is_empty() {
extra.insert("twitter".to_string(), Value::String(twitter));
};

let website: String = Input::new()
.with_prompt("enter website url")
.allow_empty(true)
.interact_text()?;

let website = if !website.is_empty() {
Some(website)
} else {
None
if !website.is_empty() {
extra.insert("website".to_string(), Value::String(website));
};

let copyright: String = Input::new()
.with_prompt("enter copyright")
.allow_empty(true)
.interact_text()?;

let copyright = if !copyright.is_empty() {
Some(copyright)
} else {
None
if !copyright.is_empty() {
extra.insert("copyright".to_string(), Value::String(copyright));
};

let items = vec![Mode::Simple, Mode::Advanced];
Expand Down Expand Up @@ -240,16 +239,15 @@ impl AppConfig {
policy_id,
name,
display_name,
twitter,
website,
copyright,
mode,
start_at_one: false,
amount,
tolerance: 50,
path: "images".into(),
sets: None,
layers,
nft_maker: None,
extra: Some(extra),
})
}
}
Expand Down
18 changes: 12 additions & 6 deletions src/layers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::path::PathBuf;

use anyhow::{anyhow, Context};
use image::{DynamicImage, GenericImageView};
use rand::Rng;

use crate::{cli::Mode, config::AppConfig};
use crate::{cli::Mode, config::LayerConfig};

const RARITIES: [&str; 5] = ["common", "uncommon", "rare", "mythical", "legendary"];

Expand All @@ -22,19 +24,23 @@ pub struct Layers {
}

impl Layers {
pub fn load(&mut self, config: &AppConfig) -> anyhow::Result<()> {
pub fn load(
&mut self,
mode: Mode,
layers: Vec<LayerConfig>,
path: PathBuf,
) -> anyhow::Result<()> {
let mut data = Vec::new();

let layer_paths = config
.layers
let layer_paths = layers
.iter()
.map(|layer| (layer, config.path.join(layer.name.clone())))
.map(|layer| (layer, path.join(layer.name.clone())))
.filter(|(_, path)| path.is_dir());

for (layer, layer_path) in layer_paths {
let mut trait_list = Vec::new();

match config.mode {
match mode {
Mode::Advanced => {
let trait_paths = layer_path
.read_dir()
Expand Down
211 changes: 148 additions & 63 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,102 +49,187 @@ fn main() -> anyhow::Result<()> {
let config = AppConfig::new(&args.config)?;
let progress = ProgressBar::new(config.amount as u64);

let mut layers = Layers::default();
let (layer_sets, unique_sets) = if let Some(sets) = &config.sets {
let mut layer_sets = Vec::new();

layers.load(&config)?;
let mut unique_sets = Vec::new();

let mut fail_count = 0;
let mut offset = config.amount;

let mut uniques = HashSet::new();
let sets_total = sets.iter().fold(0, |acc, set| acc + set.amount);

let mut count = 1;
if sets_total != config.amount {
return Err(anyhow!("amount in sets must equal the total amount"));
}

while count <= config.amount {
let unique = layers.create_unique();
for (set_index, set) in sets.iter().enumerate() {
let mut layers = Layers::default();

if uniques.contains(&unique) {
fail_count += 1;
layers.load(
config.mode,
config.layers.clone(),
config.path.join(set.name.clone()),
)?;

if fail_count > config.tolerance {
println!(
"You need more features or traits to generate {}",
config.amount
);
let mut fail_count = 0;

process::exit(1);
}
let mut uniques = HashSet::new();

continue;
}
let mut count = 1;

uniques.insert(unique);
while count <= set.amount {
let unique = layers.create_unique();

count += 1;
}
if uniques.contains(&unique) {
fail_count += 1;

utils::clean(output)?;
if fail_count > config.tolerance {
println!(
"You need more features or traits to generate {}",
set.amount
);

fs::create_dir(output)?;
process::exit(1);
}

// Calculate rarity
let mut rarity = Rarity::new(config.amount);
continue;
}

for unique in &uniques {
for (index, trait_list) in unique.iter().zip(&layers.data) {
let nft_trait = &trait_list[*index];
uniques.insert(unique);

rarity.count_trait(&nft_trait.layer, &nft_trait.name);
count += 1;
}

layer_sets.push(layers);

offset -= set.amount;

unique_sets.push((uniques, set_index, offset));
}
}

// Generate the images
uniques
.into_iter()
.enumerate()
.collect::<Vec<(usize, Vec<usize>)>>()
.par_iter()
.for_each(|(mut count, unique)| {
if config.start_at_one {
count += 1
}
(layer_sets, unique_sets)
} else {
let mut layers = Layers::default();

let mut base = RgbaImage::new(layers.width, layers.height);
layers.load(config.mode, config.layers, config.path)?;

let mut trait_info = Map::new();
let mut fail_count = 0;

let folder_name = output.join(format!("{}#{}", config.name, count));
fs::create_dir(&folder_name).expect("failed to created a folder for an NFT");
let mut uniques = HashSet::new();

for (index, trait_list) in unique.iter().zip(&layers.data) {
let nft_trait = &trait_list[*index];
let mut count = 1;

trait_info.insert(
nft_trait.layer.to_owned(),
Value::String(nft_trait.name.to_owned()),
);
while count <= config.amount {
let unique = layers.create_unique();

if let Some(image) = &nft_trait.image {
utils::merge(&mut base, image);
if uniques.contains(&unique) {
fail_count += 1;

if fail_count > config.tolerance {
println!(
"You need more features or traits to generate {}",
config.amount
);

process::exit(1);
}

continue;
}

let nft_image_path = folder_name.join(format!("{}#{}.png", config.name, count));
let attributes_path =
folder_name.join(format!("{}#{}.json", config.name, count));
let metadata_path = folder_name.join("metadata.json");
uniques.insert(unique);

base.save(nft_image_path).expect("failed to create image");
count += 1;
}

let attributes = serde_json::to_string_pretty(&trait_info)
.expect("failed to create attributes");
(vec![layers], vec![(uniques, 0, 0)])
};

utils::clean(output)?;

fs::create_dir(output)?;

fs::write(attributes_path, attributes).expect("failed to create attributes");
// Calculate rarity
let mut rarity = Rarity::new(config.amount);

let meta = metadata::build_with_attributes(&config, trait_info, count);
for (uniques, set_index, _) in &unique_sets {
for unique in uniques {
for (index, trait_list) in unique.iter().zip(&layer_sets[*set_index].data) {
let nft_trait = &trait_list[*index];

fs::write(metadata_path, meta).expect("failed to create metadata");
rarity.count_trait(&nft_trait.layer, &nft_trait.name);
}
}
}

progress.inc(1);
// Generate the images
unique_sets
.par_iter()
.for_each(|(uniques, set_index, offset)| {
let layers = &layer_sets[*set_index];

uniques
.iter()
.enumerate()
.collect::<Vec<(usize, &Vec<usize>)>>()
.par_iter()
.for_each(|(mut count, unique)| {
if config.start_at_one {
count += 1
}

let mut base = RgbaImage::new(layers.width, layers.height);

let mut trait_info = Map::new();

let folder_name =
output.join(format!("{}#{}", config.name, count + offset));
fs::create_dir(&folder_name)
.expect("failed to created a folder for an NFT");

for (index, trait_list) in unique.iter().zip(&layers.data) {
let nft_trait = &trait_list[*index];

trait_info.insert(
nft_trait.layer.to_owned(),
Value::String(nft_trait.name.to_owned()),
);

if let Some(image) = &nft_trait.image {
utils::merge(&mut base, image);
}
}

let nft_image_path =
folder_name.join(format!("{}#{}.png", config.name, count + offset));
let attributes_path = folder_name.join(format!(
"{}#{}.json",
config.name,
count + offset
));
let metadata_path = folder_name.join("metadata.json");

base.save(nft_image_path).expect("failed to create image");

let attributes = serde_json::to_string_pretty(&trait_info)
.expect("failed to create attributes");

fs::write(attributes_path, attributes)
.expect("failed to create attributes");

let meta = metadata::build_with_attributes(
trait_info,
config.policy_id.clone(),
config.name.clone(),
config.display_name.as_ref(),
config.extra.clone(),
count + offset,
);

fs::write(metadata_path, meta).expect("failed to create metadata");

progress.inc(1);
});
});

let rarity_path = output.join("rarity.json");
Expand Down
Loading

0 comments on commit df6c2b8

Please sign in to comment.