13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kurogoma939のひとりアドベントカレンダーAdvent Calendar 2024

Day 16

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

Last updated at Posted at 2024-12-15

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

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

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

方程式

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

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

\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乗根を打ち消す

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

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

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

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

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

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で積み上げます。

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

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

13
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?