粒子相册(上)世间万物,由简入繁,皆由粒子构成。就像“物质构成的最小单位是夸克”一样,图片构成的最小单位就是像素。今天就
前沿
世间万物,由简入繁,皆由粒子构成。就像“物质构成的最小单位是夸克”一样,图片构成的最小单位就是像素。今天就让我们通过粒子来构建出相册,我把它称之为粒子相册。
效果展示
按照国际惯例,我们先演示下实现的最终效果:
思路
在开始实现功能之前,我们先捋清一下实现思路。
在上述思维导图中我们可以把实现内容分为 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
新增 addParticle
和 setParticleList
方法:
/// 设置粒子列表
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 相反,其颜色的 blue 和 red 也相反,需要进行转换。
// 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)给粒子 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)Particle
新增 ax
和 ay
属性来为粒子的运动添加加速度
// 加速度ax
double ax;
// 加速度 ay
double ay;
(2)初始化 Particle
添加随机加速度 ax
和 ay
。
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个问题。
- 粒子无法完整生成比屏幕尺寸还大的图片
由于屏幕的粒子是有限的,对于尺寸特别大的图片,我们应该做缩放处理。 缩放逻辑也非常简单,通过换算图片宽高和粒子宽高的比例即可:
/// 图片转换粒子
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;
...
}
}
}
- 粒子运动结束的时间不可控
由于我们初始化的加速的是随机的,所以无法准确的计算出所有粒子完成运动的最终时间,导致我们设置的动画时长为理论最大时长,会存在有时粒子早已运动结束了,但动画还剩下一半时长的情况。
为了优化这种情况,我们这里不使用 AnimationController
改为 Ticker
。
Ticker
类用于在每一帧之间发送通知。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,至此我们完成了粒子相册的前半部分。
总结
本篇文章作为粒子相册的上篇,主要实现了由粒子构成图片、并添加粒子动画的功能。下一篇文章将丰富更多的粒子动画、实现粒子相册的完整功能。如果感兴趣的同学可以关注下,你的关注是我持续更新的动力!(#^.^#)
转载自:https://juejin.cn/post/7178818904620269624