Flutter绘制-08-龙卷风效果

1,336 阅读4分钟

查看目录-->

本文代码地址-->

先上图,用粒子实现一个简单的龙卷风效果: 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;
  }
  
}