开启成长之旅!这是我参与「日新方案 12 月更文应战」的第4天,点击查看活动详情

前沿

世间万物,由简入繁,皆由粒子构成。就像“物质构成的最小单位是夸克”相同,图片构成的最小单位便是像素。今天就让咱们经过粒子来构建出相册,我把它称之为粒子相册

作用展现

按照国际惯例,咱们先演示下完成的最终作用:

思路

在开端完成功用之前,咱们先捋清一下完成思路。

粒子相册(上)

在上述思想导图中咱们能够把完成内容分为 4个部分

粒子。分为粒子粒子办理器两部分。

提取图片像素。需求把图片转为对应的像素,便于粒子制作图片。

粒子复原图片。根据图片的像素用粒子进行复原。

粒子动画。给粒子生成图片的进程增加动画。

功用完成

一、粒子和粒子办理器

咱们先创立 Particle 实体类,增加对应特点:Github源码

  Particle({
    this.x = 0,
    this.y = 0,
    this.size = 0,
    this.color = Colors.white,
  });

然后创立 ParticleManage 办理类,用于粒子的办理。因为咱们是经过 ParticleManage 来更新粒子的,这儿 extends ChangeNotifier 用于粒子的更新:

class ParticleManage extends ChangeNotifier {}

ParticleManage 新增 addParticlesetParticleList 方法:

  /// 设置粒子列表
  void setParticleList(List<Particle> list) {
    particleList = list;
    notifyListeners();
  }
  /// 增加粒子
  void addParticle(Particle particle) {
    particleList.add(particle);
    notifyListeners();
  }

自定义 ParticlePainter 经过 CustomPaint 来完成粒子的制作:

CustomPaint(
    size: const Size(200, 200),
    painter: ParticlePainter(manage: particleManage),
);
class ParticlePainter extends CustomPainter { 
 	...
  @override
  void paint(Canvas canvas, Size size) {
    for (Particle particle in manage.particleList) {
      _drawParticle(canvas, particle);
    }
  }
   /// 制作粒子
  void _drawParticle(Canvas canvas, Particle particle) {
    canvas.drawCircle(Offset(particle.cx, particle.cy), particle.size,
        particlePaint..color = particle.color);
  }
}

制作单个粒子:

_manage.addParticle(Particle(x: 200, y: 250, size: 25, color: Colors.red)

作用:

粒子相册(上)

生成粒子背景:

double size = 4;
for (int i = 0; i < 50; i++) {
  for (int j = 0; j < 50; j++) {
    _manage.addParticle(Particle(
      x: size + 2 * size * j,
      y: size + 2 * size * i,
      size: size,
    ));
  }
}

作用:

粒子相册(上)

生成回形粒子图形,代码:

  /// 回形图形
  void toPaperClip() {
    List<int> scales = [0, 1, 2, 3, 4];
    for (int i = 0; i < 50; i++) {
      for (int j = 0; j < 50; j++) {
        if (((i == 24 - scales[0] * 5 || i == 24 + scales[0] * 5) && j >= 24 - scales[0] * 5 && j <= 24 + scales[0] * 5) ||
            ((j == 24 - scales[0] * 5 || j == 24 + scales[0] * 5) && i >= 24 - scales[0] * 5 && i <= 24 + scales[0] * 5)) {
          particleManage.particleList[i * 50 + j].color = Colors.blue;
        }
        ...
        if (((i == 24 - scales[4] * 5 || i == 24 + scales[4] * 5) && j >= 24 - scales[4] * 5 && j <= 24 + scales[4] * 5) ||
            ((j == 24 - scales[4] * 5 || j == 24 + scales[4] * 5) && i >= 24 - scales[4] * 5 && i <= 24 + scales[4] * 5)) {
          particleManage.particleList[i * 50 + j].color = Colors.blue;
        }
      }
    }
  }

作用:

粒子相册(上)

二、提取图片像素

图片像素的提取,咱们这儿经过 image 库来完成。

  Future<void> initImage() async {
    ByteData data = await rootBundle.load("assets/images/hanzi.png");
    List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    imagePic = image.decodeImage(bytes)!;
  }

要注意,经过 image 获取到的图片 x轴y轴Flutter 相反,其颜色的 bluered相反,需求进行转化。

// image库获取的x、y和Flutter相反,需求把j做为x轴
int x = width * j ~/ 200;
int y = height * i ~/ 200;
// 颜色进行转化
Color.fromARGB(color.alpha, color.blue, color.green, color.red)

三、粒子复原图片

根据获取到的图片像素用粒子进行复原。

 /// 图片转化粒子
  void imageToParticle(){
    if(imagePic == null) return;
    int width = imagePic!.width;
    int height = imagePic!.height;
    for(int i = 0; i < 50; i++) {
      for(int j = 0; j < 50; j++) {
        // image库获取的x、y和Flutter相反,需求把j做为x轴
        int x = j;
        int y = i;
        var pixel = imagePic!.getPixel(x, y);
        if(pixel != Colors.white.value) {
          particleManage.particleList[i * 200 + j].color = Colors.blue;
        }
      }
    }
  }

原图:

粒子相册(上)

作用:

粒子相册(上)

调整下粒子的颗粒度:

粒子相册(上)

四、粒子动画

假如仅仅只是经过粒子来展现图片,未免也太 “大材小用” 了。这儿咱们经过增加粒子动画让其生成图片的进程动起来。

  1. 打印机作用

咱们经过模拟打印机打印文件的动画来让粒子动起来。

(1)给粒子 particle 增加当时方位坐标,用于粒子的运动。

  // 当时x坐标
  double cx;
  // 当时x坐标
  double cy;

(2)粒子办理器 ParticleManage 新增粒子更新 onUpdate,办理粒子方位更新。

  void onUpdate() {
    for (Particle particle in particleList) {
      updateParticle(particle);
    }
    notifyListeners();
  }
    /// 更新粒子方位
  void updateParticle(Particle particle) {
    if (particle.cx < particle.x) {
      particle.cx = min(particle.x, particle.cx + 5);
    }
    if (particle.cy < particle.y) {
      particle.cy = min(particle.y, particle.cy + 5);
    }
  }

(3)增加 AnimationController 执行动画。

    _controller = AnimationController(duration: const Duration(seconds: 6), vsync: this);
    _controller.addListener(() {
      particleManage.onUpdate();
    });

作用:

粒子相册(上)

  1. 粒子生成作用

咱们还能够经过粒子的随机运动来完成从无到有的生成作用。

(1)Particle 新增 axay 特点来为粒子的运动增加加快度

  // 加快度ax
  double ax;
  // 加快度 ay
  double ay;

(2)初始化 Particle 增加随机加快度 axay

    double size = 1;
    var random = Random();
    for (int i = 0; i < 200; i++) {
      for (int j = 0; j < 200; j++) {
        double x = size + 2 * size * j;
        double y = size + 2 * size * i;
        list.add(Particle(
          x: x,
          y: y,
          cx: x - (random.nextDouble() * 200 - 100),
          cy: y - (random.nextDouble() * 200 - 100),
          size: size,
          ax: 2 + random.nextDouble() * 10,
          ay: 2 + random.nextDouble() * 10,
        ));
      }
    }
    particleManage.setParticleList(list);

(3)粒子办理器 ParticleManage 调整粒子更新 onUpdate 的逻辑。

if (particle.cx > particle.x) {
    if (particle.cx > particle.x) {
      particle.cx = max(particle.x, particle.cx - particle.ax);
    } else if (particle.cx < particle.x) {
      particle.cx = min(particle.x, particle.cx + particle.ax);
    }
    if (particle.cy > particle.y) {
      particle.cy = max(particle.y, particle.cy - particle.ay);
    } else if (particle.cy < particle.y) {
      particle.cy = min(particle.y, particle.cy + particle.ay);
    }

作用:

粒子相册(上)

五、优化

前面的粒子动画存在2个问题。

  1. 粒子无法完好生成比屏幕尺度还大的图片

因为屏幕的粒子是有限的,关于尺度特别大的图片,咱们应该做缩放处理。 缩放逻辑也非常简略,经过换算图片宽高和粒子宽高的份额即可:

  /// 图片转化粒子
  void imageToParticle(){
    if(imagePic == null) return;
    int width = imagePic!.width;
    int height = imagePic!.height;
    for(int i = 0; i < 200; i++) {
      for(int j = 0; j < 200; j++) {
        // image库获取的x、y和Flutter相反,需求把j做为x轴
        int x = width * j ~/ 200;
        int y = height * i ~/ 200;
				...
      }
    }
  }
  1. 粒子运动结束的时刻不可控

因为咱们初始化的加快的是随机的,所以无法准确的计算出所有粒子完成运动的最终时刻,导致咱们设置的动画时长为理论最大时长,会存在有时粒子早已运动结束了,但动画还剩下一半时长的状况。

为了优化这种状况,咱们这儿不运用 AnimationController 改为 TickerTicker 类用于在每一帧之间发送通知。Ticker 目标经过回调函数向它的侦听器发送 tick 事件。咱们能够运用 Ticker 目标来完成动画或守时使命。


  _ticker = createTicker(_updateTicker);
  @override
  void dispose() {
    // _controller.dispose();
    _ticker.stop(canceled: true);
    super.dispose();
  }

ParticleManage 办理器中增加粒子 completed 逻辑判断。

  /// 更新粒子
  void onUpdate() {
    bool completed = true;
    for (Particle particle in particleList) {
      updateParticle(particle);
      completed = completed && isParticleCompleted(particle);
    }
    isCompleted = completed;
    notifyListeners();
  }
  /// 粒子是否已移动到指定方位
  bool isParticleCompleted(Particle particle) {
    return particle.cx == particle.x && particle.cy == particle.y;
  }

若粒子已完成运动,则 Ticker 停止更新。

  void _updateTicker(Duration elapsed) {
    particleManage.onUpdate();
    // 获取粒子已完成运动,则停止ticker监听
    if(particleManage.isCompleted) {
      _ticker.stop();
    }
  }

OK,至此咱们完成了粒子相册的前半部分

总结

本篇文章作为粒子相册上篇,主要完成了由粒子构成图片、并增加粒子动画的功用。下一篇文章将丰富更多的粒子动画、完成粒子相册的完好功用。假如感兴趣的同学能够重视下,你的重视是我持续更新的动力!(#^.^#)

Github源码