【前言】
最後這個 Deploy NFT 才是真正真正真正的大魔王,比我想像中還要難超級多,難到我現在都不知道前言要打什麼了。只能放上一些梗圖娛樂自己…
【Deploy NFT Flow】
這邊基本規劃一下要把自己的 NFT 發行,並且在網頁之中讓大家 mint 的流程是如何。
Day | Class | Description |
---|---|---|
Day 25 | Layers Blending & MetaData | 0. Day 25~ Day 29 Deploy NFT 規劃 |
1.產 MetaData + 合圖 | ||
Day 26 | Structuring Smart Contract | 0. 介紹 Lazy Mint |
1. 建置智能合約 | ||
Day 27 | Deploy on Testnet | 0. 上傳測試資料至 IPFS |
1. Testnet 和測試 Mint | ||
Day 28 | Deploy the Lazy Mint in Website | 0. 將 Mint 實作在網站中 |
1. 並且測試在網站中 Mint 的功能 | ||
Day 29 | Deploy on Mainnet | 0. 上鏈主網 |
1. Opensea Collection 調整 |
【轉換戰場 - config.js
】
好,這邊因為 JavaScript 比較適合生產,套用在網頁上也比較方便,所以我們把原本 Day 24 的功能從 Python 移到這裡。
首先我們要有一個 config.js
來儲存一些 common 的變數以及資料。
const fs = require("fs");
const width = 1000; // 圖片的長寬
const height = 1000;
const dir = __dirname;
const baseImageUri = "..."; // 圖片最後的地址,這我們明天再來講
const startEditionFrom = 0; // 第一個 NFT 的編號
const endEditionAt = 999; // 最後一個 NFT 的編號
const editionSize = 1000; // 總生產數量
const description = ""; // NFT 上要呈現的介紹或敘述
const raceWeights = [ // 種族,這邊因為只有一種種族所以就只有一個
{
value: "DNMS_Beta",
from: 0,
to: editionSize,
},
];
種族的全部部件資料,那因為我們只有一個叫做 DNMS_Beta
的種族,裡面會有好幾個圖層,每個塗層裡面又有數個不同款式的部件。像是我們有 13 種背景顏色,裡面再放入一些相關數據。
const races = {
DNMS_Beta: {
name: "DNMS_Beta",
layers: [
//-------------------------------------------//
{
name: "background",
elements: [
{
id: 0,
name: "Indigo",
path: `${dir}/part_image/13-background/background_1_none_none_Indigo_2.png`,
shape: "None",
color: "None",
possibility: "2", // 稀有度
},
...
{
id: 12,
name: "BabyBlue",
path: `${dir}/part_image/13-background/background_13_none_none_BabyBlue_3.png`,
shape: "None",
color: "None",
possibility: "3",
},
],
position: { x: 0, y: 0 },
size: { width: width, height: height },
number: 13, // 總共有 13 種 background
}, {
name: "effect",
elements: [
{
id: 0,
name: "BlackRadical",
path: `${dir}/part_image/12-effect/effect_1_none_none_BlackRadical_5.png`,
shape: "None",
color: "None",
possibility: "5",
},
...
],
...
},
...
//-------------------------------------------//
],
},
};
這邊把所有部件檔案輸出成 .json 的格式我是用 python 寫的,因為重點是合圖以及 MetaData 所以就不多加贅述了!
接下來依然是一些需要用到的變數,稀有度、創作者自留特定款式、分島嶼這些都是一些普通 NFT 沒有的功能,多做了這些東西不知道多花我多少時間!
const poss = {
1: { "Rarity": "Mythic", "Possibility": "3.1~4.9" },
2: { "Rarity": "Legendary", "Possibility": "6.4~10.6" },
3: { "Rarity": "Epic", "Possibility": "12.5~19.4" },
4: { "Rarity": "Rare", "Possibility": "20.6~30.4" },
5: { "Rarity": "Normal", "Possibility": "33.2~40.8" }
} // 稀有度的加權
let Creater_Data = {
LU: { "dna": ["08", "00", "03", "32", "08", "02", "32", "08", "09", "06", "05", "20", "08"], "id": "0102" },
...
} // 創始者我們會 MINT 自訂的款式,這邊的 dna 等下會說到。
const island = ["Ace", "Wanderer", "Muse", "Da_Kine"] // Dino 會住在四個島上。
module.exports = { // 將以上的所有變數都輸出,在 index.js 裡面會用到
width,
height,
baseImageUri,
editionSize,
description,
startEditionFrom,
endEditionAt,
races,
raceWeights,
poss,
Creater_Data,
island
};
【轉換戰場 - index.js
】
接下來就是主戰場了,首先先把剛剛輸出的變數都引入,還有宣告一些變數,還有我們主要拿來處理圖片的 canvas
功能。
const fs = require("fs");
const { createCanvas, loadImage } = require("canvas");
const {
width,
...
} = require("./input/DinoConfig.js");
const console = require("console");
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
var metadataList = []; // 全部的 Dino 的 MetaData
var attributesList = []; // 當前這隻 Dino 擁有的屬性
var dnaList = []; // 當前這隻 Dino 擁有的 dna
在這邊 dna
會是一個陣列,裡面儲存著十三個字串,分別代表著十三個圖層我們取用了哪一個款式的部件。除了可以利用 dna
來快速取得部件,也可以用來判斷有沒有重複的部 Dino。
///////////////////////////////
// For DNA
///////////////////////////////
const createDna = (_races, _race) => {
let randNum = [];
let sh = "None";
let co = "None";
_races[_race].layers.forEach((layer) => {
let consider = Randomize(layer);
let index = Math.floor(Math.random() * parseInt(consider.length));
let randElementNum = consider[index];
// Start to decide shape here!!!
...
// Start to decide color here!!!
...
// 這兩個部分其實就是跑一個 while 迴圈直到 RANDOM 出來的款式符合同一組開合和皮膚顏色
randNum.push(randElementNum);
});
return randNum;
};
const isDnaUnique = (_DnaList = [], _dna = []) => {
let foundDna = _DnaList.find((i) => i.join("") === _dna.join(""));
return foundDna == undefined ? true : false;
};
這裡主要的重點其實是要怎麼把權重考量進去。我們設定了每個部件的加權,如果是比較稀有的款式那他的加權就會比較少,那這樣他出現的機率就會降低。
///////////////////////////////
// For Shuffle and Random
///////////////////////////////
function shuffle(array) {
// refference: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
const Randomize = (layer) => {
let total = layer.number;
let considerList = [];
for(let i = 0; i < total; i++){
let now_pos = poss[parseInt(layer.elements[i].possibility)].Possibility;
now_pos = now_pos.split("~");
now_pos = Math.floor(Math.random() * (now_pos[1] - now_pos[0] + 1)) + now_pos[1];
for (let j = 0; j < now_pos; j++){
considerList.push(layer.elements[i].id);
}
}
shuffle(considerList);
return considerList;
}
再來就是生產 MetaData 以及寫檔。
///////////////////////////////
// For MetaData
///////////////////////////////
const addMetadata = (_dna, _edition) => {
let dateTime = Date.now();
let tempMetadata = {
dna: _dna.join(""),
name: `#${_edition}`,
image: `${baseImageUri}/${_edition}.png`,
description: description,
id: _edition,
island: Born_Location(_dna),
birthday: dateTime,
attributes: attributesList,
};
metadataList.push(tempMetadata);
attributesList = [];
};
const addAttributes = (_element) => {
let selectedElement = _element.layer.selectedElement;
attributesList.push({
trait_type: _element.layer.name,
type_name: selectedElement.name,
rarity: poss[selectedElement.possibility].Rarity
});
};
const writeMetaData = (_data) => {
fs.writeFileSync("./output/_metadata.json", _data);
};
const saveMetaDataSingleFile = (_editionCount) => {
fs.writeFileSync(
`./output/${_editionCount}.json`,
JSON.stringify(metadataList.find((meta) => meta.id == _editionCount))
);
};
function Born_Location(arr) {
var born = 0;
for (var i = 0; i < arr.length; i++) {
born += parseInt(arr[i]);
};
return island[born % 4];
}
接下來是讀取部件圖片,合圖,掛上標籤,以及存檔。
///////////////////////////////
// For Drawing
///////////////////////////////
// 存檔
const saveImage = (_editionCount) => {
fs.writeFileSync(
`./output/${_editionCount}.png`,
canvas.toBuffer("image/png")
);
};
// 在 NFT 的左上角標上他的 id
const signImage = (_sig) => {
ctx.fillStyle = "#ffffff";
ctx.font = "bold 30pt Verdana";
ctx.textBaseline = "top";
ctx.textAlign = "left";
ctx.fillText(_sig, 40, 40);
};
// 讀取部件圖檔
const loadLayerImg = async (_layer) => {
return new Promise(async (resolve) => {
const image = await loadImage(`${_layer.selectedElement.path}`);
resolve({ layer: _layer, loadedImage: image });
});
};
// 將當前圖層鋪上去
const drawElement = (_element) => {
ctx.drawImage(
_element.loadedImage,
_element.layer.position.x,
_element.layer.position.y,
_element.layer.size.width,
_element.layer.size.height
);
addAttributes(_element);
};
// 取出所選部件的資料
const constructLayerToDna = (_dna = [], _races = [], _race) => {
let mappedDnaToLayers = _races[_race].layers.map((layer, index) => {
let selectedElement = layer.elements.find((e) => e.id == parseInt(_dna[index]));
return {
name: layer.name,
position: layer.position,
size: layer.size,
number: layer.number,
selectedElement: selectedElement,
};
});
return mappedDnaToLayers;
};
等上面的函數都準備好了之後,就可以開始合圖的!
///////////////////////////////
// startCreating
///////////////////////////////
// 因為我們只有一個種族,所以這個函數用不到
const getRace = (_editionCount) => {
let race = "No Race";
raceWeights.forEach((raceWeight) => {
// if (_editionCount >= raceWeight.from && _editionCount <= raceWeight.to) {
race = raceWeight.value;
// }
});
return race;
};
const startCreating = async () => {
writeMetaData("");
let editionCount = startEditionFrom;
// 跑一個迴圈
while (editionCount <= endEditionAt) {
let race = getRace(editionCount);
// 決定 dna,如果是指定的 creator_id 要特別設定 dna
let newDna = [];
if(editionCount === parseInt(Creater_Data.LU.id)){
newDna = Creater_Data.LU.dna;
}
...
else{
newDna = createDna(races, race);
}
// 確認沒有重複的 dna
if (isDnaUnique(dnaList, newDna)) {
let results = constructLayerToDna(newDna, races, race);
let loadedElements = []; //promise array
results.forEach((layer) => {
loadedElements.push(loadLayerImg(layer));
ReviseStatic(layer);
});
await Promise.all(loadedElements).then((elementArray) => {
ctx.clearRect(0, 0, width, height);
elementArray.forEach((element) => {
drawElement(element);
});
signImage(`#${editionCount}`);
saveImage(editionCount);
addMetadata(newDna, editionCount);
saveMetaDataSingleFile(editionCount);
// island = Born_Location(newDna);
let is = Born_Location(newDna);
console.log(
`Created DINO-ID: ${editionCount}, Race: ${race} with DNA: ${newDna} at Island: ${is}`
);
});
dnaList.push(newDna);
editionCount++;
} else {
console.log("DNA exists!");
}
}
writeMetaData(JSON.stringify(metadataList));
PrintStatic();
};
startCreating();
【小結】
這邊改良自 HashLips 大大的 Project,真的非常感謝網路上有這麼又強又樂意分享的大神,有了很棒的 Base 之後我就可以加上很多自己想要加的東西!不過還是遇到超多 Bug,尤其是我又想了一堆奇奇怪怪的功能,真是拿石頭砸自己腳。
明天開始會進到智能合約,真的是要替自己好好祈禱還有鼓勵,好難啊!
【參考資料】
Code generative art for NFT in node.js part 1