先上图,用粒子实现一个简单的龙卷风效果:
只记录思路和用到的技术点
核心思路
任何特效都是一步步调试出来的:
- 定义粒子,模拟时间流逝
- 定义多个粒子,让粒子动起来,盘旋上升
- 弯曲的龙卷风更生动,用贝塞尔曲线当做龙卷风的轴
- 会动的龙卷风最形象,让贝塞尔曲线随时间扭动,并让龙卷风左右前后旋转
技术点
- 使用AnimationController ,然后repeat(),模拟时间流逝。注意有个addListener方法,是每一帧的回调。在每一帧的回调里,处理粒子运动轨迹。
- 模拟时间流逝,在创建AnimationController时,可以定义duration,那么通过animation.value可以获取到一个渐变值,我一开始在模拟轨迹过程中使用到了这个值,结果是个坑。记住不要用这个值。而是定义一个变量,然后通过tick()方法时来进行轨迹演变。要记得,每一帧都有回调,也就是一秒钟差不多60次回调。转变思维。
- 粒子围绕path旋转时,使用computeMetrics()方法,获取path过程,然后通过getTangentForOffset(),该路径上该点的信息,包括角度和position,角度是该点切线与x轴正方向之间的夹角。记得canvas可以rotate,避免非xy轴时的复杂计算。
- 通过控制贝塞尔曲线的控制点让曲线扭动,进而让龙卷风扭动
代码
Particle 定义粒子类
Particle.dart
import 'dart:ui';
class Particle {
double x;
double y;
double z;
double vx;
double vy;
double vz;
double ax;
double ay;
double az;
double radius; // 粒子旋转半径
double angle; // path倾斜
double rotate; // 自身旋转
double initRotate; // 初始旋转角度
double cur;// 当前路径点举例
double curStep;// path.length的递增值,粒子旋转时 是沿着path前进的
Offset center;// 粒子沿着path旋转时每个step的中心点
Particle(
{this.x = 0,
this.y = 0,
this.z = 0,
this.vx = 0,
this.vy = 0,
this.vz = 0,
this.ax = 0,
this.ay = 0,
this.az = 0,
this.radius = 1,
this.angle = 0,
this.rotate = 0,
this.initRotate =0,
this.cur = 0,
this.center = Offset.zero,
this.curStep = 0
});
}
AxisManager 龙卷风中轴线
AxisManager.dart
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_can/c13_tornado/Particle.dart';
// 龙卷风的中轴线,继承ChangeNotifier,是为了回调tick()方法,每一帧都调用
class AxisManager extends ChangeNotifier {
Path path = Path();
double x1 = -160;// x1 y1 贝塞尔曲线的第一个控制点
double y1 = -70;
bool isX1Right = true; // x1控制点在移动过程中的方向,通过控制点的移动让龙卷风扭曲
double x2 = 160;
double y2 = -200;
bool isX2Right = true;
double x3 = -190;
double y3 = -400;
bool isX3Right = true;
double angle = 0; // 龙卷风最底部的点,设定为旋转移动,每一帧旋转的角度
AxisManager(){
path.moveTo(0, 0);
path.relativeCubicTo(x1, y1, x2, y2, x3, y3);
}
void tick() {
update();
notifyListeners();
}
void update(){
double dis = 2;// 每一帧,控制点移动的距离
if(x1<=-160){
isX1Right = true;
}else if(x1>=160){
isX1Right = false;
}
if(isX1Right){
x1+=dis;
}else{
x1-=dis;
}
if(x2<=-160){
isX2Right = true;
}else if(x2>=160){
isX2Right = false;
}
if(isX2Right){
x2+=dis;
}else{
x2-=dis;
}
if(x3<=-190){
isX3Right = true;
}else if(x3>=190){
isX3Right = false;
}
if(isX3Right){
x3+=dis;
}else{
x3-=dis;
}
path.reset();
angle-=0.02;
if(angle<-pi*2){
angle=0;
}
path.moveTo(150*sin(angle), x1/5);
path.relativeCubicTo(x1, y1, x2, y2, x3, y3);
}
Path getAxis(){
return path;
}
}
ParticleManager 粒子管理
包括粒子的初始,添加,更新等操作:
ParticleManager.dart
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_can/c13_tornado/AxisManager.dart';
import 'package:flutter_can/c13_tornado/Particle.dart';
class ParticleManager extends ChangeNotifier {
AxisManager am;
ParticleManager(this.am);
List<Particle> list = [];
void add(Particle p) {
if (p != null) {
list.add(p);
notifyListeners();
}
}
void tick() {
Random random = Random();
list.forEach((p) {
doUpdate(p,random);
});
notifyListeners();
}
// 龙卷风
void doUpdate(Particle p,Random random) {
Path axis = am.getAxis();
PathMetric pathMetric = axis.computeMetrics().first;
p.cur += p.curStep;
if(p.cur>pathMetric.length){
p.cur = 0;
}
Tangent tg = pathMetric.getTangentForOffset(p.cur);
double angle = tg.angle;
p.angle = angle;
Offset center = tg.position;
p.center = center;
p.radius = p.cur/5;
p.rotate += 0.01;
if(p.rotate>1){
p.rotate=0;
}
p.x = p.radius * sin(pi * 2 *p.rotate + p.initRotate);
}
// 蛇形上旋
void doUpdate1(Particle p,Random random) {
Path axis = am.getAxis();
PathMetric pathMetric = axis.computeMetrics().first;
p.cur += 2;
if(p.cur>pathMetric.length){
p.cur = 0;
}
Tangent tg = pathMetric.getTangentForOffset(p.cur);
double angle = tg.angle;
p.angle = angle;
Offset center = tg.position;
p.center = center;
p.radius = 100;
p.rotate += 0.01;
if(p.rotate>1){
p.rotate=0;
}
p.x = p.radius * sin(pi * 2 *p.rotate + p.initRotate);
}
// 漏斗形上旋
void doUpdate3(Particle p,Random random) {
p.y += p.vy;
if (p.y < -500) {
p.y = 0;
}
p.radius = 100 - p.y / 4;
p.rotate += 0.01;
if(p.rotate>1){
p.rotate=0;
}
p.x = p.radius * sin(pi * 2 *p.rotate + p.initRotate);
}
}
TimeLine 模拟时间流逝
TimeLine.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_can/c13_tornado/AxisManager.dart';
import 'package:flutter_can/c13_tornado/Particle.dart';
import 'package:flutter_can/c13_tornado/ParticleManager.dart';
import 'TornadoRender.dart';
class TimeLine extends StatefulWidget {
@override
_TimeLineState createState() => _TimeLineState();
}
class _TimeLineState extends State<TimeLine> with SingleTickerProviderStateMixin {
AnimationController _controller;
ParticleManager pm;
AxisManager am;
@override
void initState() {
super.initState();
am = new AxisManager();
pm = new ParticleManager(am);
initParticleManager();
_controller = new AnimationController(vsync: this,duration: const Duration(milliseconds: 2000));
_controller.addListener(() {
am.tick();
pm.tick();
});
_controller.repeat();
}
@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
size: MediaQuery.of(context).size,
painter: TornadoRender(pm),
),
);
}
void initParticleManager() {
int num = 500;
for(int i=0;i<num;i++){
Random random = Random();
pm.add(Particle(
initRotate:pi*2*i/num,
cur:100*i/num,
curStep:random.nextDouble(),
vy:-1+random.nextDouble()
));
}
}
}
TornadoRender 粒子和中轴线绘制
TornadoRender.dart
import 'dart:ui';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_can/c13_tornado/AxisManager.dart';
import 'package:flutter_can/c13_tornado/Particle.dart';
import 'ParticleManager.dart';
class TornadoRender extends CustomPainter{
ParticleManager pm;
TornadoRender(this.pm): super(repaint: pm);
Paint _windPaint = Paint()
..color = Colors.grey
..style = PaintingStyle.fill
..isAntiAlias = true;
@override
void paint(Canvas canvas, Size size) {
translateToCenter(canvas, size);
drawAxis(canvas, size);
drawParticles(canvas, size);
}
void drawAxis(Canvas canvas, Size size){
canvas.drawPath(pm.am.getAxis(), Paint() ..color=Colors.grey ..style=PaintingStyle.stroke ..strokeWidth=1);
}
void drawParticles(Canvas canvas, Size size){
int size = pm.list.length;
for(int i=0;i<size;i++){
Particle particle = pm.list[i];
canvas.save();
canvas.translate(particle.center.dx, particle.center.dy);
// canvas.rotate(-particle.angle+pi/2);
canvas.drawCircle(Offset(particle.x,particle.y), 2, _windPaint);
canvas.restore();
}
}
void translateToCenter(Canvas canvas, Size size){
canvas.translate(size.width/2, size.height-150);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}