デザイナーの角丸へのこだわりに応えよう【Flutter編】

https://qiita.com/kurogoma939/items/fab2ec2e6c39b819b996

こちらの記事の続編です。Flutterバージョンになります。

内容が重複してしまうのでかいつまんで記載すると、通常borderRadiusしか設定していないと思いますが、デザイナーの中には「スーパー楕円」という四角だけどより滑らかな角丸を持つ図形をデザインの中に組み込んでいる場合があるようです。

https://spinners.work/posts/kudakurage-superellipse-desgin/

しかし、綺麗な図形は基本的に方程式で定義できます。
今回のスーパー楕円についても以下のように方程式があります。


#### 方程式

```math
\left|\frac{x}{a}\right|^n + \left|\frac{y}{b}\right|^n = 1
```

(1) yを求めるため移項する

```math
\left|\frac{x}{a}\right|^n + \left|\frac{y}{b}\right|^n = 1 \implies \left|\frac{y}{b}\right|^n = 1 - \left|\frac{x}{a}\right|^n
```

(2) n乗根を打ち消す

```math
\left|\frac{y}{b}\right| = \left(1 - \left|\frac{x}{a}\right|^n\right)^{\frac{1}{n}}
```

(3) bをかけてyの式が出せる

```math
|y| = b\left(1 - \left|\frac{x}{a}\right|^n\right)^{1/n}
```

これを参考にFlutterで実装します。

やはり複雑なウィジェットにはなるため、`CustomPainter`で実装をします。

```dart
import 'dart:math';

import 'package:flutter/material.dart';

class SuperEllipsePainter extends CustomPainter {
  final double a; // x方向スケール
  final double b; // y方向スケール
  final double n; // 楕円形状パラメータ
  final double step; // サンプリング刻み幅
  final Color fillColor; // 塗りつぶし色

  SuperEllipsePainter({
    required this.a,
    required this.b,
    required this.n,
    this.step = 1,
    this.fillColor = const Color.fromARGB(255, 48, 48, 48),
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.fill
      ..color = fillColor;

    final centerX = size.width / 2;
    final centerY = size.height / 2;

    final path = Path();

    // 上半分のパスを描画
    bool firstPoint = true;
    for (double x = -a; x <= a; x += step) {
      final ratio = (x / a).abs();
      final yPart = 1 - pow(ratio, n);
      if (yPart < 0) continue;

      final yVal = b * pow(yPart, 1 / n);
      final drawX = centerX + x;
      final drawY = centerY - yVal;

      if (firstPoint) {
        path.moveTo(drawX, drawY);
        firstPoint = false;
      } else {
        path.lineTo(drawX, drawY);
      }
    }

    // 下半分のパスを描画
    for (double x = a; x >= -a; x -= step) {
      final ratio = (x / a).abs();
      final yPart = 1 - pow(ratio, n);
      if (yPart < 0) continue;

      final yVal = b * pow(yPart, 1 / n);
      final drawX = centerX + x;
      final drawY = centerY + yVal;
      path.lineTo(drawX, drawY);
    }

    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant SuperEllipsePainter oldDelegate) {
    return oldDelegate.a != a ||
        oldDelegate.b != b ||
        oldDelegate.n != n ||
        oldDelegate.step != step ||
        oldDelegate.fillColor != fillColor;
  }
}

class SuperEllipseWidget extends StatelessWidget {
  final double width;
  final double height;
  final double n;
  final double a;
  final double b;

  /// width, height, nを外部から受け取り、
  /// a, bは省略可能。指定しない場合は最小辺を基に自動で設定。
  SuperEllipseWidget({
    super.key,
    required this.width,
    required this.height,
    required this.n,
    double? a,
    double? b,
    Color fillColor = const Color.fromARGB(255, 48, 48, 48),
  })  : a = a ?? (min(width, height) / 2),
        b = b ?? (min(width, height) / 2);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(width, height),
      painter: SuperEllipsePainter(
        a: a,
        b: b,
        n: n,
      ),
    );
  }
}
```

こちらを用いて、以下のようにColumnで積み上げます。

```dart
SizedBox.expand(
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        SuperEllipseWidget(
          width: 100,
          height: 100,
          n: 4.5,
        ),
        const SizedBox(height: 16),
        SuperEllipseWidget(
          width: 100,
          height: 100,
          n: 3,
        ),
        const SizedBox(height: 16),
        SuperEllipseWidget(
          width: 100,
          height: 100,
          n: 2.5,
        ),
      ],
    ),
  ),
),
```

すると以下のようになります。

| 完成した状態|
|:-:|
| ![Simulator Screenshot - iPhone 16 - 2024-12-15 at 00.42.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/735195/3400c7c4-791c-de42-9f23-6d4607ebe202.png)  |


以上です！
もしデザインで登場した場合は参考になればと思います。