likes
comments
collection
share

粒子相册(上)

作者站长头像
站长
· 阅读数 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源码

转载自:https://juejin.cn/post/7178818904620269624
评论
请登录