Open Godfuzza opened 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.
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.
@Godfuzza you'd have to either change the metadata format or just estimate with globs
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.
@Godfuzza yes, but the JSON files do not stire the full file names for the layers, so they would have to be estimated.
@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?
@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).
@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?
@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/
.
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?
@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.
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? ;-)
@Godfuzza what is the the image's name? Is it undefined.png
?
how old are you?
16
yes undefined.png
@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
@Godfuzza between the lines:
}).then(buffer =>
saveBuffer(buffer, item.edition)
could you add console.log(item);
and run it again?
@bolshoytoster
This is how I added it:
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
@Godfuzza oh, add a {
to the end of line 178 and a }
to the start of line 181.
@bolshoytoster
the terminal listed all the NFTs that it generated, but it ended up saving just the one (last) NFT.
… but it looks like you're getting closer, champion! ;-)
@Godfuzza could you paste a full object from the output?
@bolshoytoster do you mean the command lines from the terminal or the PNG file?
@Godfuzza the output in the terminal.
@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',
@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"
@Godfuzza try changing all occurances of item.edition
with item.custom_fields.edition
@bolshoytoster
*****YOU ARE THE MASTER****
You made it work!
@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"
@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 #.png
s.
@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?
@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.
@bolshoytoster time is not the problem. there is no rush atm. but two runs are mandatory right?
Besides I really appreciate all the stuff you made for me so far.
@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.
@bolshoytoster okay got that. really appreciate you. are you an NFT collector?
@Godfuzza thank you
are you an NFT collector?
no
@bolshoytoster so how do you know all this stuff?
@Godfuzza it's just general programming.
@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!
@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?
@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.
@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. ;-)
@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.
@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 %)' }
probably he doesn't match the filenames with the trait names (we changed that in the metadata)
probably I need a script to change back the layers from "filename" to "name" before I run the rarity command line
@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.
@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?
@Godfuzza
So I would have to make another file
You could just add them to the same file.
Hey @bolshoytoster may I ask you one more little thing?
@Godfuzza yes
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