Flutter繪製-08-龍捲風效果

天色將變發表於2021-03-25

到目錄頁看看-->

先上圖,用粒子實現一個簡單的龍捲風效果: tenado.gif

只記錄思路和用到的技術點

核心思路

任何特效都是一步步除錯出來的:

  • 定義粒子,模擬時間流逝
  • 定義多個粒子,讓粒子動起來,盤旋上升
  • 彎曲的龍捲風更生動,用貝塞爾曲線當做龍捲風的軸
  • 會動的龍捲風最形象,讓貝塞爾曲線隨時間扭動,並讓龍捲風左右前後旋轉

技術點

  • 使用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;
  }
  
}
複製程式碼

相關文章