线程操作图片粒子化飞散及复原
先看效果图片。
第一步先了解什么是线程
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
通俗易懂的来讲,线程就是想是一个控制器,操纵着其它进程的运作。放在我们这个简单的程序中来看,图片粒子化后,每一个像素粒子都是一个运动对象,如果每一个都要自写详细的方法会过于繁杂,我们就将这些粒子放在一个大类中,用一个线程方法来统一进行调度管理,能够在很大程度上让程序结构更加清晰。
在具体的用法程序上,java中线程的用法一般有两种,一种是扩展java.lang.Thread类,另一种是实现java.lang.Runnable接口。在我们的案例方法中采用的是第一种,继承Thread类,run()方法重写线程操作,最后用start()启动线程。
第二步了解简单向量运动原理
高中数学大家都一定学过向量,“向量”均指欧几里得向量,它的定义是:一个既有大小又有方向的几何对象。向量通常被绘制为一个带箭头的线段,线段的长度代表向量的大小,箭头所指的方向就是向量的方向。
好,用人话通俗的来讲,在java程序中,我们可以把向量当作两点之间的差异,也就是从一个点到另一个点所发生的移动,如下图。
那么在程序中我们就可以通过写一个坐标,包含着x,y两个方向的点来表示其会进行的向量运动。例如:(-15,3) 向西走 15 步,然后向北走 3 步。(3,4) 向东走 3 步,然后向北走 4 步。(2,-1) 向东走 2 步,然后向南走 1 步。
在我们的程序中,具体就可以表现为,新位置 = 原位置 + 速度。一个像素粒子的初始坐标loacation是(0,0),我们给定它一个速度veloc(2,2),那么不断反复调用这个方法,小球的x,y坐标每次都会加2,就会一直变动,好像动了起来一样。
但我们会发现,这样的小球运动是一个匀速运动,因为速度veloc始终是(2,2),没有变化。这时,我们如果想做一个加速运动,就可以给它加上一个加速度acce,加速度的原理同样跟速度导致位置变化的原理是一样的,即新速度=原速度+加速度。当我们设置一个加速度acce(1,1),每次给速度加上加速度,速度就会不断加大,再把这个不断加大的速度加到坐标上,小球就会呈现出一个加速运动的状态。
在了解这两个基本的知识点后,我们就可以开始写代码程序了。
第三步构造程序
3.1 向量运动类
首先,我们可以先写一个向量运动类,里面包含着一些向量相加的基本方法,可以在后续调用。
具体代码如下:
public class PVector {
public float x,y;
public PVector(float x, float y) {
//后续创建location、veloc、acce具体对象时,导入数值
this.x=x;
this.y=y;
}
public void add(PVector pvector) {
//向量相加,用于后续位置location+速度veloc以及速度veloc+加速度acce
this.x+=pvector.x;
this.y+=pvector.y;
}
public float getX() {
//后续粒子需要调转反向运动时,需要获取此时的x轴上的加速度值
return this.x;
}
public float getY() {
//后续粒子需要调转反向运动时,需要获取此时的y轴上的加速度值
return this.y;
}
}
3.2 运动物体类
在我们的程序中,会有很多很多个像素粒子小球运动(具体多少取决于照片的大小以及后续取出照片中像素粒子的个数),为了简化程序,我们将这些小球全部放在运动物体类这个类中进行统一安排。
那我们所需要的就是在这个类中写两个方法,一个是小球如何运动的move()方法,一个是绘制小球的draw()方法,让后续线程中可以直接调用这些方法。同时也需要创建一些像素小球会用到的变量,例如:坐标location,速度veloc,加速度acce,像素小球的大小size,像素小球颜色color等。
具体代码如下:
public class Mover {
private PVector location;
private PVector veloc;
public PVector acce;
private int color;
private int size=20;
public Mover(PVector location, PVector veloc, PVector acce, int color) {
this.location=location;
this.veloc=veloc;
this.acce=acce;
this.color=color;
}
public void Draw(Graphics g) {
//画出像素粒子小球的方法
g.setColor(new Color(color));//设置画笔画像素粒子小球的颜色
g.fillOval((int)location.x, (int)location.y, size, size);//画出画像素粒子小球
}
public void Move() {
//像素粒子小球运动的方法
veloc.add(acce);//速度的变化:速度加上加速度
location.add(veloc);//位置的变化:位置加上速度
}
}
3.3 线程操作类
线程操作类是我们整个程序中最为重要,也是内容最多的一个类,其实包含着获取图片像素点信息,将像素点信息转化成一个个具体的运动物体类中的对象,线程中的run()方法等,所以我们将其分为几个部分来讲。
3.3.1 获取图片像素点信息的方法
思路是,将导入的图片转化成BufferedImage形式,就可以获取照片的像素点具体信息,转化后,先获取照片的长、宽中分别一共有多少个像素点,然后建立一个长、宽分别等于照片长宽像素个数的二维数组来存储每个长宽坐标下的像素点的RGB值,用于后续每次像素粒子小球运动后画小球时需要设置其颜色。
具体的代码如下:
public int[][] getImagePixel(String Image){
//将导入的图片转化成BufferedImage形式
File file=new File(Image);
BufferedImage bi=null;
try {
bi=ImageIO.read(file);
} catch (Exception e) {
e.printStackTrace();
}
//获取图片的长宽
int w=bi.getWidth();
int h=bi.getHeight();
//创建一个数组来来存储每个像素点的RGB值
int[][] Array=new int[w][h];
for(int i=0;i<w;i++) {
for(int j=0;j<h;j++) {//遍历每一个像素点
int pixel=bi.getRGB(i, j);//获取每一个像素点的RGB
Array[i][j]=pixel;//将每一个像素点RGB值存储在对应的二维数组位置中
}
}
return Array;
}
3.3.2 将像素粒子转化成运动物体类的对象
在这一步的方法中的思路是,我们需要将从上一步获取的存储着每一个像素粒子的数组取出,然后将每一个粒子转化为一个个运动物体类的对象,并在这里给每一个像素粒子对象所需要的位置,速度,加速度,颜色。并把这些成为运动物体类的对象的像素粒子加入一个队列中,让后续的线程来直接操作这个队列,就能够实现对所有像素粒子的操纵。
具体的代码如下:
public void init() {
int[][] Array2=getImagePixel("C:\\Users\\lenovo\\Desktop\\haidiyuleipng-001.png");//用一个数组来装之前方法中获取的所有像素点的RGB值
for(int i=0;i<Array2.length;i+=3) {
for(int j=0;j<Array2[i].length;j+=3) {
//遍历所有像素点,横纵坐标中每隔3个取一个像素点转换成具体的运动类物体类的对象
int color=Array2[i][j];//获取该像素点的RGB值
PVector location=new PVector(i+50,j+50);//给定初始位置
PVector veloc=new PVector(0,0);//给定速度
Random random=new Random();//让其在-1到1之间随机给定一个加速度值
float fx=random.nextFloat()*2-1;
float fy=random.nextFloat()*2-1;
PVector acce=new PVector(fx,fy);
Mover mover=new Mover(location,veloc,acce,color);
list.add(mover);//将运动物体类对象加入到队列中来
}
}
}
这里面有几个点需要注意,首选在遍历像素点时,每隔3个选取一个像素点转化成具体的运动类对象,而没有把每一个点都转化,是因为如果把所有点都转化成运动类对象,会导致对象过多,电脑运行时候很卡,当如如果你的电脑配置好,也可以试试。其次在给定加速度时候,是在每选取一个像素点转化成对对象时给一个随机的值,这样可以让画面看起来更具有随机性,random.nextFloat()*2-1是为了让这个随机浮点数在-1到1之间,有正有负,类似于爆炸效果,如果只有正数,则不会产生这种效果。
3.3.3 具体的线程run()方法
这一步的方法可能有一点点难理解其中的逻辑思路,需要用到类似于高中物理的小车行程问题的思路。
因为我们的像素粒子在点击鼠标左键后能够四散飞开,此时当我们点下鼠标右键时候,需要他们能够复原,回到原来的位置,这就需要我们在点击鼠标右键后改变原有的加速度方向。先看下图来进行理解:
首先在我们点击鼠标左键后,粒子不断会以一个加速度向外扩散,其走过的时间我们将其设定为count,其次在点击鼠标右键后,我们将加速度变为其相反的方向(加速度在图中就是斜率),加速度反向后,像素粒子的速度就开始不断减小,在同样经历了count时间后,速度减小到0,粒子就达到了向外飞出移动的最远距离,即第一块三角形的面积,随后仍然在不断的反向加速度,粒子速度也开始变为反向,即开始相反方向运动了,在经历count时间,粒子反向运动的速度达到了之间正向扩散运动的最大速度。最后在此时当粒子达到反向运动的最大之后再次改变粒子的加速度,再次将其加速度变为反方向,让粒子反向运动速度减速,经过等同于count时间的boundcount时间后,粒子反向运动速度降为0,并且也回到了原位,即在图中显示为正向三角形面积等娱乐反向三角形的面积。
在搞清楚了原理思路后,下面就看代码:
public void run() {
while(true) {//始终判断为真让其不断重复
BufferedImage bufferedimage=new BufferedImage(1200, 1200, BufferedImage.TYPE_INT_RGB);
Graphics gr=bufferedimage.getGraphics();
if(isstart) {//点下鼠标左键后
for(int i=0;i<list.size();i++) {
//取出队列中所有存储的运动物体类对象
Mover mover=list.get(i);
mover.Move();
mover.Draw(gr);//先让图片画在内存中
}
g.drawImage(bufferedimage, 0, 0, null);//再将图片画在窗体上
if(back) {//点击鼠标右键后
count--;
if(count==0) {//反向速度达到最大值后
for(int i=0;i<list.size();i++) {
//再次将所有运动对象的加速度反向
Mover mover=list.get(i);
mover.acce=new PVector(-mover.acce.getX(),-mover.acce.getY());
}
}
if(count<0) {//反向运动速度达到最大值后
boundcount--;
//System.out.println("boundcount"+boundcount);
}
if(boundcount==0) {//像素粒子复原
//System.out.println("boundcount"+boundcount);
isstart=false;
}
}else {//像素粒子仍在正向加速运动中
count++;
}
//System.out.println("count"+count);
}
try {//让每次反复执行run()方法之间停顿40毫秒
Thread.sleep(40);
} catch (Exception e) {}
}
}
}
这里面也同样有几点需要注意。第一,粒子先画在内存中,没有直接画在窗体上,是为了防止直接画在窗体中会存在很多重影,需要不断擦除,会导致闪烁等问题,想实践可以自己试一试。第二,严格来说里面是count,boundcount并非时间,而是每次运行一次就累加一次的次数,但是可以将其等同于时间来理解,有助于理清思路。第三如果每次循环run方法时休眠的时间过短,会导致最后像素点复原后仍有很多残影,如果恢复的像素粒子很多,同样可以尝试再次调大休眠的时间,来消除残影。第四 run方法里并没有出现点击鼠标右键第一次出现的加速度反向的代码,因为我们将其放在了监听器代码里面,详见下一个类。
整体来说,这一部分的完整代码如下:
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.Random;
import javax.imageio.ImageIO;
public class MoveThread extends Thread{
public ArrayList<Mover> list=new ArrayList();
private Graphics g;
boolean isstart=false;
boolean back=false;
public int count=0;
public int boundcount=0;
public MoveThread(Graphics g) {//构造器,传入画笔
this.g=g;
init();
}
public int[][] getImagePixel(String Image){
File file=new File(Image);
BufferedImage bi=null;
try {
bi=ImageIO.read(file);
} catch (Exception e) {
e.printStackTrace();
}
int w=bi.getWidth();
int h=bi.getHeight();
int[][] Array=new int[w][h];
for(int i=0;i<w;i++) {
for(int j=0;j<h;j++) {
int pixel=bi.getRGB(i, j);
Array[i][j]=pixel;
}
}
return Array;
}
public void init() {
int[][] Array2=getImagePixel("C:\\Users\\lenovo\\Desktop\\haidiyuleipng-001.png");
for(int i=0;i<Array2.length;i+=3) {
for(int j=0;j<Array2[i].length;j+=3) {
int color=Array2[i][j];
PVector location=new PVector(i+50,j+50);
PVector veloc=new PVector(0,0);
Random random=new Random();
float fx=random.nextFloat()*2-1;
float fy=random.nextFloat()*2-1;
PVector acce=new PVector(fx,fy);
Mover mover=new Mover(location,veloc,acce,color);
list.add(mover);
}
}
}
public void run() {
while(true) {
BufferedImage bufferedimage=new BufferedImage(1200, 1200, BufferedImage.TYPE_INT_RGB);
Graphics gr=bufferedimage.getGraphics();
if(isstart) {
for(int i=0;i<list.size();i++) {
Mover mover=list.get(i);
mover.Move();
mover.Draw(gr);
}
g.drawImage(bufferedimage, 0, 0, null);
if(back) {
count--;
if(count==0) {
for(int i=0;i<list.size();i++) {
Mover mover=list.get(i);
mover.acce=new PVector(-mover.acce.getX(),-mover.acce.getY());
}
}
if(count<0) {
boundcount--;
System.out.println("boundcount"+boundcount);
}
if(boundcount==0) {
System.out.println("boundcount"+boundcount);
isstart=false;
}
}else {
count++;
}
System.out.println("count"+count);
}
try {
Thread.sleep(40);
} catch (Exception e) {}
}
}
}
3.4 监听器类
监听器类主要实现的就是点击鼠标左键和右键后会出现的情况变化。点击鼠标左键,像素粒子开始随机扩散,点击鼠标右键后,像素粒子开始复原。
具体的代码如下:
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class Listener extends MouseAdapter{
private MoveThread mt;
public Listener(MoveThread mt) {
this.mt=mt;
}
public void mouseClicked(MouseEvent e) {
if(e.getButton()==1) {//点击鼠标左键
mt.isstart=true;
}
if(e.getButton()==3) {//点击鼠标右键
mt.back=true;
mt.boundcount=mt.count;
mt.count=2*mt.count;//count时间变为原本正向运动速度达到峰值花费的count时间的两倍,用来开始减速和反向运动
for(int i=0;i<mt.list.size();i++) {
//遍历所有队列中的像素粒子运动对象使其加速度反向
Mover mover=mt.list.get(i);
mover.acce=new PVector(-mover.acce.getX(),-mover.acce.getY());
}
}
}
}
3.5 界面类
最后,我们需要创建一个窗体见面来承载这个小动画的效果。
具体代码如下:
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
public class UI extends JFrame{
public void ShowUI() {
this.setTitle("ggg");
this.setSize(800, 1200);
this.setLocationRelativeTo(null);
this.getContentPane().setBackground(Color.BLACK);
this.setDefaultCloseOperation(3);
this.setVisible(true);
Graphics g=this.getGraphics();
MoveThread mt=new MoveThread(g);
Listener ul=new Listener(mt);
this.addMouseListener(ul);
mt.start();//启动线程
}
public static void main(String[] args) {
UI ui=new UI();
ui.ShowUI();
}
}
对于窗体和监视器中的一些知识点,需要详细了解的可以回看本人之前的文章中有过讲解,这里不再详细展开。