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 25. v-on的修饰符

>>:  【Day 25】C String

Day 4— 自动化回信机(1) 前置作业

今天开始就要来利用专题了解 GAS 的各项功能罗~ 说到要能优雅、或是狼狈的…总之要能够享受下午茶,...

Day.5 「我的样式失灵啦!你有头绪吗?」 —— CSS 选择器 与 权重

了解盒模型後,就要为标签套上各种花样了,上一篇介绍了简单的套用方法,但这个套用方法其实不太好用! ...

DAY 30 好用的套件

推荐扩充套件 Color Highlight 这边跟大家推荐 Color Highlight 这个扩...

【Day 24】用 SOLID 方式开发 React (1)

前言 在 OOP 的世界里,我们常常会听到高内聚(Cohesion),低耦合(Coupling),以...

Day 20 - Maybe Monad II (Piping)

Review 前一篇文章我们谈到了如何实作一个 Maybe Monad,而其主要的功能就是处理无值的...