Skip to content

Instantly share code, notes, and snippets.

@flar
Created November 17, 2025 20:49
Show Gist options
  • Select an option

  • Save flar/5a29bd340b2cecefccf184ea42d2d805 to your computer and use it in GitHub Desktop.

Select an option

Save flar/5a29bd340b2cecefccf184ea42d2d805 to your computer and use it in GitHub Desktop.
Shadow algorithm test and comparison
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Shadow Blur Size'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class Shape {
Shape({
required this.name,
required this.regularPath,
required this.obscuredPath,
required this.nonSimplePath,
});
String name;
Path regularPath;
Path obscuredPath;
Path nonSimplePath;
}
List<Shape> allShapes = <Shape>[
Shape(
name: 'Triangle',
regularPath: Path()
..moveTo(0, -100)
..lineTo(100, 100)
..lineTo(-100, 100)
..close(),
obscuredPath: Path() // Triangle will not be confused with a primitive shape
..moveTo(0, -100)
..lineTo(100, 100)
..lineTo(-100, 100)
..close(),
nonSimplePath: Path()
..moveTo(0, -100)
..lineTo(100, 100)
..lineTo(-100, 100)
..close()
..lineTo(0, 0),
),
Shape(
name: 'Rectangle',
regularPath: Path()
..addRect(Rect.fromLTRB(-100, -100, 100, 100)),
obscuredPath: Path() // Off by a fraction, but still basically a rect
..moveTo(-99.9, -100)
..lineTo(100, -100)
..lineTo(100, 100)
..lineTo(-100, 100)
..close(),
nonSimplePath: Path()
..addRect(Rect.fromLTRB(-100, -100, 100, 100))
..lineTo(0, 0),
),
Shape(
name: 'Circle',
regularPath: Path()
..addOval(Rect.fromLTRB(-100, -100, 100, 100)),
obscuredPath: Path() // Off by a pixel, but still basically a rect
..moveTo(0, -100)
..conicTo(99.9, -100, 100, 0, sqrt1_2) // Almost, but not quite, a quarter circle
..conicTo(100, 100, 0, 100, sqrt1_2)
..conicTo(-100, 100, -100, 0, sqrt1_2)
..conicTo(-100, -100, 0, -100, sqrt1_2)
..close(),
nonSimplePath: Path()
..addOval(Rect.fromLTRB(-100, -100, 100, 100))
..lineTo(0, 0),
),
Shape(
name: 'Oval',
regularPath: Path()
..addOval(Rect.fromLTRB(-100, -80, 100, 80)),
obscuredPath: Path() // Off by a pixel, but still basically a rect
..moveTo(0, -80)
..conicTo(99.9, -80, 100, 0, sqrt1_2) // Almost, but not quite, a quarter circle
..conicTo(100, 80, 0, 80, sqrt1_2)
..conicTo(-100, 80, -100, 0, sqrt1_2)
..conicTo(-100, -80, 0, -80, sqrt1_2)
..close(),
nonSimplePath: Path()
..addOval(Rect.fromLTRB(-100, -80, 100, 80))
..lineTo(0, 0),
),
Shape(
name: 'RoundRect',
regularPath: Path()
..addRRect(RRect.fromRectXY(Rect.fromLTRB(-100, -100, 100, 100), 20, 20)),
obscuredPath: Path()
..moveTo(80.1, -100) // Upper right non-circular by a pixel
..conicTo(100, -100, 100, -80, sqrt1_2)
..lineTo(100, 80)
..conicTo(100, 100, 80, 100, sqrt1_2)
..lineTo(-80, 100)
..conicTo(-100, 100, -100, 80, sqrt1_2)
..lineTo(-100, -80)
..conicTo(-100, -100, -80, -100, sqrt1_2)
..close(),
nonSimplePath: Path()
..addRRect(RRect.fromRectXY(Rect.fromLTRB(-100, -100, 100, 100), 20, 20))
..lineTo(0, 0),
),
];
enum AlgorithmType {
fast,
general,
backup,
}
class _MyHomePageState extends State<MyHomePage> {
final FocusNode _focusNode = FocusNode();
double _elevation = 5.0;
double _scale = 1.0;
Shape _shape = allShapes[0];
AlgorithmType _algorithmType = AlgorithmType.fast;
bool _drawShape = false;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _changeAlgorithmType(AlgorithmType newType) {
if (_algorithmType != newType) {
setState(() { _algorithmType = newType; });
}
}
KeyEventResult _keyTyped(FocusNode node, KeyEvent event) {
if (event is! KeyDownEvent) {
return KeyEventResult.ignored;
}
switch (event.logicalKey) {
case LogicalKeyboardKey.keyF:
_changeAlgorithmType(.fast);
break;
case LogicalKeyboardKey.keyG:
_changeAlgorithmType(.general);
break;
case LogicalKeyboardKey.keyB:
_changeAlgorithmType(.backup);
break;
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.arrowRight:
case LogicalKeyboardKey.arrowDown:
switch (_algorithmType) {
case .fast:
_changeAlgorithmType(.general);
break;
case .general:
_changeAlgorithmType(.backup);
break;
case .backup:
_changeAlgorithmType(.fast);
break;
}
break;
case LogicalKeyboardKey.backspace:
case LogicalKeyboardKey.arrowLeft:
case LogicalKeyboardKey.arrowUp:
switch (_algorithmType) {
case .fast:
_changeAlgorithmType(.backup);
break;
case .general:
_changeAlgorithmType(.fast);
break;
case .backup:
_changeAlgorithmType(.general);
break;
}
break;
case LogicalKeyboardKey.keyD:
setState(() { _drawShape = !_drawShape; });
break;
default:
return KeyEventResult.ignored;
}
return KeyEventResult.handled;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
backgroundColor: Colors.white,
body: Center(
child: GestureDetector(
onTap: () {
_focusNode.requestFocus();
},
child: Focus(
focusNode: _focusNode,
autofocus: true,
onKeyEvent: _keyTyped,
child: CustomPaint(
size: Size(900, 400),
foregroundPainter: _ShadowPainter(
elevation: _elevation,
scale: _scale,
shape: _shape,
algorithmType: _algorithmType,
drawShape: _drawShape,
),
),
),
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Scale:'),
ExcludeFocus(
child: Slider(value: _scale, min: 0.1, max: 5.0,
onChanged: (v) { setState(() { _scale = v; }); },
),
),
Text('Elevation:'),
ExcludeFocus(
child: Slider(value: _elevation, min: 0.0, max: 30.0,
onChanged: (v) { setState(() { _elevation = v; }); },
),
),
DropdownMenu(
dropdownMenuEntries: <DropdownMenuEntry<AlgorithmType>>[
for (var type in AlgorithmType.values)
DropdownMenuEntry(value: type, label: type.name),
],
initialSelection: _algorithmType,
onSelected: (v) { setState(() {
_algorithmType = v!;
_focusNode.requestFocus();
}); },
),
DropdownMenu(
dropdownMenuEntries: <DropdownMenuEntry<Shape>>[
for (var shape in allShapes)
DropdownMenuEntry(value: shape, label: shape.name),
],
initialSelection: _shape,
onSelected: (v) { setState(() {
_shape = v!;
_focusNode.requestFocus();
}); },
)
],
),
),
);
}
}
class _ShadowPainter extends CustomPainter {
_ShadowPainter({
required this.elevation,
required this.scale,
required this.shape,
required this.algorithmType,
required this.drawShape,
});
final double elevation;
final double scale;
final Shape shape;
final AlgorithmType algorithmType;
final bool drawShape;
Path get path => switch (algorithmType) {
.fast => shape.regularPath,
.general => shape.obscuredPath,
.backup => shape.nonSimplePath,
};
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2.0, size.height / 2.0);
canvas.scale(scale, scale);
canvas.drawShadow(path, Colors.blue, elevation, true);
if (drawShape) {
Paint paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.purple;
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment