HashLips / hashlips_art_engine

HashLips Art Engine is a tool used to create multiple different instances of artworks based on provided layers.
MIT License
7.17k stars 4.3k forks source link

Generating NFTs via Visual Studio Code using existing JSON files #1447

Open Godfuzza opened 1 year ago

Godfuzza commented 1 year ago

Hey everyone, is it possible to generate NFTs via Visual Studio Code using existing JSON files?

Normally I generate NFTs using "npm run generate". This command line generates NFTs according to the config file. Is it possible to reverse the process? I want to read out existing JSON files and generate NFTs according to the information in the JSON files.

The reason is that I just updated some of the layer information and want to rerun the process ending up with the same NFTs. Thanx. Godfuzza

bolshoytoster commented 1 year ago

@Godfuzza no but you could try to implement it yourself. The main issue is that filenames aren't stored in the JSON files.

Godfuzza commented 1 year ago

Hey @bolshoytoster thanx for your feedback. Logically it must be possible to revert the process. If I run a command that selects the layers by random, it should be possible to generate a command that reads out the JSON files and use the layer information to build the NFTs with the according layers.

bolshoytoster commented 1 year ago

@Godfuzza you'd have to either change the metadata format or just estimate with globs

Godfuzza commented 1 year ago

Hey @bolshoytoster, I don't know if I was precise enough in my question. I am focussing on creating NFT images from the PNG image data (layers) via Visual Studio Code. This process runs randomly and differently each time. I would like to reverse the process and not create new random NFTs, but have a Command Line read the existing JSON files from the first run associated with the NFTs and regenerate the NFTs. Why do I want to do this? Because I have adjusted the layers graphically after the 1st run and now I don't want to have new randomly generated NFTs in the 2nd run, but exactly the same ones as they are stored in the JSON files via the features.

bolshoytoster commented 1 year ago

@Godfuzza yes, but the JSON files do not stire the full file names for the layers, so they would have to be estimated.

Godfuzza commented 1 year ago

@bolshoytoster now I got it. I checked it. You are right! The JSON just stores the filename w/o the hashtag rarity score and the syntax ending. Any workaround?

bolshoytoster commented 1 year ago

@Godfuzza in src/main.js ~line 178: https://github.com/HashLips/hashlips_art_engine/blob/d8ee279043d2d4a8de3bdfac0d89d0e966fb04a2/src/main.js#L177-L179

You could change the selectedElement.name to selectedElement.filename, then all of the collections you generate after that will have the full filename there instead.

Then you'd need another script to generate it, I could do this after school (a few hours).

Godfuzza commented 1 year ago

@bolshoytoster That's more coding than I expected. ;-) I am more the designer but I could try. If you would/could support me I could probably support you with something I am able to do – designwork – as an exchange so to say?

bolshoytoster commented 1 year ago

@Godfuzza

If you've changed the value as in my previous comment, you should be able to create a new file (something like reverse.js) (if you make the file in src/ you'll have to add that when you run it) and add the following code:

const basePath = process.cwd();
const fs = require("fs");
const { freemem } = require('os');
const {
  Canvas,
  CanvasRenderingContext2d,
  CanvasRenderingContext2dInit,
  Image,
  SetSource
} = require(`${basePath}/node_modules/canvas/build/Release/canvas.node`);

const buildDir = `${basePath}/build`;
const layersDir = `${basePath}/layers`;

let config;

try {
  config = require(`${basePath}/src/config.js`)
} catch (error) {
  console.error(`Syntax error: ${error.message} in src/config.js`);
  process.exit();
}

let {
  format,
  background,
  gif,
  rarityDelimiter,
  text
} = config;

let HashlipsGiffer;

if (gif.export) {
  HashlipsGiffer = require(`${basePath}/modules/HashlipsGiffer.js`);
}

let loadedImages = {};

const saveBuffer = (buffer, _editionCount) => {
  fs.writeFileSync(
    `${buildDir}/images/${_editionCount}.png`,
    buffer,
    () => {}
  );
  console.log(`Saved edition: ${_editionCount}`);
};

const genColor = () => {
  let hue = Math.floor(Math.random() * 360);
  let pastel = `hsl(${hue}, 100%, ${background.brightness})`;
  return pastel;
};

const drawBackground = (ctx) => {
  ctx.fillStyle = background.static ? background.default : genColor();
  ctx.fillRect(0, 0, format.width, format.height);
};

const cleanName = (_str) => {
  let nameWithoutExtension = _str.slice(0, -4);
  var nameWithoutWeight = nameWithoutExtension.split(rarityDelimiter).shift();
  return nameWithoutWeight;
};

const drawElements = (canvas, ctx, elements, _editionCount) => {
  let hashlipsGiffer;
  if (gif.export) {
    hashlipsGiffer = new HashlipsGiffer(
      canvas,
      ctx,
      `${buildDir}/gifs/${_editionCount}.gif`,
      gif.repeat,
      gif.quality,
      gif.delay
    );
    hashlipsGiffer.start();
  }

  if (background.generate) {
    drawBackground(ctx);
  }

  elements.forEach((element, _index) => {
//    layer = element.layer;

    // we unfortunately can't accurately determine these
//    ctx.globalAlpha = layer.opacity;
//    ctx.globalCompositeOperation = layer.blend;
    if (!text.only) {
      if (loadedImages[`${element.trait_type}/${element.value}`]) {
        element.loadedImage = loadedImages[`${element.trait_type}/${element.value}`];
      } else if (`${element.trait_type}/${element.value}` in loadedImages) {
        console.error(`Skipping ${element.trait_type}/${element.value} due to previous error`);
        return;
      } else {
        const loadedImage = new Image();

        loadedImage.onload = () => {
          element.loadedImage = loadedImage;
          loadedImages[`${element.trait_type}/${element.value}`] = loadedImage;
        }

        loadedImage.onerror = (e) => {
          console.error(`${e}: ${element.trait_type}/${element.value}`);
          loadedImages[`${element.trait_type}/${element.value}`] = undefined;
        }

        SetSource.call(loadedImage, `${layersDir}/${element.trait_type}/${element.value}`);
      }
    }

    text.only
      ? addText(
          ctx,
          cleanName(element.value),
          text.xGap,
          text.yGap * (_index + 1),
          text.size
        )
      : ctx.drawImage(
          element.loadedImage,
//          layer.posX,
//          layer.posY,
          0,
          0,
          format.width,
          format.height
        );

    if (gif.export) {
      hashlipsGiffer.add();
    }
  });

  if (gif.export) {
    hashlipsGiffer.stop();
  }
};

// read json data from `build/json/_metadata.json`
let rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`);
let data = JSON.parse(rawdata);

// use quicker rendering if memory is available
lowMemory =
    freemem() < format.width * format.height * (background.generate ? 3 : 4) * data.length;

let globalCanvas, globalCtx;
if (lowMemory) {
  globalCanvas = new Canvas(format.width, format.height);
  globalCtx = new CanvasRenderingContext2d(globalCanvas, { alpha: !background.generate });
  globalCtx.imageSmoothingEnabled = format.smoothing;
}

// loop through all images
data.forEach((item) => {
  if (lowMemory) {
    if (!background.generate) {
      globalCtx.clearRect(0, 0, format.width, format.height);
    }
    drawElements(globalCanvas, globalCtx, item.attributes, item.edition);

    saveBuffer(globalCanvas.toBuffer("image/png", { resolution: format.resolution }), item.edition);
  } else {
    new Promise(resolve => {
      const canvas = new Canvas(format.width, format.height);
      const ctx = new CanvasRenderingContext2d(canvas, { alpha: !background.generate });
      ctx.imageSmoothingEnabled = format.smoothing;

      drawElements(canvas, ctx, item.attributes, item.edition);

      canvas.toBuffer((_, buffer) =>
        resolve(buffer)
      , "image/png", { resolution: format.resolution })
    }).then(buffer =>
      saveBuffer(buffer, item.edition)
    );
  }
});

Unfortunately it's not able to maintain layer opacity, blend or positioning (the ones defined in src/config.js) unless you added that to metadata as well.

To run, run either node reverse.js or node src/reverse.js, depending on whether you added the file in src/.

Godfuzza commented 1 year ago

Hey @bolshoytoster great stuff! Thanx, buddy. I will try this out later when I am back at home and see if it works. I guess I will have to copy the JSON folder with the json files into the build folder in Visual Studio Code and leave the IMAGES folder empty, right?

bolshoytoster commented 1 year ago

@Godfuzza

I guess I will have to copy the JSON folder with the json files

While you only need build/json/_metadata.json, you should probably copy the whole folder.

and leave the IMAGES folder empty, right?

Yep, but make sure there is a build/images, since this script doesn't make it itself.

Godfuzza commented 1 year ago

hey @bolshoytoster okay, I made a testrun. here is what happened:

I changed the value to "filenames" and created 10 Test-NFTs. That worked well. The changes are now in the metadata. Then I created the "reverse.js" with your code and ran it.

It only created 1 NFT in the images folder (the very last one of the collection). I think the routine simply overwrote the same file 10x and did not create 10 individual files.

Besides … you did that after school? … how old are you? ;-)

bolshoytoster commented 1 year ago

@Godfuzza what is the the image's name? Is it undefined.png?

how old are you?

16

Godfuzza commented 1 year ago

yes undefined.png

Godfuzza commented 1 year ago

@bolshoytoster this was the terminal log:

Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined Saved edition: undefined

bolshoytoster commented 1 year ago

@Godfuzza between the lines:

    }).then(buffer =>
      saveBuffer(buffer, item.edition)

could you add console.log(item); and run it again?

Godfuzza commented 1 year ago

@bolshoytoster This is how I added it: Bildschirmfoto 2022-11-15 um 19 55 09

I got this syntax error: SyntaxError: missing ) after argument list at Object.compileFunction (node:vm:360:18) at wrapSafe (node:internal/modules/cjs/loader:1055:15) at Module._compile (node:internal/modules/cjs/loader:1090:27) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10) at Module.load (node:internal/modules/cjs/loader:1004:32) at Function.Module._load (node:internal/modules/cjs/loader:839:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:17:47

bolshoytoster commented 1 year ago

@Godfuzza oh, add a { to the end of line 178 and a } to the start of line 181.

Godfuzza commented 1 year ago

@bolshoytoster the terminal listed all the NFTs that it generated, but it ended up saving just the one (last) NFT. Bildschirmfoto 2022-11-15 um 20 01 11

Godfuzza commented 1 year ago

… but it looks like you're getting closer, champion! ;-)

bolshoytoster commented 1 year ago

@Godfuzza could you paste a full object from the output?

Godfuzza commented 1 year ago

@bolshoytoster do you mean the command lines from the terminal or the PNG file?

bolshoytoster commented 1 year ago

@Godfuzza the output in the terminal.

Godfuzza commented 1 year ago

@bolshoytoster { name: 'tbd #1', description: tbds', file_url: 'ipfs://NewUriToReplace/1.png', custom_fields: { dna: '1df67b0f7113ae6bc2d21e2da1d16d1b8f8c14ee', edition: 1, date: 1668536516067, compiler: 'HashLips Art Engine' }, attributes: [ { trait_type: 'legendary', value: 'common#100.png', loadedImage: [Image] }, { trait_type: 'background', value: 'red.jpg', loadedImage: [Image] }, { trait_type: 'wings', value: 'no wings#200.png', loadedImage: [Image] }, { trait_type: 'eyes', value: 'golden balls.png', loadedImage: [Image] }, { trait_type: 'skin', value: 'blue lizard dizzy.png', loadedImage: [Image] }, { trait_type: 'tattoo', value: 'no tattoo#100.png', loadedImage: [Image] }, { trait_type: 'clothing', value: 'blue cannabis hoodie.png', loadedImage: [Image] }, { trait_type: 'beard', value: 'moustache#10.png', loadedImage: [Image] }, { trait_type: 'headdress', value: 'golden handband#3.png', loadedImage: [Image] }, { trait_type: 'eyepatch', value: 'no eyepatch#100.png', loadedImage: [Image] }, { trait_type: 'earrings', value: 'no earrings#100.png', loadedImage: [Image] }, { trait_type: 'noserings', value: 'pink#15.png', loadedImage: [Image] }, { trait_type: 'chinrings', value: 'no chinrings#100.png', loadedImage: [Image] }, { trait_type: 'bandana', value: 'no bandana#250.png', loadedImage: [Image] }, { trait_type: 'horns', value: 'red morel fungus horns#100.png', loadedImage: [Image] }, { trait_type: 'glasses', value: 'no glasses#100.png', loadedImage: [Image] }, { trait_type: 'items', value: 'no item#100.png', loadedImage: [Image] }, { trait_type: 'consuming', value: 'golden three stars joint white yellow smoke.png', loadedImage: [Image] } ] } Saved edition: undefined { name: 'Stoned Horns Camp #2',

Godfuzza commented 1 year ago

@bolshoytoster

i probalbly see one problem here with the change to the filename for the display of the traits later in the metadata on opensea. The trait will then be named "black glasses#10.png" instead of "black glasses"

bolshoytoster commented 1 year ago

@Godfuzza try changing all occurances of item.edition with item.custom_fields.edition

Godfuzza commented 1 year ago

@bolshoytoster

*****YOU ARE THE MASTER****

You made it work!

Godfuzza commented 1 year ago

@bolshoytoster

Now I have to figure out, how I can change the values before I upload the metadata on opensea. Cause otherwise a trait will then be named "black glasses#10.png" instead of "black glasses"

bolshoytoster commented 1 year ago

@Godfuzza You could remove the line:

          element.loadedImage = loadedImage;

, change the line

        element.loadedImage = loadedImages[`${element.trait_type}/${element.value}`];

to

        loadedImage = loadedImages[`${element.trait_type}/${element.value}`];

, change the line

          element.loadedImage,

to just

          loadedImage,

That will remove unneccessary data.


Then add the lines

  item.attributes.forEach((element) => {
    element.value = cleanName(element.value);
  });

  fs.writeFileSync(
    `${basePath}/build/json/${item.custom_fields.edition}.json`,
    JSON.stringify(item, null, 2)
  );

between the lines at the end:

  }
});

And finally add

fs.writeFileSync(
  `${basePath}/build/json/_metadata.json`,
  JSON.stringify(data, null, 2)
);

at the end.

That should update the metadata files and remove the #.pngs.

Godfuzza commented 1 year ago

@bolshoytoster can I add these new lines before I run the "reverse.js" or do I have to add these after I made the reversed NFT before uploading the metadata on opensea?

bolshoytoster commented 1 year ago

@Godfuzza it won't matter what order you do it, but it'll end up regenerating the same images again, which might take a while for a big collection.

Godfuzza commented 1 year ago

@bolshoytoster time is not the problem. there is no rush atm. but two runs are mandatory right?

  1. run > to reproduce the NFTs with the reverse.js file
  2. run > to change the values back from "filename" to "name" in the JSON files in order not to end up with funny trait names like "black glasses#100" Did I get that right? Or can I make the changes you sent in your last chat even before I run the reverse process? Just want to get that right. ;-)

Besides I really appreciate all the stuff you made for me so far.

bolshoytoster commented 1 year ago

@Godfuzza you only need to run it once, but multiple times won't break anything.

Or can I make the changes you sent in your last chat even before I run the reverse process?

Yes.

Godfuzza commented 1 year ago

@bolshoytoster okay got that. really appreciate you. are you an NFT collector?

bolshoytoster commented 1 year ago

@Godfuzza thank you

are you an NFT collector?

no

Godfuzza commented 1 year ago

@bolshoytoster so how do you know all this stuff?

bolshoytoster commented 1 year ago

@Godfuzza it's just general programming.

Godfuzza commented 1 year ago

@bolshoytoster you have solved one of my two personal challenges that I faced. You are very talented, bro! Especially for a 16-ys old programmer! Respect!

Godfuzza commented 1 year ago

@bolshoytoster one more question concerning the metadata/json files:

When I run the config.js file I get metadata with the ending #100.png etc. The reverse file does not change the _metadata.json right? What did you mean that the new command lines will remove the endings then?

Godfuzza commented 1 year ago

@bolshoytoster Hey I fixed it myself. I made two files: one for the reverse run and another one for the metadata cleaning. The only file that did not get "cleaned" is the _metadata.json file. Have to figure out, if that needs to be uploaded on opensea or just the json files of the NFTs.

Godfuzza commented 1 year ago

@bolshoytoster

Hey bro, please let me know if and how I can repay you for your great work. I'm a graphic designer and I'm happy to assist if you need help or support with anything. I can also offer you a freemint spot in the collection if you'd be interested - you wrote that you're not actually an NFT collector.

May I call on your skills again on one last thing if necessary? Are you familiar with the rarity score on NFTs? I would like to know how to determine the Rarity Score of the generated NFTs. I know that with layer files, I can add a hashtag with a number that determines how often percentage wise that trait should be used. I am concerned with displaying the total score of an NFT within the collection.

I've read about a tool that should make this possible. Are you familiar with this? https://www.openrarity.dev/

Thanx, bro.

Besides … wyf … I am from Germany. ;-)

bolshoytoster commented 1 year ago

@Godfuzza

Hey bro, please let me know if and how I can repay you

It's ok.

I would like to know how to determine the Rarity Score of the generated NFTs

I know you could run either node utils/rarity.js or npm run rarity in hashlips which will output the rarity of each trait, but I think openrarity is probably the best choice since I'm not sure how metadata is used when they're uploaded, despite the fact that I can't see any useful documentation.

wyf

UK.

Godfuzza commented 1 year ago

@bolshoytoster It's strange … if I run the commands all the rarities are shown with "0" occurrence …

trait: 'can weed purple blue tongue', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'can weed purple pink tongue', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'can weed purple', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'can zebra blue tongue', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'can zebra pink tongue', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'can zebra', weight: '1', occurrence: '0 in 300 editions (0.00 %)' } { trait: 'golden joint green smoke trail', weight: '1', occurrence: '0 in 300 editions (0.00 %)' }

Godfuzza commented 1 year ago

probably he doesn't match the filenames with the trait names (we changed that in the metadata)

Godfuzza commented 1 year ago

probably I need a script to change back the layers from "filename" to "name" before I run the rarity command line

bolshoytoster commented 1 year ago

@Godfuzza if you added the lines:

  item.attributes.forEach((element)=> {
    let nameWithoutExtension = element.value.slice(0, -4);
    element.value = nameWithoutExtension.split('#').shift();
  });

between lines ~17-18 of utils/update_info.js: https://github.com/HashLips/hashlips_art_engine/blob/d8ee279043d2d4a8de3bdfac0d89d0e966fb04a2/utils/update_info.js#L17-L18

You should be able to just run node utils/update_info.js to clean the names.

Godfuzza commented 1 year ago

@bolshoytoster okay got this. but I need the js.file with the filenames to run the reverse process right? So I would have to make another file with your last changes if I need to switch back to a file with "names" instead of "filenames" in order to recognize that trait occurrence, right?

bolshoytoster commented 1 year ago

@Godfuzza

So I would have to make another file

You could just add them to the same file.

Godfuzza commented 1 year ago

Hey @bolshoytoster may I ask you one more little thing?

bolshoytoster commented 1 year ago

@Godfuzza yes