Skip to content

Instantly share code, notes, and snippets.

@SEGVeenstra
Created September 21, 2024 09:59
Show Gist options
  • Select an option

  • Save SEGVeenstra/dda3861ae51964afea939475372cb605 to your computer and use it in GitHub Desktop.

Select an option

Save SEGVeenstra/dda3861ae51964afea939475372cb605 to your computer and use it in GitHub Desktop.
SciFiContainer
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:collection/collection.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: SciFiContainer(
// This color will be used for the border
foregroundColor: Colors.cyan,
// This color will be used for the background
backgroundColor: Colors.cyan.withOpacity(.2),
// You can specify the 'size' of the corners.
// The corners are cut in 45 degree angles
corners: SciFiContainerCorners.only(
bottomRight: 32.0,
topLeft: 16.0,
),
// You can add borders to your container to make it look more SciFi-like
borders: SciFiContainerBorders.only(
top: SciFiContainerBorderIndent(
start: .2, // Where the indent should start 0.0 - 1.0
end: .7, // Where the indent should end 0.0 - 1.0
depth: 16, // DHow deep the indent goes inside the container
),
bottom: SciFiContainerBorderIndent(
start: .15,
end: .75,
depth: -16, // Negative numbers will make it an outdent
),
),
// You can add an onTap function
onTap: () {},
// This container will show the automatically calculated padding
// SciFiContainer will add padding to make sure your child fits
// inside the container. (not very optimised)
// You can override this behavior by setting `SciFiContainer.padding`
child: Container(
width: 300,
height: 200,
color: Colors.green.withOpacity(.1),
),
),
),
),
);
}
}
class SciFiContainer extends StatelessWidget {
const SciFiContainer({
super.key,
required this.child,
this.corners = const SciFiContainerCorners.zero(),
this.borders = const SciFiContainerBorders.none(),
required this.foregroundColor,
required this.backgroundColor,
this.padding,
this.onTap,
});
final Widget child;
final Color foregroundColor;
final Color backgroundColor;
final SciFiContainerCorners corners;
final SciFiContainerBorders borders;
final EdgeInsets? padding;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final topPadding = [
corners.topLeft / 2 - borders.top.offset.clampNegative(),
corners.topRight / 2 - borders.top.offset.clampNegative(),
borders.top.offset,
].max;
final rightPadding = <double>[
corners.topRight / 2 - borders.right.offset.clampNegative(),
corners.bottomRight / 2 - borders.right.offset.clampNegative(),
borders.right.offset,
].max;
final bottomPadding = [
corners.bottomLeft / 2 - borders.bottom.offset.clampNegative(),
corners.bottomRight / 2 - borders.bottom.offset.clampNegative(),
borders.bottom.offset,
].max;
final leftPadding = [
corners.topLeft / 2 - borders.left.offset.clampNegative(),
corners.bottomLeft / 2 - borders.left.offset.clampNegative(),
borders.left.offset,
].max;
final padding = this.padding ??
EdgeInsets.only(
left: leftPadding,
top: topPadding,
bottom: bottomPadding,
right: rightPadding,
);
return ClipPath(
clipBehavior: Clip.hardEdge,
clipper: _SciFiClipper(
borders: borders,
corners: corners,
),
child: Material(
color: backgroundColor,
child: InkWell(
onTap: onTap,
child: CustomPaint(
foregroundPainter: SciFiForegroundPainter(
strokeColor: foregroundColor,
corners: corners,
borders: borders,
),
child: Padding(
padding: padding,
child: DefaultTextStyle(
style: TextStyle(
color: foregroundColor,
),
child: child,
),
),
),
),
),
);
}
}
class SciFiForegroundPainter extends CustomPainter {
SciFiForegroundPainter({
required this.strokeColor,
required this.corners,
required this.borders,
});
final Color strokeColor;
final SciFiContainerCorners corners;
final SciFiContainerBorders borders;
@override
void paint(Canvas canvas, Size size) {
final strokePaint = Paint()
..color = strokeColor
..strokeWidth = 2
..style = PaintingStyle.stroke;
final path = _buildPath(size, corners, borders);
canvas.drawPath(path, strokePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class SciFiContainerCorners {
const SciFiContainerCorners.only({
this.bottomLeft = 0,
this.bottomRight = 0,
this.topLeft = 0,
this.topRight = 0,
});
const SciFiContainerCorners.zero()
: topLeft = 0,
topRight = 0,
bottomLeft = 0,
bottomRight = 0;
final double topLeft;
final double topRight;
final double bottomLeft;
final double bottomRight;
}
class SciFiContainerBorders {
const SciFiContainerBorders.none()
: top = const SciFiContainerBorderNone(),
right = const SciFiContainerBorderNone(),
bottom = const SciFiContainerBorderNone(),
left = const SciFiContainerBorderNone();
const SciFiContainerBorders.only({
this.top = const SciFiContainerBorderNone(),
this.right = const SciFiContainerBorderNone(),
this.bottom = const SciFiContainerBorderNone(),
this.left = const SciFiContainerBorderNone(),
});
final SciFiContainerBorderDecoration top;
final SciFiContainerBorderDecoration bottom;
final SciFiContainerBorderDecoration left;
final SciFiContainerBorderDecoration right;
}
sealed class SciFiContainerBorderDecoration {
const SciFiContainerBorderDecoration();
final double offset = 0;
List<Point<double>> calculateTopBorder(
Point<double> start,
Point<double> end,
) =>
[];
List<Point<double>> calculateRightBorder(
Point<double> start,
Point<double> end,
) =>
[];
List<Point<double>> calculateBottomBorder(
Point<double> start,
Point<double> end,
) =>
[];
List<Point<double>> calculateLeftBorder(
Point<double> start,
Point<double> end,
) =>
[];
}
class SciFiContainerBorderIndent extends SciFiContainerBorderDecoration {
const SciFiContainerBorderIndent({
required this.start,
required this.end,
required this.depth,
});
final double start;
final double end;
final double depth;
@override
double get offset => depth;
@override
List<Point<double>> calculateTopBorder(
Point<double> start,
Point<double> end,
) {
final borderWidth = end.x - start.x;
final borderPoint1 = Point(
start.x + (borderWidth * this.start),
start.y,
);
final borderPoint2 = Point(
start.x + (borderWidth * this.start) + depth.abs(),
start.y + depth,
);
final borderPoint3 = Point(
start.x + (borderWidth * this.end) - depth.abs(),
start.y + depth,
);
final borderPoint4 = Point(
start.x + (borderWidth * this.end),
start.y,
);
return [
borderPoint1,
borderPoint2,
borderPoint3,
borderPoint4,
];
}
@override
List<Point<double>> calculateRightBorder(
Point<double> start, Point<double> end) {
final borderLength = end.y - start.y;
final borderPoint1 = Point(
start.x,
start.y + (borderLength * this.start),
);
final borderPoint2 = Point(
start.x - depth,
start.y + (borderLength * this.start) + depth.abs(),
);
final borderPoint3 = Point(
start.x - depth,
start.y + (borderLength * this.end) - depth.abs(),
);
final borderPoint4 = Point(
start.x,
start.y + (borderLength * this.end),
);
return [
borderPoint1,
borderPoint2,
borderPoint3,
borderPoint4,
];
}
@override
List<Point<double>> calculateBottomBorder(
Point<double> start,
Point<double> end,
) {
final borderLength = start.x - end.x;
final borderPoint1 = Point(
start.x - (borderLength * this.start),
start.y,
);
final borderPoint2 = Point(
start.x - (borderLength * this.start) - depth.abs(),
start.y - depth,
);
final borderPoint3 = Point(
start.x - (borderLength * this.end) + depth.abs(),
start.y - depth,
);
final borderPoint4 = Point(
start.x - (borderLength * this.end),
start.y,
);
return [
borderPoint1,
borderPoint2,
borderPoint3,
borderPoint4,
];
}
@override
List<Point<double>> calculateLeftBorder(
Point<double> start, Point<double> end) {
final borderLength = end.y - start.y;
final borderPoint1 = Point(
start.x,
start.y + (borderLength * this.start),
);
final borderPoint2 = Point(
start.x + depth,
start.y + (borderLength * this.start) - depth.abs(),
);
final borderPoint3 = Point(
start.x + depth,
start.y + (borderLength * this.end) + depth.abs(),
);
final borderPoint4 = Point(
start.x,
start.y + (borderLength * this.end),
);
return [
borderPoint1,
borderPoint2,
borderPoint3,
borderPoint4,
];
}
}
class SciFiContainerBorderNone extends SciFiContainerBorderDecoration {
const SciFiContainerBorderNone();
}
class _SciFiClipper extends CustomClipper<Path> {
_SciFiClipper({
required this.corners,
required this.borders,
});
final SciFiContainerCorners corners;
final SciFiContainerBorders borders;
@override
Path getClip(Size size) {
return _buildPath(size, corners, borders);
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
}
Path _buildPath(
Size size,
SciFiContainerCorners corners,
SciFiContainerBorders borders,
) {
// Define the points for the base shape
final p1 =
Point<double>(0, corners.topLeft - borders.top.offset.clampNegative());
final p2 =
Point<double>(corners.topLeft, 0 - borders.top.offset.clampNegative());
final p3 = Point<double>(
size.width - corners.topRight + borders.right.offset.clampNegative(),
0 - borders.top.offset.clampNegative());
final p4 = Point<double>(size.width + borders.right.offset.clampNegative(),
corners.topRight - borders.top.offset.clampNegative());
final p5 = Point<double>(
size.width + borders.right.offset.clampNegative(),
size.height - corners.bottomRight + borders.bottom.offset.clampNegative(),
);
final p6 = Point<double>(
size.width - corners.bottomRight + borders.right.offset.clampNegative(),
size.height + borders.bottom.offset.clampNegative(),
);
final p7 = Point<double>(
corners.bottomLeft,
size.height + borders.bottom.offset.clampNegative(),
);
final p8 = Point<double>(
0,
size.height - corners.bottomLeft + borders.bottom.offset.clampNegative(),
);
// Draw the path based on the points
final path = Path();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
for (final position in borders.top.calculateTopBorder(p2, p3)) {
path.lineTo(position.x, position.y);
}
path.lineTo(p3.x, p3.y);
path.lineTo(p4.x, p4.y);
for (final position in borders.right.calculateRightBorder(p4, p5)) {
path.lineTo(position.x, position.y);
}
path.lineTo(p5.x, p5.y);
path.lineTo(p6.x, p6.y);
for (final position in borders.bottom.calculateBottomBorder(p6, p7)) {
path.lineTo(position.x, position.y);
}
path.lineTo(p7.x, p7.y);
path.lineTo(p8.x, p8.y);
for (final position in borders.left.calculateLeftBorder(p8, p1)) {
path.lineTo(position.x, position.y);
}
path.lineTo(p1.x, p1.y);
return path;
}
extension DoubleExt on double {
double clampNegative() {
if (this > 0) {
return 0;
} else {
return this;
}
}
double clampPositive() {
if (this < 0) {
return 0;
} else {
return this;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment