こちらの記事の続編です。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,
),
],
),
),
),
すると以下のようになります。
完成した状態 |
---|
以上です!
もしデザインで登場した場合は参考になればと思います。