自创银河系,转转转转转----Java球类的椭圆轨迹运动----立体效果

每次看到3D电影,我们都容易心血澎湃。在2D的屏幕,纸张上,体现出3D效果,一直是人们研究的方向,而今天就让以球为例来完成吧。

星系

在实现项目之前我们要明白如何画出一个立体球

for(int i = 0;i < 1000;i++){
    g.setColor(new Color(i / 4,i / 4,i / 4));
    g.fillOval(200 + i / 4 ,300 + i / 4,500 - i / 2,500 - i / 2);
}

原理如下:我们不断绘上逐渐缩小(注意由于我们绘画起点是左上角的点,所以当圆变小时,要将起点向右下方向移动),且逐渐变亮(颜色由白变黑)的实心圆,就能产生立体效果,

这个可以自己先试试,文章不贴图了。
 

1.完成所需平板

我们这次是继承JPanel而来,大致与之前一样,不同的是我们的工具界面。

public void addTool(JPanel toolPanel){

    toolPanel.setLayout(new FlowLayout());
    Dimension btnDim = new Dimension(300,80);
    Dimension toolDim = new Dimension(180,50);
    String[] btnnames = {"开始","停止","继续"};
    String[] jlsnames = {" 起点x坐标: "," 起点y坐标:"," 轨迹宽度:", " 轨迹长度:"," 球尺寸:"};
    for(int i = 0;i < 5;i++){
        JLabel jl = new JLabel(jlsnames[i]);
        jl.setFont(new java.awt.Font("Dialog", 1, 30));//设置标签字体
        JTextField jt = new JTextField();
        jt.setFont(new java.awt.Font("Dialog", 1, 30));//设置输入框字体
        jl.setPreferredSize(toolDim);
        jt.setPreferredSize(toolDim);
        toolPanel.add(jl);
        toolPanel.add(jt);
        ac.jts[i] = jt;//得到输入框的地址
    }
    for (int i = 0; i < 3; i++) {
        JButton btn = new JButton(btnnames[i]);
        btn.setPreferredSize(btnDim);
        btn.addActionListener(ac);
        toolPanel.add(btn);
    }

}//在Jpanel上加上工具

由于我们需要读取界面输入内容,我们可以使用JTextFiled(文本输入框)。而使用JLabel(标签),用于提示用户输入什么。

我们需要得到的信息有,行星运动的轨迹起点,轨迹大小,以及行星大小。

当然由于系统默认字体过于小了,也可以使用setFont方法来自我调节,达到满意效果。

2.完成我们的监听器类

1.建立好我们需要的属性。

    JTextField[] jts = new JTextField[5];
    Graphics g;
    Boolean flag = true;
    Vector<Ball> balls ;
    int[] data = new int[5];

JTextField数组是为了得到输入框的内容,在给按钮绑定监听器的时候,将界面的文本输入框的地址传给监听器中,使得可以读取。

 ac.jts[i] = jt;//得到输入框的地址

但我们需要注意的是,JTextFiled是文本输入框,是String字符串类型,不能当成我们需所得数字使用,故自己写一个方法用于转换,同时建立 

int[] data = new int[5];//储存输入数字

public int change(String input) {
        int allNum = 0;
        int num = 0;
        for (int i = 0; i < input.length(); i++) {
            char a = input.charAt(i);
            int t = (int) a;
            if (t <= 57 && t >= 48) {
                 num = t - 48;
                 allNum*= 10;
                allNum += num;
            }
        }
        return allNum;
    }//把文本框中的字符串转换为 int;

flag则用于行星运行与否的判断。

至于Graphics g 是老生常谈了,而balls 数组(界面内创建,传给监听器)则是储存行星信息的,便于之后绘制。

2.写好按钮反应

public void actionPerformed(ActionEvent e) {

        String btnname = e.getActionCommand();
        String name = btnname;

        if (name.equals("开始")) {

            for(int i = 0;i < 5;i++){
                data[i] = change(jts[i].getText());
            }//得到文本框中的数字

            Ball ball = new Ball(data[0],data[1],data[2],data[3],data[4]);
            ball.orbit(); //先存好轨迹
            balls.add(ball);

        }
        if(name.equals("继续")){
            flag = true;
        }
        if(name.equals("停止")){
            flag = false;
        }
    }

按钮反应如下:

开始:使用界面中文本输入框的信息,创建ball对象,并存入balls数组中。

继续:flag为真

停止:flag为假

3.完成Ball类(界面类创建了该类的数组,且填写了一个默认小球)

1.建立好所需属性,用构析方法得到部分。

    private int xPoint, yPoint , width,  height,size;
    double t; //t用来决定drawBall方法中,i缩减倍数。

    Vector<Integer> x=new Vector<>();
    Vector<Integer> y=new Vector<>();//存放轨迹坐标

    Vector<Double> moveSize=new Vector<>();//存放倍数

    Random rnd = new Random();
    int luckyDog = rnd.nextInt(3);//决定那个颜色为小球颜色

    public Ball(int xPoint, int yPoint, int width, int height, int size) {
        this.xPoint = xPoint;
        this.yPoint = yPoint;
        this.width = width;
        this.height = height;
        this.size = size;
        this.t = 1000 / size;
    }

除了轨迹起点,大小,以及行星尺寸外,我们还需要注意小球随着位置的变化是需要缩放的,可能在绘制过程中  moveSize.get(i) - i会小于0,所以 t 的目的是防止出现负数。

至于数组注释写好了他们的作用,而随机数是用于绘制小球时决定颜色的。

2.计算移动中尺寸的方法

public double countSize(int x,int y) {//参数为轨迹坐标

        double maxDistance;
        double a = width > height ? 0.5 * width : 0.5 * height;
        double b = width < height ? 0.5 * height : 0.5 * width;
        double c = Math.pow(a * a - b * b,2);//得到椭圆的 a,b,c数据
        if(b < c){
            double m = b * b / c * c;
             maxDistance = - c * c * m * m + 2 * b * b * m + a * a + b * b;
        }else{
             maxDistance = 4 * b * b;
        }//通过圆锥曲线知识,求出离下端点最远的点

        double distance = Math.pow(Math.abs(x - xPoint - 0.5 * width),2) +
                Math.pow(Math.abs(y - yPoint - height),2);//求出点到观察点距离

        double time = 1 - Math.pow(distance/maxDistance,0.5) * 0.6;
        double moveSize = this.size * time;
        return moveSize;//为了使效果更好,采取下端点为观察点,这样更能体现出近大远小。
        //之后一定要好好学习数学,怎么都用得上的!!!55555,可恶极了,还是逃不脱数学的魔掌!!!
    }//计算移动的球大小

我们是以下端点为观察点的,离其最远点我们将尺寸缩放 0.4倍,其他按照这个比例来缩放,

至于距离求解可利用高中圆锥曲线知识而得。

3.轨迹方法

public void orbit() {//为了不成为“shit山”,可以多次使用,改用参数来完成!

        int num = 0;

        BufferedImage orbitbuff = new BufferedImage(2000, 2000, BufferedImage.TYPE_INT_RGB);
        Graphics bg = orbitbuff.getGraphics();
        bg.setColor(Color.white);
        bg.drawOval(xPoint, yPoint, width, height);//用缓存画笔画上一个椭圆,提供轨迹
        //值得注意,缓存图片默认为黑色,故我们画上白色椭圆

        for (int i = xPoint; i <= xPoint + width; i++) {
            for (int j = yPoint; j <= yPoint + height; j++) {
                int color = orbitbuff.getRGB(i, j);
                if (color == -1/*为白色*/) {
                    x.add(i);
                    y.add(j);//得到坐标点

                    moveSize.add(countSize(x.get(num),y.get(num++)));//得到缩放尺寸

                    break;/*跳出循环目的是,由于椭圆是对称图形,会出现一个横坐标,
                     两个纵坐标的情况,如果以此绘制球,会出现上下翻跳的情况。*/
                }
            }
        }//仅仅得到椭圆上半部的坐标点

        for (int i = xPoint + width; i >= xPoint; i--) {
            for (int j = yPoint + height; j >= yPoint; j--) {
                int color = orbitbuff.getRGB(i, j);
                if (color == -1/*为白色*/) {
                    x.add(i);
                    y.add(j);

                    moveSize.add(countSize(x.get(num),y.get(num++)));//得到缩放尺寸

                    break;/*跳出循环目的是,由于椭圆是对称图形,会出现一个纵坐标,
                     两个横坐标的情况,如果以此绘制球,会出现左右翻跳的情况。*/
                }
            }
        }//得到椭圆下半部分的坐标点
        //读出缓存图像上椭圆的坐标点,存为我们的数组里,可作为轨迹点
    }//轨迹方法(内含缩放)

值得注意球移动时上下翻跳的原因,而且明白orbitbuff缓存图像,只是为了得到轨迹点,并非绘制。

4.绘制小球方法

 public void ballpaint(Graphics bg , int x ,int y, double size ){

            for (int i = 0; i < 1000; i++) {

                switch (luckyDog){
                    case 0 :
                        Color ballColor0 = new Color(i / 6,0 , 0);
                        bg.setColor(ballColor0);
                        break;
                    case 1 :
                        Color ballColor1 = new Color(0,i / 6 , 0);
                        bg.setColor(ballColor1);
                        break;
                    case 2 :
                        Color ballColor2= new Color(0,0 , i / 6);
                        bg.setColor(ballColor2);
                        break;
                }//决定是哪个颜色的小球

                bg.fillOval((int) (x + i / t / 2), (int) (y + i / t / 2),
                        (int) (size - i / t), (int) (size - i / t));
            }/*改变大小的操作,是通过缩放倍数而完成的!!!!
        这是改进得来,效果更好*/

    }//画一个球方法

小球绘制与立体球绘制大致一样

不同:用switch来决定小球的颜色,而luckyDog值是对象创建时就决定了的,不会变化。

4.完成绘画线程(在界面类时,就已经开启)

为什么要开启线程呢,因为行星运动效果,是不断绘制小球,而随之绘制背景图将其消除,达到动态效果。如果不采用多线程,那么程序卡死,不能实现其他的应用。

1.建立好所需属性,构析方法得到部分。

    Listener ac ;//不用什么 new Listener(),避免死循环创建问题。
    Vector<Ball> balls;//不用什么 new Vector<Ball>(),避免数组地址不一,数据不同问题!
    
    BufferedImage buff = new BufferedImage(2000,1840,BufferedImage.TYPE_INT_RGB);
    Graphics bg = buff.getGraphics();//用于绘制的缓存图片

    Graphics g;

    Vector<Integer> num = new Vector<Integer>();//用于之后绘画方法点的迭代,而分别设置界限;

    public DrawThread(Listener ac, Vector<Ball> balls,Graphics g) throws IOException {
        this.ac = ac;
        this.balls = balls;
        this.g = g;
    }

2.得到背景图片

    File file = new File("C:\\Users\\27259\\Desktop\\milkyRiver.jpg");
    BufferedImage padbuff = ImageIO.read(file);//得到底板背景图片

3.写上run方法里的内容

    public void run(){
        while(true) {
            
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }//防止移动过快,效果不好
            //也可以减少资源过多占用,使得负载过重

            while(!ac.flag){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }//用死循环卡住线程时,一定要加上一些内容,不然计算过快不会再去取 ac.flag,使得线程无法继续

            for (int i = 0;i < balls.size(); i++) {
                if(num.size() <= balls.size()) {/*之所num数组多建一个单位,是为了
                防止 num.get(i) == balls.get(i).x.size() 处的 num.get(i)越界,
                因为 for (int i = 0; i < balls.size(); i++) 处的 balls.size() 可能变大了!!!*/
                    //小小加一,手速就跟不上了!
                    num.add(0);
                }
            }//设置迭代界限

            bg.drawImage(padbuff,0,0,null);//画上背景图片

            for (int i = 0; i < balls.size(); i++) {
               
                int t = num.get(i);
                balls.get(i).ballpaint(bg,balls.get(i).x.get(t),
                        balls.get(i).y.get(t),balls.get(i).moveSize.get(t));

                num.set(i,t+1);
                if(num.get(i) == balls.get(i).x.size() - 1){
                    num.set(i,0);
                }

            }//完成迭代并绘制。

            g.drawImage(buff,0,0,null);//绘制缓存图片
        }
    }

当时监听类所写的 flag 就用在了  while(!ac.flag) 来卡住线程,当然要注意填写一部分内容。

这其中比较难以理解的就是 num数组 了,这是因为为了 避免闪屏 的问题,我们该将所有需要绘制的小球同时绘制在一张缓存图像上,这样绘制背景图时,都是在缓存图像里完成,不会闪屏。

但是我们需要明白并非每个小球的轨迹点数目一致,故需要用一个数组用来遍历各个小球的轨迹点。

1.建立 balls数组 尺寸加一的 num数组,原因如上。

2.遍历 balls数组,让每个 ball对象 都使用 ballpaint方法,而参数的下标使用对应num的值。

3.对应num的值加1

4.判断是否等于对应小球坐标点个数减1,如果是则重置num = 0;

于是我们就完成了自己的星系,之后可以不断尝试想要的天体运转哦。

猜你喜欢

转载自blog.csdn.net/AkinanCZ/article/details/126217306