Create your own Milky Way, turn and turn - elliptical trajectory movement of Java balls - three-dimensional effect

Every time we see a 3D movie, we are easily excited. It has always been the direction of people's research to reflect 3D effects on 2D screens and paper, but let's take a ball as an example to complete it today.

galaxy

Before implementing the project, we need to understand how to draw a three-dimensional ball

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);
}

The principle is as follows: We keep drawing a solid that gradually shrinks (note that since the starting point of our drawing is the upper left corner, when the circle becomes smaller, the starting point should be moved to the lower right) and gradually brightens (the color changes from white to black) A circle can produce a three-dimensional effect,

You can try this yourself first, the article does not post pictures.
 

1. Complete the required tablet

We inherited JPanel this time, roughly the same as before, the difference is our tool interface.

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上加上工具

Since we need to read interface input, we can use JTextFiled (text input box). Instead, use JLabel (label), which is used to prompt the user to enter something.

The information we need to obtain includes the starting point of the trajectory of the planetary motion, the size of the trajectory, and the size of the planet.

Of course, because the system default font is too small, you can also use the setFont method to adjust it yourself to achieve a satisfactory effect.

2. Complete our listener class

1. Establish the attributes we need.

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

The JTextField array is to obtain the content of the input box. When binding the listener to the button, the address of the text input box on the interface is passed to the listener so that it can be read.

 ac.jts[i] = jt;//Get the address of the input box

But what we need to pay attention to is that JTextFiled is a text input box, which is of String type and cannot be used as the number we need, so write a method for conversion and create 

int[] data = new int[5];//store input number

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;

The flag is used to judge whether the planet is running or not.

As for Graphics g, it is a cliché, and the balls array (created in the interface and passed to the listener) stores planetary information for later drawing.

2. Write the button response

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;
        }
    }

The button reacts as follows:

Start: Use the information in the text input box in the interface to create a ball object and store it in the balls array.

continue: flag is true

stop: flag is false

3. Complete the Ball class (the interface class creates an array of this class and fills in a default ball)

1. Establish the required attributes, and use the method of construction to obtain the part.

    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;
    }

In addition to the trajectory starting point, size, and planet size, we also need to pay attention to the small ball needs to be scaled as the position changes. Maybe moveSize.get(i) - i will be less than 0 during the drawing process, so the purpose of t is to prevent A negative number appears.

As for the array annotations, their functions are written, and the random number is used to determine the color when drawing the ball.

2. The method of calculating the moving size

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,可恶极了,还是逃不脱数学的魔掌!!!
    }//计算移动的球大小

We use the following endpoint as the observation point, and we scale the size by 0.4 times from the farthest point, and the others are scaled according to this ratio,

As for the distance solution, it can be obtained by using the knowledge of high school conic curves.

3. Trajectory method

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

It is worth noting the reason why the ball jumps up and down when it moves, and understands that the orbitbuff caches the image, just to get the trajectory point, not to draw.

4. Draw the ball method

 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));
            }/*改变大小的操作,是通过缩放倍数而完成的!!!!
        这是改进得来,效果更好*/

    }//画一个球方法

Small ball drawing is roughly the same as solid ball drawing

Different: use switch to determine the color of the ball, and the luckyDog value is determined when the object is created and will not change.

4. Complete the drawing thread (it has already been turned on when the interface class is in)

Why open the thread, because the effect of planetary movement is to continuously draw small balls, and then draw the background image to eliminate it to achieve a dynamic effect. If multi-threading is not used, the program will be stuck and other applications cannot be implemented.

1. Establish the required attributes, and get the part by the analysis method.

    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. Get the background image

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

3. Write the content in the run method

    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);//绘制缓存图片
        }
    }

At that time, the flag written by the listening class was used in   while(!ac.flag) to block the thread. Of course, you must pay attention to filling in part of the content.

The more difficult thing to understand is  the num array . This is because in order to  avoid the problem of splash screen  , we should draw all the balls that need to be drawn on a cached image at the same time, so that when drawing the background image, it is all in the cached image. Completed without flickering.

But we need to understand that not every ball has the same number of track points, so we need to use an array to traverse the track points of each ball.

1. Create a num array with the size of the balls array plus one, for the above reasons.

2. Traverse the balls array, let each ball object use the ballpaint method, and the subscript of the parameter uses the value corresponding to num.

3. Add 1 to the value corresponding to num

4. Determine whether it is equal to the number of corresponding small ball coordinate points minus 1, if so, reset num = 0;

So we have completed our own galaxy, and then we can keep trying to move the celestial bodies we want.

Guess you like

Origin blog.csdn.net/AkinanCZ/article/details/126217306