【Flutter】カスタマイズ描画（CustomPaint）のCanvas

カスタマイズ描画について

- [前編](https://qiita.com/ling350181/items/9a42411b38d1ea26d538)
- [Paint編](https://qiita.com/ling350181/items/19d26741981a86a4026a)
- [Path編](https://qiita.com/ling350181/items/745c0c6c04c4037298de)

※本記事は下記のZenn本にまとめました。

https://zenn.dev/cxj/books/07def04506182d

# キャンバス変換
キャンバス変換関数は下記５つある。
```dart
void skew(double sx, double sy)
void rotate(double radians)
void scale(double sx, [double sy])
void translate(double dx, double dy)
void transform(Float64List matrix4)
```
- translate(double dx, double dy)
並行移動
- scale(double sx, [double sy])
スケール
- rotate(double radians)
回転する
- skew(double sx, double sy)
斜め切り替え
- transform(Float64List matrix4)
矩形移動

## translate
translateはキャンパスのの中心を移動する関数です。
下記のソースで並行移動を確認しましょう。
```dart
// キャンバスの開始点を画面の中央に移動します
canvas.translate(size.width / 2, size.height / 2);
```
上記の中心点移動あるとないの違いは下記の図で一目瞭然です。黄色枠は基準です。
![translate](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/a2ae05ca-30f0-3601-ad60-96a848c685f8.png)

ソースコード：
```dart
class Demo extends StatefulWidget {
  @override
  _DemoState createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        title: const Text("Demo"),
      ),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Container(
              color: Colors.yellow,
              height: 100,
              width: 100,
              child: CustomPaint(
                painter: CirclePainter(false),
              ),
            ),
            Container(
              color: Colors.yellow,
              height: 100,
              width: 100,
              child: CustomPaint(
                painter: CirclePainter(true),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CirclePainter extends CustomPainter {
  final bool show;
  CirclePainter(this.show);
  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.blue;
    if (show) {
      // キャンバスの開始点を画面の中央に移動します
      canvas.translate(size.width / 2, size.height / 2);
    }
    canvas.drawCircle(const Offset(0, 0), 50, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

```

並行移動を用いてグリッドの描画もできます。
考え方は下記です。
1. 描画スタートはキャンパスの中心へ移動
1. 線を描く
1. 中心をX軸方向へ並行移動し,横線を描く
1. それを繰り返して処理する
1. 中心点をリセットする
1. 線を描く
1. 中心をY軸方向へ並行移動し,縦線を描く
1. それを繰り返して処理する

ソースコード：
```dart
  // グリッドの幅
  final double space = 20;
  Paint _gridPint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..color = color;

  void _drawGridRight(Canvas canvas, Size size) {
    // キャンパスの保存
    canvas.save();
    // 横線を描く
    for (int i = 0; i < size.width / 2 / space; i++) {
      canvas.drawLine(Offset(0, 0), Offset(0, size.height / 2), _gridPint);
      canvas.translate(space, 0);
    }
    // キャンパスリセット（原点リセット）
    canvas.restore();

    canvas.save();　　
    // 縦線を描く
    for (int i = 0; i < size.height / 2 / space; i++) {
      canvas.drawLine(Offset(0, 0), Offset(size.width / 2, 0), _gridPint);
      canvas.translate(0, space);
    }
    canvas.restore();
  }
```
効果は下記通りです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/64a2614e-c356-614b-6450-45de43042382.png"width=280>

## スケール
スケールを用いて鏡像を作ることは可能です。
では、スケールを用いて上記のグリッドを完成させましょう。
```dart
  void _drawGrid(Canvas canvas, Size size) {
    // 右下のグリッド
    _drawGridRight(canvas, size);

    canvas.save();
    // X軸の镜像
    canvas.scale(1, -1);
    _drawGridRight(canvas, size);
    // リセット
    canvas.restore();

    canvas.save();
    // Y軸の镜像
    canvas.scale(-1, 1);
    _drawGridRight(canvas, size);
    canvas.restore();

    canvas.save();
    // 原点の镜像
    canvas.scale(-1, -1);
    _drawGridRight(canvas, size);
    canvas.restore();
  }
```
描画効果は下記です。
|X軸の鏡像|Y軸の鏡像|原点の鏡像|
| - | - | - |
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/fc629786-3f84-2a8a-8edf-385c15e41b40.png"width=180>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/c6e6373e-0403-7ad8-c9c5-7508f09078de.png"width=180>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/0b6833b5-7100-b9e6-066e-4d586683cc44.png"width=180>|

## 回転する
キャンパスを回転することで、ペンの位置を移動しなくても、円を描くことができます。
```dart
void _drawDot(Canvas canvas, Paint paint) {
    const int count = 12;
    canvas.save();
    for (int i = 0; i < count; i++) {
      var step = 2 * pi / count;
      canvas.drawLine(
        const Offset(80, 0),
        const Offset(100, 0),
        paint,
      );
      // キャンパスを回転する
      canvas.rotate(step);
    }
    canvas.restore();
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/a7f5a395-7989-0297-9714-98994f837203.png"width=280>

# 図形の描画
- 点
drawPoints、drawRawPoints
- 線
drawLine
- 矩形
drawRect、drawRRect、drawDRRect
- 円
drawCircle,drawOval,drawArc

## 点
点のモデルは下記３種類あります。
```dart
enum PointMode {
  points,
  lines,
  polygon,
}
```
```dart
void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    // グリッド
    _drawGrid(canvas, size);
    // 点のPaint
    var paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10
      ..strokeCap = StrokeCap.round;
    // 点を描く
    _drawPointsWithPoints(canvas, paint);
  }

  void _drawPointsWithPoints(Canvas canvas, Paint paint) {
    // 点の集合体
    const List<Offset> points = [
      Offset(-120, -20),
      Offset(-80, -80),
      Offset(40, 40),
      Offset(0, 0),
      Offset(40, -140),
      Offset(80, -160),
      Offset(120, -100),
    ];
    canvas.drawPoints(PointMode.points, points, paint);
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/0a8cd8d7-10d4-03b3-3723-c6d7f5ce7883.png"width=280>
同じ点の集合体でモデルごとの表現は下記通りです。
PointMode.pointsはdrawRawPointsと同じ効果です。
PointMode.linesの場合、集合体が奇数個は最後の１つは無効です。
PointMode.polygonは線を順番で連結し、図形にします。
| points | lines | polygon |
| - | - | - |
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/eb496697-9559-f317-95ac-3a724142fa2a.png"width=180>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/fcf12063-b696-756a-5dc0-30ae181a516e.png"width=180>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/351d258a-d4a7-46d5-c6e0-f801cd27cfe8.png"width=180>|

上記の知識によって、drawPointsだけで折れ線グラフの作成は可能です。
```dart
void _drawPointsWithPoints(Canvas canvas, Paint paint) {
    const List<Offset> points = [
      Offset(-120, -20),
      Offset(-80, -80),
      Offset(-40, -60),
      Offset(0, 0),
      Offset(40, -140),
      Offset(80, 100),
      Offset(120, -100),
    ];
    paint.strokeWidth = 10;
    // 点を描く
    canvas.drawPoints(PointMode.points, points, paint);
    // 線を描く
    paint.strokeWidth = 2;
    canvas.drawPoints(PointMode.polygon, points, paint);
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/1e82aa3c-82bb-4ceb-ba79-10fe7befa037.png"width=280>

## 線
上記の方法以外、線を描く関数も用意されています。
座標のスタートとエンドの宣言し、点と点を繋ぎます。下記のソースから座標のXY軸を描くこと出来ました。
```dart
  void _drawAxis(Canvas canvas, Size size) {
    _gridPint
      ..color = Colors.green
      ..strokeWidth = 1.5;
    canvas.drawLine(Offset(-size.width / 2, 0), Offset(size.width / 2, 0), _gridPint);
    canvas.drawLine(Offset(0, -size.height / 2), Offset(0, size.height / 2), _gridPint);
    canvas.drawLine(Offset(0, size.height / 2), Offset(0 - 7.0, size.height / 2 - 10), _gridPint);
    canvas.drawLine(Offset(0, size.height / 2), Offset(0 + 7.0, size.height / 2 - 10), _gridPint);
    canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2 - 10, 7), _gridPint);
    canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2 - 10, -7), _gridPint);
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/f156ac93-b123-2798-19f6-166ae016f3c3.png"width=280>

## 矩形
- drawRect
矩形を宣言するコンストラクターは５つあります。
```dart
// 中心から矩形を描く
Rect.fromCenter
// 左上右下から矩形を描く
Rect.fromLTRB
// 左上幅高さから矩形を描く
Rect.fromLTWH
// 内接円から矩形を描く
Rect.fromCircle
// ポイントから矩形を描く
Rect.fromPoints
```
例から確認しましょう
```dart
  void _drawRect(Canvas canvas) {
    _gridPint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    //中心から矩形を描く
    Rect rectFromCenter = Rect.fromCenter(
      center: const Offset(0, 0),
      width: 160,
      height: 160,
    );
    canvas.drawRect(rectFromCenter, _gridPint);
    // 左上右下から矩形を描く
    Rect rectFromLTRB = const Rect.fromLTRB(-120, -120, -80, -80);
    canvas.drawRect(rectFromLTRB, _gridPint..color = Colors.red);
    // 左上幅高さから矩形を描く
    Rect rectFromLTWH = const Rect.fromLTWH(80, -120, 40, 40);
    canvas.drawRect(rectFromLTWH, _gridPint..color = Colors.orange);
    // 内接円から矩形を描く
    Rect rectFromCircle = Rect.fromCircle(
      center: const Offset(100, 100),
      radius: 20,
    );
    canvas.drawRect(rectFromCircle, _gridPint..color = Colors.green);
    // ポイントから矩形を描く
    Rect rectFromPoints = Rect.fromPoints(
      const Offset(-120, 80),
      const Offset(-80, 120),
    );
    canvas.drawRect(rectFromPoints, _gridPint..color = Colors.purple);
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/1b6527a8-60a4-dfda-c26a-b7d88a6513c0.png"width=280>
- drawRRect
角丸矩形は同じく宣言するコンストラクターは５つあります。
```dart
// 中心から矩形を描く
RRect.fromRectXY
// 左上右下から矩形を描く
RRect.fromLTRBXY
// 左上幅高さから矩形を描く
RRect.fromLTRBR
// 内接円から矩形を描く
RRect.fromLTRBAndCorners
// ポイントから矩形を描く
RRect.fromRectAndCorners
```
例から確認しましょう,XYは角丸の中心です。
```dart

void _drawRRect(Canvas canvas) {
    _gridPint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    // fromRectXY
    Rect rectFromCenter = Rect.fromCenter(
      center: const Offset(0, 0),
      width: 160,
      height: 160,
    );
    canvas.drawRRect(RRect.fromRectXY(rectFromCenter, 40, 40), _gridPint);

    //　fromLTRBXY
    canvas.drawRRect(
      const RRect.fromLTRBXY(-120, -120, -80, -80, 10, 10),
      _gridPint..color = Colors.red,
    );

    //　fromLTRBR
    canvas.drawRRect(
      RRect.fromLTRBR(80, -120, 120, -80, const Radius.circular(10)),
      _gridPint..color = Colors.orange,
    );

    //　fromLTRBAndCorners
    canvas.drawRRect(
      RRect.fromLTRBAndCorners(
        80,
        80,
        120,
        120,
        topLeft: const Radius.elliptical(10, 10),
        topRight: const Radius.elliptical(10, 10),
        bottomRight: const Radius.elliptical(10, 10),
        bottomLeft: const Radius.elliptical(10, 10),
      ),
      _gridPint..color = Colors.green,
    );

    //　ポイントから矩形を描く
    Rect rectFromPoints = Rect.fromPoints(
      const Offset(-120, 80),
      const Offset(-80, 120),
    );
    canvas.drawRRect(
      RRect.fromRectAndCorners(
        rectFromPoints,
        bottomLeft: const Radius.elliptical(10, 10),
        topLeft: const Radius.elliptical(10, 10),
        topRight: const Radius.elliptical(10, 10),
        bottomRight: const Radius.elliptical(10, 10),
      ),
      _gridPint..color = Colors.purple,
    );
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/77edc191-9181-9d00-b3ce-fcca6042d6de.png"width=280>

- drawDRRect
二つの角丸矩形を描いて、両者の差分領域を描くことができます。利用場面としては写真のフレームを描く時とか。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/2c3cb821-7c7a-c88c-364f-33956a2245be.png"width=280>

outerとinner２つを定義しないといけないですが、制限としてはouterの領域はinnerより大きくしないといけないです。
```dart
void drawDRRect(
  RRect outer,
  RRect inner,
  Paint paint,
)
```
例：
```dart
void _drawDRRect(Canvas canvas) {
    _paint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    Rect outRect = Rect.fromCenter(center: Offset(0, 0), width: 160, height: 160);
    Rect inRect = Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100);
    canvas.drawDRRect(RRect.fromRectXY(outRect, 0, 0), RRect.fromRectXY(inRect, 20, 20), _paint);
  }
```
効果:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/0265d147-291c-2aed-785c-ea63eba6323f.png"width=280>

## 円
- drawCircle(円)
- drawOval（楕円）
- drawArc(アーク)

```dart
void drawCircle(
  Offset c,
  double radius,// 半径
  Paint paint,
)

void drawOval(
 Rect rect,// 楕円の内接矩形
 Paint paint,
)

void drawArc(
  Rect rect,// 内接矩形
  double startAngle,// 開始ラジアン
  double sweepAngle,// 終了ラジアン
  bool useCenter,// 中心に連結するか
  Paint paint,
)
```
下記例で確認しましょう
```dart
  void _drawRound(Canvas canvas) {
    _paint.color = Colors.green;
    canvas.save();
    canvas.translate(-100, 0);
    // 円形
    canvas.drawCircle(const Offset(0, 0), 25, _paint);
    canvas.restore();

    // 楕円
    canvas.save();
    var rect = Rect.fromCenter(
      center: const Offset(0, 0),
      height: 40,
      width: 50,
    );
    canvas.translate(-40, 0);
    canvas.drawOval(rect, _paint);
    canvas.restore();

    // アーク
    canvas.save();
    canvas.translate(40, 0);
    canvas.drawArc(rect, 0, pi / 2 * 3, true, _paint);
    canvas.restore();

    // 中心に連結しない
    canvas.save();
    canvas.translate(100, 0);
    canvas.drawArc(rect, 0, pi / 2 * 3, false, _paint);
    canvas.restore();
  }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491433/76de7fa7-eef0-e186-2a56-df92ccf8bcfe.png"width=280>