Skip to content

Instantly share code, notes, and snippets.

@Kurogoma4D
Last active May 15, 2021 09:16
Show Gist options
  • Save Kurogoma4D/62cf41e3bd9eec99e881131f864eab3d to your computer and use it in GitHub Desktop.
Save Kurogoma4D/62cf41e3bd9eec99e881131f864eab3d to your computer and use it in GitHub Desktop.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MouseEffect extends StatelessWidget {
const MouseEffect({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _BackGround(),
),
),
Positioned.fill(
child: _EffectParent(),
),
],
);
}
}
class _EffectParent extends StatefulWidget {
const _EffectParent({Key? key}) : super(key: key);
@override
__EffectParentState createState() => __EffectParentState();
}
class __EffectParentState extends State<_EffectParent>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation? animation;
Offset position = Offset.zero;
@override
void initState() {
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 350));
animation = CurvedAnimation(parent: controller!, curve: Curves.easeOut);
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapUp: (detail) {
position = detail.globalPosition;
controller?.forward(from: 0.0);
},
behavior: HitTestBehavior.opaque,
child: AnimatedBuilder(
animation: animation!,
builder: (context, _) {
return ClipPath(
clipper: _EffectClipper(position, animation!.value),
child: CustomPaint(
painter: _Effect(position, animation!.value),
),
);
},
),
);
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
class _BackGround extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final background = Paint()
..shader = ui.Gradient.linear(
size.topCenter(Offset.zero),
size.bottomCenter(Offset.zero),
[Color(0xccab89b5), Color(0xcc5050f5), Color(0xcc32ecdc)],
[0.0, 0.35, 1.0],
);
canvas.drawRect(
Rect.fromLTWH(0.0, 0.0, size.width, size.height), background);
}
@override
bool shouldRepaint(covariant _BackGround oldDelegate) => false;
}
/// クリックしたときの座標に円を描画するためのCustomPainter。
///
/// [value] はアニメーションの値を受け取る想定(0~1)で、この値に比例して円のサイズが変化する。
class _Effect extends CustomPainter {
final Offset position;
final double value;
_Effect(this.position, this.value);
@override
void paint(Canvas canvas, Size size) {
final base = Paint()..color = Colors.white;
final radius = 30.0;
if (position != Offset.zero)
canvas.drawCircle(position, radius * value, base);
}
@override
bool shouldRepaint(covariant _Effect oldDelegate) =>
value != oldDelegate.value;
}
/// クリックしたときに描画される円をクリッピングするためのCustomClipper。
///
/// 円の最大サイズ(半径30dp)の円を追加し、少しディレイを追加したアニメーションの値を半径とした円を追加している。
/// `fillType` を [PathFillType.evenOdd] にすることで、追加した2つの円の内側のみを描画するようになっている。
///
/// ディレイの計算式は時間tの関数 `F(t) = (t - 0.4) * t_max / 0.6 (0 ≦ t ≦ 1)` で、単純に一次関数を右方向に平行移動した上で
/// `t = 1.0` のときに `F(t) = 1.0` となるように傾きを調整したものである。
/// see: https://gist.github.com/Kurogoma4D/62cf41e3bd9eec99e881131f864eab3d#gistcomment-3743539
class _EffectClipper extends CustomClipper<Path> {
final double value;
final Offset position;
_EffectClipper(this.position, this.value);
@override
ui.Path getClip(ui.Size size) => Path()
..addOval(Rect.fromCircle(center: position, radius: 30))
..addOval(Rect.fromCircle(center: position, radius: _delayedRadius(value) * 30))
..fillType = PathFillType.evenOdd;
@override
bool shouldReclip(covariant _EffectClipper oldClipper) =>
this.value != oldClipper.value;
double _delayedRadius(double t) => ((t - 0.4) * 1.0 / 0.6).clamp(0.0, 1.0);
}
void main() => runApp(
MaterialApp(
home: App(),
),
);
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: MouseEffect(),
);
}
}
@Kurogoma4D
Copy link
Author

graph

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment