import 'dart:math'; import 'package:aves/model/entry/entry.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/painting.dart'; extension ExtraWidgetShape on WidgetShape { static const double _defaultCornerRadius = 24; Path path(Size widgetSize, double devicePixelRatio, {double? cornerRadiusPx}) { final rect = Offset.zero & widgetSize; switch (this) { case WidgetShape.bumpyColumns: return _buildBumpyColumnsPath(rect); case WidgetShape.bumpyRows: return _buildBumpyRowsPath(rect); case WidgetShape.circle: return Path()..addOval( Rect.fromCircle( center: rect.center, radius: rect.shortestSide / 2, ), ); case WidgetShape.concaveSquare: return _buildConcaveSquarePath(rect); case WidgetShape.heart: return _buildHeartPath(rect); case WidgetShape.rrect: return Path()..addRRect(BorderRadius.circular(cornerRadiusPx ?? (_defaultCornerRadius * devicePixelRatio)).toRRect(rect)); case WidgetShape.tearRectLeft: final radius = cornerRadiusPx ?? (_defaultCornerRadius * devicePixelRatio); return _buildTearRectPath(rect, topLeftRadiusPx: radius, topRightRadiusPx: radius * 2); case WidgetShape.tearRectRight: final radius = cornerRadiusPx ?? (_defaultCornerRadius * devicePixelRatio); return _buildTearRectPath(rect, topLeftRadiusPx: radius * 2, topRightRadiusPx: radius); case WidgetShape.wavyCircle16: return _buildWavyCirclePath(rect, 16, .5); } } Path _buildBumpyColumnsPath(Rect rect) { final radius = rect.width / 4; final topY = radius; final bottomY = rect.height - radius; const angleUnit = pi / 6; return Path() ..moveTo(0, topY) ..arcTo(Rect.fromCircle(center: Offset(radius, topY), radius: radius), -6 * angleUnit, 4 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(radius * 2, topY), radius: radius), -4 * angleUnit, 2 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(radius * 3, topY), radius: radius), -4 * angleUnit, 4 * angleUnit, false) ..lineTo(rect.width, bottomY) ..arcTo(Rect.fromCircle(center: Offset(radius * 3, bottomY), radius: radius), 0, 4 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(radius * 2, bottomY), radius: radius), 2 * angleUnit, 2 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(radius, bottomY), radius: radius), 2 * angleUnit, 4 * angleUnit, false) ..lineTo(0, topY); } Path _buildBumpyRowsPath(Rect rect) { final radius = rect.height / 4; final leftX = radius; final rightX = rect.width - radius; const angleUnit = pi / 6; return Path() ..moveTo(leftX, 0) ..lineTo(rightX, 0) ..arcTo(Rect.fromCircle(center: Offset(rightX, radius), radius: radius), -3 * angleUnit, 4 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(rightX, radius * 2), radius: radius), -angleUnit, 2 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(rightX, radius * 3), radius: radius), -angleUnit, 4 * angleUnit, false) ..lineTo(leftX, rect.height) ..arcTo(Rect.fromCircle(center: Offset(leftX, radius * 3), radius: radius), 3 * angleUnit, 4 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(leftX, radius * 2), radius: radius), 5 * angleUnit, 2 * angleUnit, false) ..arcTo(Rect.fromCircle(center: Offset(leftX, radius), radius: radius), 5 * angleUnit, 4 * angleUnit, false); } Path _buildConcaveSquarePath(Rect rect) { final center = rect.center; final dim = rect.shortestSide; final radius = dim / 4; final tl = center + Offset(-radius, -radius); final tr = center + Offset(radius, -radius); final br = center + Offset(radius, radius); final bl = center + Offset(-radius, radius); final outsideDim = radius * (1 + sqrt(3)); final left = center + Offset(-outsideDim, 0); final top = center + Offset(0, -outsideDim); final right = center + Offset(outsideDim, 0); final bottom = center + Offset(0, outsideDim); final tlTop = (tl + top) / 2; final topTr = (top + tr) / 2; final trRight = (tr + right) / 2; final rightBr = (right + br) / 2; final brBottom = (br + bottom) / 2; final bottomBl = (bottom + bl) / 2; final blLeft = (bl + left) / 2; final leftTl = (left + tl) / 2; final r = Radius.circular(radius); return Path() ..moveTo(tlTop.dx, tlTop.dy) ..arcToPoint(topTr, radius: r, clockwise: false) ..arcToPoint(trRight, radius: r) ..arcToPoint(rightBr, radius: r, clockwise: false) ..arcToPoint(brBottom, radius: r) ..arcToPoint(bottomBl, radius: r, clockwise: false) ..arcToPoint(blLeft, radius: r) ..arcToPoint(leftTl, radius: r, clockwise: false) ..arcToPoint(tlTop, radius: r); } Path _buildHeartPath(Rect rect) { final center = rect.center; final dim = rect.shortestSide; const p0dy = -.4; const p1dx = .5; const p1dy = -.4; const p2dx = .8; const p2dy = .5; const p3dy = .5 - p0dy; return Path() ..moveTo(center.dx, center.dy) ..relativeMoveTo(0, dim * p0dy) ..relativeCubicTo(dim * -p1dx, dim * p1dy, dim * -p2dx, dim * p2dy, 0, dim * p3dy) ..moveTo(center.dx, center.dy) ..relativeMoveTo(0, dim * p0dy) ..relativeCubicTo(dim * p1dx, dim * p1dy, dim * p2dx, dim * p2dy, 0, dim * p3dy); } Path _buildTearRectPath(Rect rect, {required double topLeftRadiusPx, required double topRightRadiusPx}) { final topLeftRadius = Radius.circular(topLeftRadiusPx); final topRightRadius = Radius.circular(topRightRadiusPx); return Path()..addRRect( BorderRadius.only( topLeft: topLeftRadius, topRight: topRightRadius, bottomLeft: topRightRadius, bottomRight: topLeftRadius, ).toRRect(rect), ); } Path _buildWavyCirclePath(Rect rect, int bumpCount, double amplitudeFactor, {double angleOffset = 0}) { final center = rect.center; final dim = rect.shortestSide; final waveAmplitude = amplitudeFactor / bumpCount; final circleRadius = (dim / 2) * (1 - waveAmplitude); final pointCount = (dim * dim).round(); final angleIncrement = 2 * pi / pointCount; final points = List.generate(pointCount, (i) { final t = angleIncrement * i; final r = cos((t + angleOffset) * bumpCount) * waveAmplitude + 1; final dx = r * cos(t) * circleRadius; final dy = r * sin(t) * circleRadius; return Offset(center.dx + dx, center.dy + dy); }); final origin = points.first; final path = Path()..moveTo(origin.dx, origin.dy); for (var i = 0; i <= pointCount; i++) { final p = points[i % pointCount]; path.lineTo(p.dx, p.dy); } return path; } double extentPx(Size widgetSizePx, AvesEntry entry) { switch (this) { case WidgetShape.bumpyColumns: case WidgetShape.bumpyRows: case WidgetShape.rrect: case WidgetShape.tearRectLeft: case WidgetShape.tearRectRight: final entryRatio = entry.displayAspectRatio; final widgetRatio = widgetSizePx.width / widgetSizePx.height; if (entryRatio > 1) { // landscape entry, must return thumbnail height as extent if (widgetRatio > entryRatio) { return widgetSizePx.width / entryRatio; } else { return widgetSizePx.height; } } else { // portrait entry, must return thumbnail width as extent if (widgetRatio > entryRatio) { return widgetSizePx.width; } else { return widgetSizePx.height * entryRatio; } } case WidgetShape.circle: case WidgetShape.heart: case WidgetShape.wavyCircle16: case WidgetShape.concaveSquare: return widgetSizePx.shortestSide; } } }