【Flutter】ジェスチャーで図形を回転させてみた

# はじめに
ペイントツールを作るために、良さそうなパッケージをいくつか使ってみて、
図形の回転はどんな実装をしているのか気になったので、実装してみました

![spin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/7f369051-104f-4b7f-18aa-5f206aee2378.gif)


```dart
class MyPainter extends CustomPainter {
  const MyPainter({
    required this.angle,
  });
  final double angle;

  @override
  void paint(Canvas canvas, Size size) {
    final defaultPaint = Paint()
      ..strokeWidth = 2
      ..color = const Color(0xFF000000)
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    // Flutterのキャンバスは、左上が(0, 0)の原点となる
    // プラスのx座標は右方向、プラスのy座標は下方向への移動となる
    // 100x100のRectを作成（左上の座標が(100, 100)となる)
    final rect = Rect.fromLTRB(100, 100, 200, 200);
    
    // 現在の状態を保存
    canvas.save(); 

    // Rectの中心を原点とする
    const double centerX = 150; // (100 + 200) / 2
    const double centerY = 150; // (100 + 200) / 2
    canvas.translate(centerX, centerY);

    // キャンバスを回転する
    canvas.rotate(angle);

    // 描画位置に影響するため、原点を戻す
    canvas.translate(-centerX, -centerY);

    // 四角形を描画
    canvas.drawRect(const Rect.fromLTRB(100, 100, 200, 200), defaultPaint);

    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

class MyPainterPage extends StatelessWidget {
  MyPainterPage({super.key});
  final angle = ValueNotifier<double>(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: ColoredBox(
            color: Colors.orange,
            child: GestureDetector(
              onScaleUpdate: (details) {
                angle.value = details.rotation;
              },
              child: ValueListenableBuilder(
                valueListenable: angle,
                builder: (context, angle, _) {
                  return CustomPaint(
                    size: const Size(double.infinity, double.infinity),
                    painter: MyPainter(angle: angle),
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}
```

# ポイント解説
## Flutterのペイントの原点について
Flutterのペイントの原点は、デフォルトは左上(0, 0)となっている
プラスのx座標は右方向、プラスのy座標は下方向への移動となる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/d00b62d7-dd64-cadd-d01c-89cdffec1813.png)

つまり、以下のように(10, 10)の座標に点を描画すると、左上から右に10、下に10移動した座標に点が描画される
```dart
canvas.drawPoints(PointMode.points, [const Offset(10, 10)], paint);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/d262c06a-4735-bcbd-1680-4e47b8aca369.png)


## Rect.fromLTRBについて

下記のように実装すると、fromLTRBの引数は、下記に相当する。
L: 左の辺のx座標
T: 上の辺のy座標
R: 右の辺のx座標
B: 下の辺のy座標

```dart
final rect = Rect.fromLTRB(100, 100, 200, 200);
canvas.drawRect(rect, defaultPaint);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/269a6ea8-9462-4e5e-3441-d1e4781400e5.png)

## キャンバスの原点の移動について
`canvas.translate(x, y);`で原点を移動できる。
Flutterのペイントの描画は原点を基準に計算されるので、原点を移動すると描画位置が変わる。

下記のコードの場合、(10+10, 10+10)のポイントに点が描画される
```dart
canvas.translate(10, 10);
canvas.drawPoints(PointMode.points, [const Offset(10, 10)], paint);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/ab186a21-5871-1897-5016-aa9f2052a569.png)

:::note info
キャンパスの原点の移動は、以下の場合に使用することが多いと思います。
- 描画する座標を単純にする時
```dart
final rectWidth = 100;
final rectHeight = 100;
final posX = 100;
final posY = 100;
// final rect = Rect.fromLTRB(posX, posY, posX + rectWidth, posY + rectHeight);
// canvas.drawRect(rect, defaultPaint);
canvas.translate(posX, posY);
final rect = Rect.fromLTRB(0, 0, rectWidth, rectHeight);
```
- 図形の回転の中心を変更する時
次の章で解説します
:::

## キャンバスの回転について
`canvas.rotate(angle)`でキャンバスを回転できます。
キャンバスを回転させて、図形を描画することで、キャンバスを回転した分、図形も回転します。
例えば、下記のようにキャンバスを斜めにして、まっすぐな線を引くことで、斜めの線が引けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/54113875-ed49-7159-8b30-e1e96e2757b9.png)

:::note info
ChatGPTに聞いたところ、他にも図形を回転させる方法がありました。
① 座標計算（回転行列）	を使う
② drawVertices() を使う
③ Matrix4 (Transform) を使う

キャンバスの回転の使い所としては、複数の図形をすべて回転させるケースと、描画する座標を単純にするケースで使用するみたいです。
:::

## キャンバスの状態と復元について
`canvas.save()`でキャンバスの回転状態と原点の位置が保存され、`canvas.restore()`でキャンバスの回転状態と原点の位置が一つ前の状態に戻ります。
サンプルコードだと下記の意味になります。

```dart
    // 原点(0, 0) 回転なしのデフォルトの状態を保存
    canvas.save(); 

    // 省略
    // 四角形描画
    
    // 原点(0, 0) 回転なしのデフォルトの状態に復元
    canvas.restore();
```