iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Modern Web

All In One NFT Website Development系列 第 25

Day 25【Deploy NFT - Layers Blending & MetaData】Read the License

207176907_4084835581565289_6541950307456049932_n.jpg

【前言】
最後這個 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 所以就不多加贅述了!

210547979_4098569530191894_2156828383657658631_n.jpg

接下來依然是一些需要用到的變數,稀有度、創作者自留特定款式、分島嶼這些都是一些普通 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,尤其是我又想了一堆奇奇怪怪的功能,真是拿石頭砸自己腳。

明天開始會進到智能合約,真的是要替自己好好祈禱還有鼓勵,好難啊!

210359798_4085029834879197_1287203667197399854_n.jpg

【參考資料】
Code generative art for NFT in node.js part 1


上一篇
Day 24【Random Picture Blending in Python】return bool;
下一篇
Day 26【Deploy NFT - Lazy-Minting & Smart Contract】Right Click and Save Image As
系列文
All In One NFT Website Development30

尚未有邦友留言

立即登入留言