本投稿は 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

上記画像は2021/12 時点のものです。
左ペインにコード、右ペインにその結果が表示されています。
DartPadのSunflowerというサンプルが、CustomPaintの例となっています。
コードと結果を照らし合わせてじっくり眺めてもらえば、なんとなく、動きとその実装がわかってくるかと思います。
さて、今回作るのはクリスマスツリーですので、おもむろに「Sunflower」という単語を「XmasTree」に書き換えちゃいましょう。
私は一旦vimにコードをコピペし、「:%s/Sunflower/XmasTree/g」としました。
次に、骨格となるところ以外を削ります。
- サンプルの、ひまわりの動きを実装している箇所 及び変数など
- 今回は使用しないAppBar, Drawerなど
結果は下記です。
※削ってないけど一部コードの記載を省略している箇所があります。
このままでは動きません。あくまで説明用です。
void main() {
runApp(XmasTree());
}
class XmasTreePainter extends CustomPainter {
XmasTreePainter();
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(XmasTreePainter oldDelegate) {
return oldDelegate.seeds != seeds;
}
}
class XmasTree extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _XmasTreeState();
}
}
class _XmasTreeState extends State<XmasTree> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
child: Column(
children: [
Container(
child: SizedBox(
child: CustomPaint(
painter: XmasTreePainter(seedCount),
),),),
Text("Showing $seedCount seeds"),
ConstrainedBox(
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);
double sideLength = 100;
leaves
..moveTo(topPos.dx, topPos.dy)
..lineTo(topPos.dx - sideLength * math.sin(math.pi / 6),
topPos.dy + sideLength * math.cos(math.pi / 6))
..lineTo(topPos.dx + sideLength / 2,
topPos.dy + sideLength * math.cos(math.pi / 6));
leaves.close();
canvas.drawPath(leaves, paint);
Pathのオブジェクト、leavesに対して、 「..」としているのはカスケード記法と呼ばれるもので、同一オブジェクトに対して複数の操作をするときに使える書き方です。
今回は、正三角形を作りました。

これと同じものを複数作成し、縦に少しずつずらしつつ重ねていけばいい感じになります。大きさも変えていくと、もっといい感じになります。
@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);
double dft = 0;
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);
結果がこちらです。

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は、葉を構成するループ内で使われた変数で、三角形の個数を表しています。
三角形の個数に応じて木の幹の太さを変えるイメージです。

オーナメントを飾ります
ペイントを重ねるときは基本後から書いたものが前面になります。
背面にしたいものから順番に書きましょう(描きましょう)。
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;
double startPointX = topPos.dx - 12 * (treeSize / 3 + 1) / 2;
garlandPath.moveTo(startPointX, startPointY);
double endPointY = topPos.dy + (trunkTop - topPos.dy) * 2 / 3;
double endPointX = topPos.dx + 20 * (treeSize / 3 + 1) / 2;
garlandPath.quadraticBezierTo(startPointX, endPointY, endPointX, endPointY);
canvas.drawPath(garlandPath, garland);
諸々数値がハードコードされていますが、これは絵を見て微調整して決定した値です。あまり気にしないでください。
Flutter(DartPad)にはホットリロード機能があり、保存したらすぐに結果に反映されるため、このような微調整が簡単にできます。
曲線は、quadraticBezierTo(中継地点のx座標, 中継地点のy座標, 終了地点x座標, 終了地点y座標) でベジェ曲線を描くことができます。
紐が垂れる曲線は、本当は重力加速度を加味したカテナリー曲線*2となるのですが、そこまでやると結構大変なので、これにて勘弁いただければと思います。
本当は複数ガーランドをかけたかったですが、座標の計算が大変だったため、寂しいですが一つとさせてください。

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(中心座標のオフセット, 半径, ペイントオブジェクト)」で描くことができます。

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 の角度にあります。
これを三角関数を使って直交座標に変換すればよいだけです。

これにて完成です!
ただ、せっかく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」をスライダーで可変にできるようにし、それぞれの値をカスタムペイントに渡す形に修正しました。スライダーを調整すると動きがよくわかると思います。

まとめ
完成したプログラムはこちらです。
dartpad.dev
色々と改変して遊んでみてください。
- オーナメントを追加する
- ボールが光るアニメーションを追加する。
- 他にも弄れるスライダーを追加する。(ガーランドの位置/数/太さを変える)
などなど
検証はしていませんが、Flutterですので、こちらで作成したプログラムはWeb, iOS, Androidで動かすことができると思われます。
ワンコードでいろいろなプラットフォームで動かすことができるのはやはり便利ですね。
では、メリークリスマス!
www.tecotec.co.jp