Created
November 17, 2025 20:49
-
-
Save flar/5a29bd340b2cecefccf184ea42d2d805 to your computer and use it in GitHub Desktop.
Shadow algorithm test and comparison
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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