Hardhatで始めるDapps開発

どうも、次世代デジタル基盤開発事業部の土田です。

最近までLINE Blockchainを触ってましたが、Solidity関連のご相談も増えてますので、置いていかれないように頑張ります。 これまではTruffleSuiteで開発環境を構築してきましたが、今回はHardhatという開発環境を使ってみたいと思います。

TruffleSuiteで始めるDapps開発 - テコテック開発者ブログ

TruffleSuiteで始めるDapps開発2 - テコテック開発者ブログ

TruffleSuiteで始めるDapps開発3 - テコテック開発者ブログ

筆者環境

Windows10
WSL2(Windows Subsystem for Linux)
Ubuntu 20.04
MetaMask
Chrome
Hardhat 2.10.1

Hardhatをインストール

公式ドキュメントをご参照下さい。npmもyarnも特に問題なくインストールできました。

hardhat.org

早速起動

下記コマンドを実行し、サンプルプロジェクトを作成します。

npx hardhat

タスクとは

A task is a JavaScript async function with some associated metadata.

hardhat.org

Hardhatで何かをする時にはタスクとして定義して実行する必要があるそうです。

サンプルプロジェクトで実行できるタスクは下記コマンドを実行すると確認できます。

npx hardhat

JavaScriptのサンプル作成時にはaccountsという独自タスクが定義されていたのですが、TypeScriptでプロジェクトを作成すると無かったので作成してみます。

タスクはルートディレクトリのhardhat.config.tsに定義します。

import { HardhatUserConfig, task } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import { HardhatRuntimeEnvironment } from "hardhat/types";

const accounts = async (args: string, hre: HardhatRuntimeEnvironment) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
};

task("accounts", "Prints the list of accounts", accounts);

const config: HardhatUserConfig = {
  solidity: "0.8.9",
};

export default config;

再度タスクを確認すると・・・

accountsが追加されていますね!

早速実行してみましょう。

npx hardhat accounts

デフォルトで用意されているアカウントを確認することができました。

ノード起動

ノード起動はデフォルトのタスクが用意されていますので、そのまま使いましょう。

npx hardhat node

ついでにMetaMaskで起動したノードに接続し、アカウントもインポートしてみます。

ChainIDはデフォルトで31337だそうです。

hardhat.org

無事、接続&インポート完了しました。

送金も試してみます。

問題なく送金できましたね!
MetaMaskで送金を行った場合、ノードを再起動すると下記エラーが発生することがありますので、MetaMaskのアカウントリセットをお試し下さい。

## Nonce too high. Expected nonce to be 0 but got X. Note that transactions can't be queued when automining.

参考

medium.com

クリプトゾンビを進める

チャプター1の13まで進めます。

cryptozombies.io

当時、特に出なかったエラーが表示されたので、備忘として残しておきます。

Different number of components on the left hand side (1) than on the right hand side (0).

0.6以降でpushの戻り値が変わったそうです。

• Syntax: push(element) for dynamic storage arrays do not return the new length anymore.

github.com

該当箇所を下記の通り修正します。

        // 以下はエラー
        // uint id =  zombies.push(Zombie(_name, _dna)) - 1;

        zombies.push(Zombie(_name, _dna));
        uint id = zombies.length - 1;

コンパイルしてみる

コンパイルされたソース一式はルートディレクトリ直下のartifactsフォルダに生成されます。

npx hardhat compile

また、abiからTypeScriptも生成されます。HardhatではデフォルトでTypeChainを採用しているようです。

hardhat.org

github.com

生成後はルートディレクトリ直下のtypechain-typesに生成されます。 ソースコードはこんな感じ。

これで型安全にコントラクトの開発を行う準備ができました!

テストしてみる

テストコードの配置先のデフォルトはルートディレクトリ直下のtestフォルダになります。今回はZombieFactory.tsとしてテストコードを作成しました。

import { expect } from "chai";
import { ethers } from "hardhat";

describe("ZombieFactory.sol", function () {
  async function deployZombieFactory() {
    const ZombieFactory = await ethers.getContractFactory("ZombieFactory");
    const zombieFactory = await ZombieFactory.deploy();

    return { zombieFactory };
  }

  describe("createRandomZombie", function () {
    it("Verify that the name and DNA of the created zombie has 16 digits.", async function () {
      const { zombieFactory } = await deployZombieFactory();

      const testZombieName = "test";
      await zombieFactory.createRandomZombie(testZombieName);

      const zombie = await zombieFactory.zombies(0);
      expect(zombie.name).to.equal(testZombieName);
      expect(zombie.dna.toString().length).to.equal(16);
    });
  });

  describe("Events", function () {
    it("Should emit an event on createRandomZombie", async function () {
      const { zombieFactory } = await deployZombieFactory();

      await expect(zombieFactory.createRandomZombie("test")).to.emit(
        zombieFactory,
        "NewZombie"
      );
    });
  });
});

実行してみます。

npx hardhat test

無事、テストも通りました!

また、未コンパイルの状態ではコンパイルとテストを合わせて実行するようでした。

デプロイしてみる

デプロイするコントラクトを定義します。scripts/deploy.tsを修正しましょう。

import { ethers } from "hardhat";

async function main() {
  const ZombieFactory = await ethers.getContractFactory("ZombieFactory");
  const zombieFactory = await ZombieFactory.deploy();
 
  await zombieFactory.deployed();

  console.log("ZombieFactory deployed to:", zombieFactory.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

デプロイは下記コマンドを実行します。

npx hardhat run scripts/deploy.ts --network localhost

hardhat consoleでコントラクトを実行する

デプロイしたコントラクトをhardhat consoleでも確認してみます。

tsuchida@DESKTOP-PBCJR7M:~/workspace/hardhat-cryptzombie$ npx hardhat console --network localhost
Welcome to Node.js v16.15.1.
Type ".help" for more information.
> const ZombieFactory = await ethers.getContractFactory('ZombieFactory')
undefined
> const zombiefactory = await ZombieFactory.attach("コントラクトアドレス")
undefined
> await zombiefactory.createRandomZombie("test")

hardhat nodeを実行中のターミナルにて、実行されていることが確認できます。

今回のトラブルまとめ

Different number of components on the left hand side (1) than on the right hand side (0).solidity(7364)View Problem

0.6以降で配列が参照を返すようになったため、要素数を別途取得する必要があります。

参考

ethereum.stackexchange.com

github.com

error: ProviderError: Error: Transaction reverted without a reason string

hardhat consoleでコントラクトを実行した際に、下記のようなエラーが発生しました。この場合は、hardhat.config.tsに設定を追加する必要があります。

> await zombiefactory.createRandomZombie('test')
Uncaught:
Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (reason="Error: Transaction reverted without a reason string", method="estimateGas", transaction={"from":"アドレス","to":"アドレス","data":"データ","accessList":null}, error={"name":"ProviderError","code":-32603,"_isProviderError":true,"data":{"message":"Error: Transaction reverted without a reason string","data":"0x"}}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.6.8)
    at step (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
    at EthersProviderWrapper.<anonymous> (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:603:20)
    at checkError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:78:20)
    at Logger.throwError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/logger/src.ts/index.ts:273:20)
    at Logger.makeError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/logger/src.ts/index.ts:261:28) {
  reason: 'Error: Transaction reverted without a reason string',
  code: 'UNPREDICTABLE_GAS_LIMIT',
  method: 'estimateGas',
  transaction: {
    from: 'アドレス',
    to: 'アドレス',
    data: 'データ',
    accessList: null
  },
  error: ProviderError: Error: Transaction reverted without a reason string
      at HttpProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/http.ts:78:19)
      at AutomaticSenderProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/accounts.ts:351:34)
      at AutomaticGasProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/gas-providers.ts:136:34)
      at AutomaticGasPriceProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/gas-providers.ts:153:36)
      at BackwardsCompatibilityProviderAdapter.send (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/backwards-compatibility.ts:36:27)
      at EthersProviderWrapper.send (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:48)
      at EthersProviderWrapper.<anonymous> (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:601:31)
      at step (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
      at Object.next (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:29:53)
      at /home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:23:71
}

対策抜粋

const config: HardhatUserConfig = {
  solidity: "0.8.9",
  networks: {
    localhost: { allowUnlimitedContractSize: true },
    hardhat: { allowUnlimitedContractSize: true },
  },
};

参考

github.com

おわり

これでひと通りHardhatに触れることが出来ました。

Truffle、Ganacheも簡単でしたがHardhatも同じくらい簡単ですし、プラグインも充実していてTypeScriptの開発が捗りそうで良さそうです。動作も軽くていい感じなので、今後はHardhatでやると思います。

www.tecotec.co.jp

NestJSでAPI開発を行う

こんにちは、次世代デジタル基盤開発事業部の野口です。 関わったプロジェクトで以下の構成でexpressでバックエンド開発をしたので、そのアウトプットとして記事を書きます。

  • express
  • routing-controllers
  • inversify(DI)
  • typeorm

ただ、この環境を作るのが難しそうなので、もっと簡単にCLI一発で似たような環境が作れるNestJSで作成していきたいと思います。本当はNestJSを触ってみたかっただけですが、、、

nestjs.com

NestJSはNode.js上で動作するオープンソースのバックエンド開発フレームワークで expressをコアにして作られています。 詳しくは以下を読むと分かりやすいです。
NestJSにざっくり入門してみる

今回はtodoListのCRUD操作と簡単な検索機能を作成することをゴールとしたいと思います。

続きを読む

【React v18】リリース情報で気になった機能とcreate-react-app使用時の注意点

こんにちは、次世代デジタル基盤開発事業部の渡邊です。
今回は今年の3月末にリリースされたReact v18(※1)の中身が気になったので、それに関する備忘録と create-react-appでプロジェクト作成する際の諸注意(?)についてご紹介していきます。

※1:この記事はv18.0.0のリリース情報を参考に書いています。

[目次]

  • 環境
  • 新機能
    • 《useTransition》
  • 補足事項
    • 《create-react-appでプロジェクト作成する際の注意点》
  • まとめ
続きを読む

テコテック福利厚生が手厚くなったの巻

この度、テコテックの福利厚生の一環として企業型確定拠出年金に任意で加入できる制度が2022年誕生しました。 企業型確定拠出年金のざっくりとした制度は、毎月給与の一部を金融商品に投資を行って60歳になったら受け取りができる自分で自分の年金を作るというものです。 企業型確定拠出年金に加入すれば、所得税・住民税が減額されたりと様々なメリットもあります。詳しくは投資信託協会のHPをご覧ください。

www.toushin.or.jp

ここからは雑談になります。

特にいままで投資をしたことがなく、貯金のみという方に向けに個人的な想いをつらつら記載していきます。

将来のことを考え、毎月の給与から計画的に貯金していることは素晴らしいことです。でもその貯金は将来も今と同じ価値ですか!? 2022年、インフレの足音が聞こえてきていると感じています。インフレとはモノの値段が上がること。ガソリン代が上がったりアイスクリームなどの食品の値段が上がったり、電車の初乗り運賃も上がりましたね。皆様も日々感じているかと思います。 いままでなら100円で買えたものが120円出さないと買えなくなってきている。ここで貯金の話に戻すと、今、例えば100万円の貯金があったとしてもインフレによって将来的に現在の80万円程度の価値になってしまう可能性があるんじゃないのかってことです。

少しでもインフレに対抗するための策として投資があるのではと考えます。企業型確定拠出年金では自分のお給料の一部を投資信託で運用します。運用で損をすることももちろんありますが、投資をしないことで先ほど説明したように見えないところで損をしている可能性もあります。(補足しますが、企業型確定拠出年金の場合、運用で損をしていても加入しているだけで減税されているのでトータルでみるとよっぽどのことがない限りプラスです) 貯金の場合は現在利息もほとんどつかないため、金額が変化することはありませんが、投資信託は日々お金の価値が上下します。そういった経験をすることが出来るのは企業型確定拠出年金の良いところです。

投資は短期では結果が出にくいものです。長くコツコツ続けることが一番重要です。毎月購入する投資信託の価額が下がっても次に購入できる量が増えるので若い人ほどメリットが大きいです。下がっても悲観せず、同じ金額でたくさん買えるようになると考えると良いでしょう!

こちらはドルコスト平均法というもので、SBI証券のHPに詳しく記載してあります。

www.sbisec.co.jp

最後に

2022年4月からは高校で資産形成の授業が家庭科で始まります。私たちが学生時代にはなかったもので、いまの若い子はうらやましいなと思います。けど、時間は戻らないので、いまできる最善の策を考えていきましょう。

投資をしろ!と言うつもりもありません。給与のほんの一部を投資してみて面白さであったり、社会の動きが学べて、自分から進んで投資をしようと思ったら証券口座を開設して、日本株や米国株に投資していけばいいのではないのでしょうか?

とりあえず、テコテックは企業型確定拠出年金に任意で加入できる制度が誕生したよって話でした。投資を自分でやり始めるのは少しハードルが高いので会社でできるっていいですね。

その他の福利厚生の内容や会社の情報はリクルートサイトをご確認ください。

www.tecotec.co.jp

Pythonでバックテストのモンテカルロシミュレーションを行う

証券フロンティア事業部サーバーエンジニアの伊奈です。

前の記事(Webhookでのティック取得から東証「arrowhead」への流し込み -後編- - テコテック開発者ブログ)を書いた後から、システムトレードのバックテスト(決められたルールに従って機械的に行う取引を過去の価格データを使って検証すること)をしていました。

その際にバックテストで得られた期待値をモンテカルロシミュレーションしたのでそのことを書きたいと思います。

モンテカルロシミュレーションを行うことで、バックテストでは得られなかった統計的特性を見つけ取引戦略の有効性を評価できます。

続きを読む

FlutterのCustomPaintウィジェットでクリスマスツリーを描いて遊ぶ

本投稿は TECOTEC Advent Calendar 2021 の25日目の記事です。

こんにちは。次世代デジタル基盤開発事業部の六本木です。

テコテックといえばブロックチェーン。ブロックチェーンといえばビットコイン。ビットコインといえばマークルツリー!*1 ということで、マークルツリーのつくり方を共有します!

。。といきたいところですが、この季節はマークルツリーよりもクリスマスツリーですよね。 ということでクリスマスツリーのつくり方共有します!!

(マークルツリーのつくり方はわからないので、ちゃんと勉強してから いつか詳しく共有しますね)

また、わたくしFlutterの勉強中でして、最近FlutterのCustomPaintウィジェットを使うとお絵描きできることを知ったので、 今回クリスマスツリーはFlutterで作ってみたいと思います。

Flutterとは

FlutterとはGoogleが開発するモバイルアプリのクロスプラットフォーム開発可能なフレームワークで、 ウィジェット(Widget)と呼ばれるコンポーネントを組み合わせることにより柔軟にアプリ開発を行うことができます。 Flutterで用いるプログラミング言語Dartの構文がJavaやJavaScriptに似ているので、これらの言語の経験とオブジェクト指向の理解があれば、 簡単にアプリ開発を始めることができます。

flutter.dev

CustomPaintとは

Flutterのウィジェットの一つで、柔軟に絵をかくことができます。 既存のウィジェットには存在しない形のウィジェットを描きたいときなどに利用します。 三角形、矩形、曲線、円、なんでも書くことができます。

api.flutter.dev

では早速ですが始めましょう。

CustomPaintのひな型を用意する

Dart のコードスニペットツールである、DartPadにカスタムペイントのサンプルがあるので、そちらを使いましょう。

dartpad.dev

f:id:teco_roppongi:20211206195759p:plain

上記画像は2021/12 時点のものです。

左ペインにコード、右ペインにその結果が表示されています。 DartPadのSunflowerというサンプルが、CustomPaintの例となっています。

コードと結果を照らし合わせてじっくり眺めてもらえば、なんとなく、動きとその実装がわかってくるかと思います。

さて、今回作るのはクリスマスツリーですので、おもむろに「Sunflower」という単語を「XmasTree」に書き換えちゃいましょう。 私は一旦vimにコードをコピペし、「:%s/Sunflower/XmasTree/g」としました。

次に、骨格となるところ以外を削ります。

  • サンプルの、ひまわりの動きを実装している箇所 及び変数など
  • 今回は使用しないAppBar, Drawerなど

結果は下記です。

※削ってないけど一部コードの記載を省略している箇所があります。 このままでは動きません。あくまで説明用です。

void main() {
  runApp(XmasTree());
}

class XmasTreePainter extends CustomPainter { // ★1
  XmasTreePainter();

  @override
  void paint(Canvas canvas, Size size) { // ★2
  }
  @override
  bool shouldRepaint(XmasTreePainter oldDelegate) { // ★3
    return oldDelegate.seeds != seeds;
  }
}

class XmasTree extends StatefulWidget { // ★4
  @override
  State<StatefulWidget> createState() {
    return _XmasTreeState();
  }
}

class _XmasTreeState extends State<XmasTree> { // ★5
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          child: Column(
            children: [
              Container(
                child: SizedBox(
                  child: CustomPaint( // ★6
                    painter: XmasTreePainter(seedCount),
                  ),),),
              Text("Showing $seedCount seeds"),
              ConstrainedBox( // ★7
                child: Slider.adaptive(
                  min: 20,
                  max: 2000,
                  value: seeds,
                  onChanged: (newValue) {
                    setState(() {
                      seeds = newValue;
                    });
                  },),),],),),),);
    }
}

それぞれ特筆する箇所を★でコメントしました。

★4, ★5 はFlutterに馴染みのない人はギョッとするかもしれませんが、StatefulWidgetを扱う際はよくやる書き方です。 理解していないうち は、細かいことを気にせず、そういうものだと思って書いてOKです。(かくいう私もその一人)

★6 でCustomPaint ウィジェットのコンストラクタ引数painterに、今回作成するペインターを指定しています。

★1にて、CustomPainterを継承したXmasTreePainterを定義しています。 抽象クラスCustomPainterは、以下2つをオーバーライドする必要があります。

  • ★2 paintメソッド:ここに絵を書いていきます。引数canvasに対して、draw〇〇(例えばcanvas.drawCircleで円)として絵を描きます。
  • ★3 shouldRepaintメソッド:名称とサンプルから、ウィジェットを再描画する条件を書くもの。という認識ですが、ちゃんとは理解していません。return Falseとしてもなぜかちゃんと動いているように見えます。。

★7は、サンプルでは種の数を調整するスライダーとなっていますが、クリスマスツリーの特徴を調整するスライダーに流用したいと思うので残しておきます。

単純図形でツリーを描こう

モミの木を描く
1. 葉の部分を描く

モミの木の尖った葉は、二等辺三角形を縦にずらしながら重ねていけば、表現することができます。 三角形は、いろいろと書き方があるかと思いますが、このように書きました。

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    // 葉っぱなので緑色
    paint.color = Colors.green; 
    // 描く図形の辿る経路
    var leaves = Path(); 
    // 三角形の頂点の座標
    Offset topPos = Offset(size.width / 2, size.height / 5);    
    // 三角形の1辺の長さ
    double sideLength = 100;
    
    leaves
       ..moveTo(topPos.dx, topPos.dy) // 開始点を指定
       ..lineTo(topPos.dx - sideLength * math.sin(math.pi / 6), // 直線の端のx座標(左下)
           topPos.dy + sideLength * math.cos(math.pi / 6))       // 直線の端のy座標(左下)
       ..lineTo(topPos.dx + sideLength / 2,                                // 直線の端のx座標(右下)
           topPos.dy + sideLength * math.cos(math.pi / 6));       // 直線の端のx座標(右下)
        
    leaves.close(); // 最後の点と、開始点を結ぶ
    canvas.drawPath(leaves, paint); //キャバスに描く

Pathのオブジェクト、leavesに対して、 「..」としているのはカスケード記法と呼ばれるもので、同一オブジェクトに対して複数の操作をするときに使える書き方です。 今回は、正三角形を作りました。

f:id:teco_roppongi:20211207100737p:plain

これと同じものを複数作成し、縦に少しずつずらしつつ重ねていけばいい感じになります。大きさも変えていくと、もっといい感じになります。

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    // 葉っぱなので緑色
    paint.color = Colors.green; 
    // 描く図形の辿る経路
    var leaves = Path(); 
    // クリスマスツリーの頂点の座標
    Offset topPos = Offset(size.width / 2, size.height / 5);    
    // クリスマスツリー頂点からの距離 dft: distance from top
    double dft = 0;
    // 三角形の1辺の長さ
    double sideLength = 0;
 // 三角形の数
 int treeSize = 20;

    for (i = 0; i < treeSize; i++) {
      dft = topPos.dy + (1 / 2) * math.pow(i + 1, 2).toDouble(); //頂点からの距離は二乗で伸びていくようにする
      sideLength = 10 * (i + 1); 

      leaves
        ..moveTo(topPos.dx, dft)
        ..lineTo(topPos.dx - sideLength * math.sin(math.pi / 6),
            dft + sideLength * math.cos(math.pi / 6))
        ..lineTo(topPos.dx + sideLength / 2,
            dft + sideLength * math.cos(math.pi / 6));
    }

    leaves.close();
    canvas.drawPath(leaves, paint);

結果がこちらです。

f:id:teco_roppongi:20211207101755p:plain

2. 木の幹と、スタンドカバーを描く

木の幹と、スタンドカバー(木の根元を隠すカバー?)は四角形で表現しましょう

    // もみの木のみき
    var trunkLeft = topPos.dx - i;
    var trunkTop = dft + sideLength * math.cos(math.pi / 6);
    var trunkWidth = 2 * i.toDouble();
    var trunkHeight = i.toDouble();
    paint.color = Colors.brown;
    var trunk = Rect.fromLTWH(trunkLeft, trunkTop, trunkWidth, trunkHeight);
    canvas.drawRect(trunk, paint);
   
    // スタンドカバー
    var standLeft = trunkLeft - i;
    var standTop = trunkTop + trunkHeight;
    var standWidth = 2 * trunkWidth;
    var standHeight = 2 * trunkHeight;
    var stand = Rect.fromLTWH(standLeft, standTop, standWidth, standHeight);
    canvas.drawRect(stand, paint);

四角形は「Rect.fromLTWH(左の座標, 上の座標, 幅, 高さ)」で定義することができます。

iは、葉を構成するループ内で使われた変数で、三角形の個数を表しています。

三角形の個数に応じて木の幹の太さを変えるイメージです。

f:id:teco_roppongi:20211207102658p:plain

オーナメントを飾ります

ペイントを重ねるときは基本後から書いたものが前面になります。 背面にしたいものから順番に書きましょう(描きましょう)。

1. ガーランド

ツリーに掛けるひも状のやつです。

    // ガーランド
    var garland = Paint()
      ..color = Colors.white                              // 色は白
      ..style = PaintingStyle.stroke                   // 線状
      ..strokeWidth = treeSize.toDouble() / 3; // 線の幅

    final garlandPath = Path();
    double startPointY = topPos.dy + (trunkTop - topPos.dy) * 1 / 3; // 開始地点のy座標(左上)
    double startPointX = topPos.dx - 12 * (treeSize / 3 + 1) / 2;         // 開始地点のx座標(左上)
    garlandPath.moveTo(startPointX, startPointY);                               // 開始地点を定義

    double endPointY = topPos.dy + (trunkTop - topPos.dy) * 2 / 3;  // 終了地点のy座標(右下)
    double endPointX = topPos.dx + 20 * (treeSize / 3 + 1) / 2;         // 終了地点のx座標(右下)

    garlandPath.quadraticBezierTo(startPointX, endPointY, endPointX, endPointY); // ベジェ曲線を描く

    canvas.drawPath(garlandPath, garland);

諸々数値がハードコードされていますが、これは絵を見て微調整して決定した値です。あまり気にしないでください。

Flutter(DartPad)にはホットリロード機能があり、保存したらすぐに結果に反映されるため、このような微調整が簡単にできます。

曲線は、quadraticBezierTo(中継地点のx座標, 中継地点のy座標, 終了地点x座標, 終了地点y座標) でベジェ曲線を描くことができます。

紐が垂れる曲線は、本当は重力加速度を加味したカテナリー曲線*2となるのですが、そこまでやると結構大変なので、これにて勘弁いただければと思います。

本当は複数ガーランドをかけたかったですが、座標の計算が大変だったため、寂しいですが一つとさせてください。

f:id:teco_roppongi:20211207104504p:plain

2. ボール

ガーランドが寂しい感じなので、せめてボールはカラフルにしましょう。 ランダムにボールを配置するようにしました。

    // ボール
    List<MaterialColor> colorList = [
      Colors.red,
      Colors.blue,
      Colors.lightGreen
    ];
    int ballNum = 20;
    for (int j = 0; j < ballNum; j++) {
      double randomPosY = math.Random().nextDouble() * (trunkTop - topPos.dy);
      double randomPosX =
          2 * (math.Random().nextDouble() - (1 / 2)) * randomPosY / 4;
      int randomColorIndex = math.Random().nextInt(colorList.length);
      paint.color = colorList[randomColorIndex];
      canvas.drawCircle(Offset(topPos.dx + randomPosX, topPos.dy + randomPosY),
          i * size.width / 1000, paint);
    }

今回は3色用意しました(赤、青、薄緑)

そして、20個のボールを配置するわけですが、その位置と色はランダムにします。

ファイルの先頭に「import 'dart:math' as math;」をすると、ランダムに数値を生成してくれるメソッドを使うことができるようになります。

「math.Random().nextDouble」で「0~1」までのランダムな実数、「math.Random().nextInt(数)」で「0~数」までのランダムな整数を生成することができます。

Y座標とX座標はツリーからはみ出さないようにだけ注意してください。宙に浮いてしまうので。

ボールは円で表現します。円は「 canvas.drawCircle(中心座標のオフセット, 半径, ペイントオブジェクト)」で描くことができます。

f:id:teco_roppongi:20211207105839p:plain

3. 星(五芒星)

これがあるとないとではクリスマスツリー感が100倍くらい違うと思いますので、必須です。

    paint.color = Colors.yellow;
    var star = Path();

    double radius = i.toDouble();
    for (int j = 0; j < 6; j++) {
      var angle = -math.pi / 10 + j * 4 * math.pi / 5;
      star.lineTo(radius * math.cos(angle) + topPos.dx,
          radius * math.sin(angle) + topPos.dy);
    }
    canvas.drawPath(star, paint);

一筆書きで星を描く要領でプログラムすればよいです。

線は5個引きますので5回ループします。

星の各頂点が円上に分布されると考えると、隣の頂点は、現時点での頂点の2π/5の角度にあります。

ただ、一筆書きする場合は頂点を一個スキップしますので 2×2π/5 = 4π /5 の角度にあります。

これを三角関数を使って直交座標に変換すればよいだけです。

f:id:teco_roppongi:20211207111303p:plain

これにて完成です! ただ、せっかくFlutterでやっていますのでもう少しだけ遊び心を入れましょう

木の高さとボールの数を調整できるようにする

Sunflowerのサンプルに調整スライダーがありましたので、それを流用させていただき、 木の高さとボールの数を調整できるようにしましょう。

class XmasTreePainter extends CustomPainter {
  // こちらのクラスにプロパティを保持
  final int treeSize;
  final int ballNum;

  // コンストラクタでプロパティに値を渡す
  XmasTreePainter(this.treeSize, this.ballNum);

  @override
  void paint(Canvas canvas, Size size) {
  略
  int treeSize = 20; // ←これを探して消すint ballNum = 20; // ←これを探して消す
  略  
  }

// スライダーの種別
enum barType {
  ballNum,
  treeSize,
}

class _XmasTreeState extends State<XmasTree> {
  // デフォルトの値
  int treeSize = 20; 
  int ballNum = 10;

  // スライダーの定義
  Widget _adjustmentSlider(barText, min, max, value, barType bar) {
    return Column(
      children: [
        Text("$barText $value"),
        ConstrainedBox(
          constraints: const BoxConstraints.tightFor(width: 200),
          child: Slider.adaptive(
            min: min,
            max: max,
            value: value.toDouble(),
            onChanged: (newValue) {
              setState(() {
                if (bar == barType.ballNum) {
                  ballNum = newValue.toInt();
                } else if (bar == barType.treeSize) {
                  treeSize = newValue.toInt();
                }
              });},),),],);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          child: Column(
            children: [
              Container(
                child: SizedBox(
                  child: CustomPaint( 
                    painter: XmasTreePainter(treeSize, ballNum), // ★ここで値を渡す
                  ),),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _adjustmentSlider(
                      "木の大きさ", 3, 30, treeSize.toDouble(), barType.treeSize),
                  _adjustmentSlider(
                      "ボールの数", 1, 30, ballNum.toDouble(), barType.ballNum),
                ],}}
}

木の高さ「treeSize」とボールの数「ballNum」をスライダーで可変にできるようにし、それぞれの値をカスタムペイントに渡す形に修正しました。スライダーを調整すると動きがよくわかると思います。

f:id:teco_roppongi:20211207113040p:plain

まとめ

完成したプログラムはこちらです。

dartpad.dev

色々と改変して遊んでみてください。

  • オーナメントを追加する
  • ボールが光るアニメーションを追加する。
  • 他にも弄れるスライダーを追加する。(ガーランドの位置/数/太さを変える)

などなど

検証はしていませんが、Flutterですので、こちらで作成したプログラムはWeb, iOS, Androidで動かすことができると思われます。 ワンコードでいろいろなプラットフォームで動かすことができるのはやはり便利ですね。

では、メリークリスマス!

www.tecotec.co.jp

Google Cloud Vision APIを使ってPDFからテキスト検出

本投稿は TECOTEC Advent Calendar 2021 の24日目の記事です。

はじめに

こんにちは、決済認証システム開発事業部の鍛治と申します。
現在プロジェクトでGoogleVisionAPIを使った光学文字検出機能(OCR)の開発に携わっています。 VisionAPIとはGoogleの提供する機械学習モデルを用いて画像から顔検出やテキスト情報を取得できる機能です 。
本記事ではPDFからのテキスト検出をPHPでやってみたのでご紹介します。

続きを読む