先上圖,用粒子實現一個簡單的龍捲風效果:
只記錄思路和用到的技術點
核心思路
任何特效都是一步步除錯出來的:
- 定義粒子,模擬時間流逝
- 定義多個粒子,讓粒子動起來,盤旋上升
- 彎曲的龍捲風更生動,用貝塞爾曲線當做龍捲風的軸
- 會動的龍捲風最形象,讓貝塞爾曲線隨時間扭動,並讓龍捲風左右前後旋轉
技術點
- 使用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;
}
}
複製程式碼