Skip to content

Instantly share code, notes, and snippets.

@dumazy
Last active June 6, 2024 19:57
Show Gist options
  • Save dumazy/610b2ad61e19f733cccdf9007d552395 to your computer and use it in GitHub Desktop.
Save dumazy/610b2ad61e19f733cccdf9007d552395 to your computer and use it in GitHub Desktop.
Animated border in a heart shape using PathMetric.getTangentForOffset
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui' as ui;
import 'dart:ui';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: AnimatedBorder(
child: SizedBox(
width: 200,
height: 200,
child: Center(
child: Align(
alignment: Alignment(0, 0.4),
child: FlutterLogo(
size: 74,
),
),
),
),
),
),
),
);
}
}
class AnimatedBorder extends StatefulWidget {
const AnimatedBorder({
super.key,
required this.child,
});
final Widget child;
@override
State<AnimatedBorder> createState() => _AnimatedBorderState();
}
class _AnimatedBorderState extends State<AnimatedBorder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
);
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
_controller.repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: _AnimatedHeartShape(
color: Colors.red,
progress: _animation.value,
),
child: child,
);
},
child: widget.child,
);
}
}
class _AnimatedHeartShape extends CustomPainter {
_AnimatedHeartShape({
required this.progress,
required this.color,
});
final double progress;
final Color color;
@override
void paint(Canvas canvas, Size size) {
final path = createPath(size);
final pathMetric = path.computeMetrics().first;
final center = Offset(size.width / 2, size.height / 2);
final currentPoint = getOffsetOnPath(pathMetric, progress);
final startAngle = calculateAngleFromCenter(center, currentPoint);
final endAngle = startAngle + (2 * pi);
final paint = Paint()
..shader = ui.Gradient.sweep(
center,
[
color,
color.withOpacity(0),
],
null,
TileMode.repeated,
startAngle,
endAngle,
)
..strokeWidth = 10.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _AnimatedHeartShape oldDelegate) =>
oldDelegate.progress != progress;
Path createPath(Size size) {
final width = size.width;
final height = size.height;
final path = Path();
// center top
path.moveTo(width / 2, height * 0.35);
// left to bottom
path.cubicTo(
0.2 * width,
height * 0.1,
-0.25 * width,
height * 0.6,
width / 2,
height,
);
// right to top
path.cubicTo(
1.25 * width,
height * 0.6,
0.8 * width,
height * 0.1,
0.5 * width,
height * 0.35,
);
return path;
}
Offset getOffsetOnPath(PathMetric pathMetric, double progress) {
double totalLength = pathMetric.length;
final distance = totalLength * progress;
Tangent? tangent = pathMetric.getTangentForOffset(distance);
return tangent!.position;
}
double calculateAngleFromCenter(Offset center, Offset point) {
double dx = point.dx - center.dx;
double dy = point.dy - center.dy;
return atan2(dy, dx);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment