LoginSignup
5
6

More than 1 year has passed since last update.

Webpack + TypeScript で fxhash

Last updated at Posted at 2022-06-27

はじめに

本記事では、TezosのGenerative Artプラットフォームである fxhash で、NFTシリーズをつくるためのWebpack + TypeScript環境を紹介します。
fxhashでは任意の動的なhtmlをNFTシリーズとして作品化できます。WebpackやTypeScriptなどJavaScriptでの開発における効率化手段を導入することは、単純作業を自動化しクリエイティブプロセスに専念できるようになるため恩恵は大きいです。

まず実装した環境とその説明をします。文末に、WebpackやTypeScriptを導入する動機やfxhashについての説明を加筆しました。
実装したテンプレートは以下のリポジトリで公開しています。ご自由に活用&改変してください。

p5.js template

three.js template

TypeScriptのための型定義

WebpackによってTypeScriptのコンパイル環境を作りますが、この点の説明は省きます。上記のコードを参照したり、様々な良記事を参照することができます。

TypeScript導入で注意すべき点は、既存のJavaScriptを内包する際は、既存コードについても型情報を与えてあげないと、コンパイルが通らないということです。なので、fxhashのテンプレートから提供されるオブジェクトや関数の型定義を行います。

希少性(Rarity)をデザインするためにfxhashではFeaturesという仕組みがあります。これをAPI側が読めるように、window.$fxhashFeaturesに任意のキーバリューを入れますが、そのままだとWindowオブジェクトに$fxhashFeaturesというメンバー実装がないためコンパイルできません。この部分は、window.$fxhashFeatures はメンバーが不定なので、anyの型としてWindowオブジェクトをinterfaceとして拡張することで実現できます。

src/types/fxhash.d.ts
declare let fxhash: string;
declare let fxrand: () => number;
declare let fxpreview: () => void;
declare let isFxpreview: boolean;

interface Window {
    $fxhashFeatures?: any;
}

実際のソースでは以下のようにしています。

// 任意のパラメータを作る
const fxhashFeatures = {
    'theme': rand() > 0.5 ? 'light' : 'dark',
    'test rand value': rand(0, 10.0),
};

window.$fxhashFeatures = fxhashFeatures;

sandboxでこのように見えれば、設定は成功しています。
image.png

ESLint, Prettierの追加

コード自体の品質(コーディングスタイルの一貫性や可読性)をあげるために、LinterPerttierを導入することは良いことに思えます。

導入については以下が参考になります。

今回はTypeScriptであることも考慮します。

npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier prettier

それぞれ、ESLintとPrettierの設定は以下にしています。このあたりは、マイルールを決めて運用します。

.eslintrc
{
    "root": true,
    "parser": "@typescript-eslint/parser",
    "plugins": [
        "@typescript-eslint"
    ],
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended",
        "prettier"
    ]
}

.prettierrc
{
    "trailingComma": "es5",
    "tabWidth": 4,
    "semi": true,
    "printWidth": 130,
    "singleQuote": true,
    "endOfLine": "auto"
}

開発環境とリリース環境それぞれの設定

この項目は、ほぼすべて公式のBoilerplateが提案と実装をしており、本記事はその引用+若干の改変と解説をします。

開発時は、開発サポートのための諸機能を使いながらクイックにイテレーションをまわしたいですが、作品としてプラットフォームに提出するときは、開発サポートの諸機能はすべて省き、ファイル容量を可能な限り小さくし、ストレージコストを抑え、表示速度を上げることが重要になります。また作品提出時は、一式をZipに固める必要があるので、これも自動化することで提出時の作業の負荷を下げます。

Webpackでの必要なタスクの設定を複数に(開発とリリースなどに)切り分けるには、コマンドごとに読み込ませるConfigファイルを変えるという素朴な方法で実現できます。以下のようにすれば、npm run buildnpm run devで異なる設定のWebpackのタスクを走らせることができます。

package.json
{
    ...
    "scripts": {
        "build": "webpack --config ./config/webpack.config.prod.js",
        "dev": "webpack serve --config ./config/webpack.config.dev.js"
    }
    ...
}

開発用とリリース用の目的に応じたタスク分けについて、それぞれの方針と実装を以下に示します。

開発用

方針

  • 難読化しない
  • source-mapを有効化し、エラーコードを確認しやすくしたり、ステップ実行をできるようにする
  • 仮想サーバーを作り、ホットリロードを有効にする

実装

/config/webpack.config.dev.js
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    // modeをdevelopment指定をすると難読化しない
    mode: "development",
    // source-mapを有効化することで、出力となるmain.jsとソースの対応を保持する
    devtool: "source-map",
    entry: "./src/index.ts",
    output: {
        path: path.resolve(__dirname, "../dist"),
        filename: "main.js",
        clean: true,
    },
    resolve: {
        extensions: [".js", ".ts"],
        modules: [path.resolve("./src"), path.resolve("./node_modules")],
    },
    module: {
        rules: [
            { test: /\.ts$/, loader: "ts-loader", exclude: /node_modules/ }
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            favicon: false,
            template: "./public/index.html"
        }),
    ],
    devServer: {
        hot: true,
        port: 8080,
        open: true,
        client: {
            overlay: {
                errors: true,
                warnings: false,
            },
        },
    },
}

リリース用

方針

  • 難読化・軽量化をする
  • Sourcemapの生成を止める
  • 出力されたコンパイル済みのスクリプトやアセット(バンドル)一式をZip化する

実装

/config/webpack.config.prod.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const AdmZip = require("adm-zip")

// Zip化のためのプラグインクラス
class ZipperPlugin {
    constructor(options = {}) {
        this.options = {
            outputPath: path.resolve(__dirname, "../dist-zipped/project.zip"),
            ...options
        }
    }

    apply(compiler) {
        compiler.hooks.done.tapAsync(
            "ZipperPlugin",
            (stats, callback) => {
                const outputPath = stats.compilation.outputOptions.path;
                const zip = new AdmZip();
                zip.addLocalFolder(outputPath);
                zip.toBuffer();
                zip.writeZip(this.options.outputPath);
                callback();
            }
        )
    }
}

module.exports = {
    // modeをproduction指定をすると軽量化・難読化をおこなう
    mode: "production",
    entry: "./src/index.ts",
    output: {
        path: path.resolve(__dirname, "../dist"),
        filename: "main.js",
        clean: true,
    },
    resolve: {
        extensions: [".js", ".ts"],
        modules: [path.resolve("./src"), path.resolve("./node_modules")],
    },
    module: {
        rules: [
            { test: /\.ts$/, loader: "ts-loader", exclude: /node_modules/ }
        ],
    },
    plugins: [
        // index.htmlをバンドルに加える
        new HtmlWebpackPlugin({
            favicon: false,
            template: "./public/index.html"
        }),
        // index.html以外をバンドルに加える
        new CopyPlugin({
            patterns: [
                {
                    from: "public",
                    filter: async (filePath) => {
                        return path.basename(filePath) !== "index.html"
                    }
                }
            ]
        }),
        // outputとなるディレクトリをZip化
        new ZipperPlugin()
    ]
}

ランダム関数の開発環境とリリース環境での出し分け

fxhashでの作品に用いる乱数は、fxrand()を用います。これは、NFT毎に個別のハッシュ値の文字列fxhashをシード値にしており、この値が変わらないと、fxrand()の返す値は変わりません。何もしないと、この値は、fxhash用の埋め込みスクリプトに記述された値のままです。乱数の固定は、様々な乱数バリエーションをクイックに確認したいときに不都合になるので、思い切って、開発時とリリース時で使うランダム関数を分けます。

具体的な説明として、1~10の間の乱数を作りたい場面を例にします。Processingの組み込み関数を使う場合は、

const val: number = p.random(1, 10);

Math.randomを使う場合は、

const val: number = 1 + Math.random() * (10 - 1);

両方ともリロードすると乱数も変わるのでバリエーションをクイックに確認できます。しかし、作品と提出する場合は、乱数生成はfxrandで行う必要があります。よって

const val: number = p.lerp(fxrand(), 1, 10);

というような書き換えが必要になります。この場合、fxrand関数はリロードしても変わらない値を返します。開発時とリリース時でいちいちコードを書き換えるのは馬鹿にならない手間となるので、ソースの実装を明示的に変えなくても、開発時とリリース時で異なる乱数生成器を選択するようなrand関数を実装します。

rand関数の実装方法としては、開発環境とリリース環境に応じたConfigを変え、環境変数に与える値も変えます。これによってソース側で開発環境かリリース環境かを判定できるようになるのでランダム関数の出し分けが可能になります。

config/webpack.config.dev.js
// Config用Jsonオブジェクトを代入
// ここで読み込むファイルを開発時用とリリース時用で書き換える
const environmentConfig = require(path.resolve(__dirname, `./env/dev.js`));

module.exports = {
    plugins: [
        ...,
        // 開発環境とリリース環境で異なる内容がソースから読み取りできる
        new webpack.DefinePlugin({
            'process.env': JSON.stringify(environmentConfig)
        }),
    ]
}
src/utils/rand.ts
// import文なしに、process.envを読み込める 
const seedRandom = process.env.isDebug ? Math.random : fxrand;

export const rand = (min?: number, max?: number): number => {
    if (min !== undefined && max === undefined) {
        return seedRandom() * min;
    } else if (min !== undefined && max !== undefined) {
        return min + seedRandom() * (max - min);
    }
    return seedRandom();
};

これで、以下のような書き方で統一して書くことができるようになりました。

const val: number = rand(1, 10);

おまけ1:導入の動機

Webpackの利点

Webpackは、Webに必要なアセット類を効率よく準備することを目的としたNode環境のタスクランナーですが、必要な記述量の割にできることが多いです。npmに公開されているツール類を広く利活用でき、コードの品質を担保したりできます。例えば、セミコロン有無やインデントの方法を統一できるPrettierを導入したりできます。

また、ファイル類を処理する方法を、開発時とリリース時とで分けることができます。開発時は、開発サポートのための諸機能を使いながらクイックにイテレーションをまわしたいですが、作品としてプラットフォームに提出するときは、開発サポートの諸機能はすべて省き、ファイル容量を可能な限り小さくし、ストレージコストを抑え、表示速度を上げることが重要になりますが、こうした相反する出力方法の両方にうまく対応し、手順化することができます。

TypeScriptの利点

JavaScriptは本来、型が厳格でないスクリプト言語ですが、TypeScriptは型を厳格にしたインタプリタ言語に近づけます。一見これは冗長で面倒なものにするように思われますが、私は制作における恩恵は大きいと感じています。
型が厳格になることで、まずエディターがコードを解析しやすくなります。書いているコードに対して、クラス・メソッドの列挙、タイピング中のサジェスト、クラスやメソッドが宣言された場所に素早くジャンプできる機能(コードジャンプ:WindowsのVS CodeだとF12)が使えるようになります。これによって、使うAPIを深く理解せずとも、コードサジェストやメソッドの引数や実装の確認がスムーズになるため、素早く確実なコーディングができるでしょう。

とくにThree.jsのような引数の仕様が複雑な異なるオブジェクトを、次々とインスタンス化して操作することが必要になるライブラリでは、ストレスが大幅に軽減されます。

おまけ2:fxhashについて

fxhashはGenerative ArtのNFTプラットフォームです。主にブラウザで動作するグラフィック要素を持つhtmlをNFTとして作品化し販売できます。

興味深い点は、購入時に生成される一意の文字列(hash値)をシード値にしてランダム関数等を動かすことで、一つのコード/トークン(Generative Token)から複数のバリエーション(NFTs)を生成するいわば作品のシリーズを自体を扱うことにプラットフォームとしての主眼があることです。グラフィックを生成するために多用するOSSであるThree.jsやp5.jsなどと利益を按分する方法が用意されていることもプラットフォームの態度として称賛すべき点です。

また、シリーズの中でパラメータもプログラムによって確率操作することで、希少性を意図的に設計でき、かつその希少性の判定や集計機能がプラットフォーム側で実装されているので、シリーズをつくる上での楽しさやNFT購入への期待の情勢ができます。

仮想通貨は支配的なプラットフォームであるEthereumではなく、Tezosです。チェーン検証作業のアルゴリズムにProof of Stake (PoS)を採用しているために、いわゆるハッシュレート競争(ブロックを検証し報酬を得るための計算スピードの競争、そこから大型のマイニングプラットフォームによるGPU買い占め競争もBitcoinなどPoWプラットフォームでは巻き起こりました)と、マイニングにかかる莫大な計算量による電力コスト・環境負荷からは自由で、アーティストやプログラマーは、そういった批判の文脈からは開放されて制作をできるということは心理的なメリットに思います。

fxhashは単純なHTMLとして開けるファイル群を用意できればNFT化できますが、各シリーズがNFTとして不変である必要があり、IPFSにホストされるという点からもCDNなど外部リソースや、APIを叩いたり、または、カメラなどデバイスのリソースにアクセスしようとするコードは動作できません。全体の作品の容量も現状15MB以内の制限があります。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6