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でやってみたのでご紹介します。

続きを読む

React+Node.js(express)+MySQLの環境を整えてReactとNode.jsの自己学習に取り組んでみた

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

こんにちは、次世代デジタル基盤開発事業部の渡邊です。

今回の記事では、自己学習を目的として「React+Node.js+MySQL」の組み合わせで「マンガ管理アプリ」を作ってみた時に取り組んだことを備忘録として残していきます。

なお、今回の取り組みでは「予めMySQLに登録されたマンガ情報をReactで構築した検索画面から検索して表示させる」ことを目標としています。

イメージとしては以下のようなものを作ります。(かなりシンプルです・・・)

f:id:teco_watanabe_y:20211215171834p:plain

今回の記事は長い記事となりますので、先に構成をご紹介します。

目次

  • 目次
    • 環境
    • 環境構築
      • 1.WSL/Ubuntu/Node.js/npmのインストール
      • 2.MySQLのインストール
    • 接続確認
      • 1.React+Node.js
      • 2.Node.js+MySQL
      • 3.React+Node.js+MySQL
    • UIの実装
      • 1.Headerコンポーネントの実装
      • 2.Formコンポーネントの実装
      • 3.Listコンポーネントの実装
    • useStateの利用
    • POST通信の実装
      • 1.React側の修正
      • 2.Node.js側の修正
    • コンポーネントの表示/非表示切り替え
      • 1.Appコンポーネントの修正
      • 2.Formコンポーネントの修正
      • 3.Listコンポーネントの修正
    • まとめ
続きを読む

データ可視化ツール-Googleデータポータルを使ってみよう

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

はじめに

こんにちは!決済認証システム開発事業部の金城と申します。初投稿で緊張しておりますが、よろしくお願いいたします。

あらゆるデータが取得可能になったこの時代、溜めたデータを的確に分析し、活用することが益々重要になってきています。 しかし、膨大なデータの整理し、多くの人にとって理解しやすいように、デザインを工夫しながら可視化するのは容易なことではありません。

そんな時、Googleが無料で提供しているダッシュボード作成ツール「Googleデータポータル」を使えば、見やすいグラフを誰でも簡単に作成することができます!

今回は、Googleデータポータルの基礎的な使い方について紹介していきます。

目次

  • はじめに
  • 目次
  • 使ってみよう
    • データの準備
    • レポートの新規作成
    • データのインポート
    • グラフの作成
    • グラフの表示
    • データをさらに追加する
    • グラフを共有する
  • おわりに
続きを読む

GASで定期実行しているスクリプトのエラー通知をLINEに飛ばすようにしてみた

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

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

Google Apps Script(以下GAS)で毎日定期実行しているスクリプト(某暗号資産取引所のAPIを呼んでいる)があるのですが、極稀にエラーが発生してしまいます。

エラー発生時は元々GASからメール通知が来るようになってはいるのですが、即座に気がつくことが難しいです。

そこで、LINEに通知を飛ばしたいと思いました。実際やってみると予想以上に簡単だったので、やり方を紹介したいと思います。

続きを読む

【Laravel】APIを使って天気予報を取得してみた

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

はじめに

こんにちは。決済認証システム開発事業部の阿部です。
2021年4月に新卒で入社し、サーバーサイドエンジニアをしています。

業務の中でAPIを触ることが多いため、今回はOpenWeatherAPI(天気予報)を例にとってAPIの使い方を紹介しようと思います。

準備

アカウント登録

まずは、こちらからアカウント登録を行います。
アカウント登録後、API Keyが発行されます。ページ上部の[My API Keys]をクリックします。
f:id:teco_abe:20211216111827p:plain

発行されたAPI Keyを一覧できるページが開きます。このページの中のKey欄にある文字列をテキストファイルなどに保存しておきます。 f:id:teco_abe:20211216113142p:plain

APIの使い方の確認

プログラムを書く前にAPIの使い方の確認をします。

続きを読む

OAuth、OpenID Connectについて改めて調べてみた

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

はじめに

2021年9月に入社した決済認証システム事業部の田中です。
これまでバックエンドの開発・設計などを担当してきて、今はある案件のPMをやっています。

ふとAPIの認可/認証周りについてなんとなくの理解になっているなと思い、アドベンドカレンダーの記事執筆を機に改めて調べてみました。
すごく簡単ではありますが、OAuthとOpenID Connectについて触れていきます。

続きを読む