前言
在学校里,我由于语文月考的缘故,接触了国宝。然后,我突发奇想想用Java来模拟千里江山图这一国宝,希望我的设计能有恢宏大气的效果
那么接下来就是大展宏图的时候了!!!
---------------------手动分割---------------------
具体步骤
1.
窗体的设计
public class background {
public void showbg() {
JFrame bg = new JFrame();
// 设置窗体基本属性
bg.setTitle("地球平原");
bg.setSize(800, 600);
bg.setLocationRelativeTo(null);
// 窗体关闭时结束程序
bg.setDefaultCloseOperation(bg.EXIT_ON_CLOSE);
// 设置背景颜色,直接设置没有用,需要用Panel组件
Color bgcolor = new Color(255, 255, 255);
bg.getContentPane().setBackground(bgcolor);
// 设置布局,放在可见前才显示
FlowLayout flow = new FlowLayout();
bg.setLayout(flow);
// 设置可见,并获取画布对象
bg.setVisible(true);
Graphics g = bg.getGraphics();
// 设置鼠标监听器
MouseListener mouselistener = new MouseListener();
bg.addMouseListener(mouselistener);
}
public static void main(String args[]) {
background bg = new background();
bg.showbg();
}
}
这一部分没有什么需要具体描述的
需要注意的是窗体背景的设置需要用到Panel组件
直接set是没有用的
2.
山脉的绘制
public void showmt(double startx, double starty, double endx, double endy, Color mtcolor, double range, double timescounter) {
double midx, midy;
//计算中点坐标
midx = (startx + endx) / 2;
midy = (starty + endy) / 2 + (Math.random() * 2 - 1)/*正负号*/ * range;
if (timescounter++ == times) {
//画山脉轮廓
g.drawLine((int) startx, (int) starty, (int) midx, (int) midy);
g.drawLine((int) midx, (int) midy, (int) endx, (int) endy);
} else {
//随着相邻两个点的横坐标距离减少,纵坐标随机起伏范围也要减小
range *= rate;
//递归
showmt(startx, starty, midx, midy, mtcolor, range, timescounter);
showmt(midx, midy, endx, endy, mtcolor, range, timescounter);
}
}
这部分是整个绘制的重点,比较考验递归的思维
· 大致思路是取一条水平横线,计算横线两个端点(记为P,Q)的中点M’,将中点向上或者向下平移一段距离,得到新的点M。
---将P和M两点作为新的端点重复上述过程
---将M和Q两点也作为新的端点重复上述过程
最终将所得的所有点连起来,这样我们就会得到一条折线,只要对变化时的参数进行控制,就会呈现山脉的效果。
· 整个的迭代过程类似于一个满二叉树。times为二叉树的总层数,timescounter为这一次调用函数时所在的那一层。
如果我们对变换时的参数不加控制,就会使得画出的直线类似于噪音的声波,变得杂乱无章。想画出比较类似山脉的图案:
· 要设置中点上下偏移的范围range,保证不会出现山峰过高或过矮
· 为了使得山脉更平滑,加入了参数rate∈(0,1),随着timescounter的增加,range*=rate,相邻两点间的高度起伏就不会太大,否则就会如下图所示
3.
添加监听器
public class MouseListener implements java.awt.event.MouseListener, ActionListener {
Graphics g;
public void setGraphics(Graphics g) {
this.g = g;
}
public void mouseClicked(MouseEvent e) {
//绘制山脉
mountain mountain = new mountain();
mountain.setGraphics(g);
//此处为伪代码,颜色和位置根据需要修改
Color mtcolor = new Color( ,,);
mountain.showmt(0, starty, 800, endy, mtcolor, 100, 1);
// showmt(double startx, double starty, double endx, double endy, Color mtcolor, double range, double timescounter)
}
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void actionPerformed(ActionEvent e) {
}
}
运行结果
最终画出来应该是这样的轮廓
⬇️
当然
光是这样的图
肯定是没办法模拟国宝的
所以还需要修改
填充
public void showmt(double startx, double starty, double endx, double endy, Color mtcolor, double range, double timescounter) {
double midx, midy;
//计算中点坐标
midx = (startx + endx) / 2;
midy = (starty + endy) / 2 + (Math.random() * 2 - 1) * range;
if (timescounter++ == times) {
//画山脉轮廓
g.drawLine((int) startx, (int) starty, (int) midx, (int) midy);
g.drawLine((int) midx, (int) midy, (int) endx, (int) endy);
//填充山脉
g.setColor(mtcolor);
g.drawLine((int) midx, (int) midy, (int) midx, (int) 600/*画布底部*/);
} else {
//随着相邻两个点的横坐标距离减少,纵坐标随机起伏范围也要减小
range *= rate;
//递归
showmt(startx, starty, midx, midy, mtcolor, range, timescounter);
showmt(midx, midy, endx, endy, mtcolor, range, timescounter);
}
}
这是递归次数(times)为10时画出的效果
这是递归次数(times)为11时画出的效果:
群山
for (int i = 0; i < 4; i++) {
int distance = 100;
// 每座山脉间距离
mountain mountain = new mountain();
mountain.setGraphics(bufferg);
// 先在缓存中绘制
Color mtcolor = new Color(i * 50, i * 50, i * 50);
// 设置每座山脉颜色
mountain.showmt(0, 50 + i * distance, 800, 50 + i * distance, mtcolor, 100, 1);// showmt(double startx, double starty, double endx, double endy, Color mtcolor, double range, double timescounter)
}
只有一座山肯定是不够的,因此我们要用循环语句来画出多座山脉,同时对远近山脉的高低、颜色进行调整,这部分代码放在监听器中。
提速
//绘制山脉
// 设置缓存
BufferedImage buffer = new
BufferedImage(800, 400,
BufferedImage.TYPE_INT_RGB);
// 设置缓存画布
Graphics bufferg = buffer.getGraphics();
// 设置画布背景颜色
Color bufferbgcolor = new
Color(255,255,255);
bufferg.setColor(bufferbgcolor);
bufferg.fillRect(0, 0, 800, 400);
// 在缓存中绘制山脉
for (int i = 0; i < 4; i++) {
int distance = 100;// 每座山脉间距离
mountain mountain = new mountain();
mountain.setGraphics(bufferg);
// 先在缓存中绘制
Color mtcolor = new Color(i * 50, i * 50, i * 50);
// 设置每座山脉颜色
mountain.showmt(0, 50 + i * distance, 800, 50 + i * distance, mtcolor, 100,
1); // showmt(double startx, double starty, double endx, double endy, Color mtcolor, double range, double timescounter)
}
实际运行过上面的代码就会知道,递归11次会使得画出图像需要1秒左右的过程,有没有什么办法能使图像在运行的瞬间就显示出来呢?这就需要用到计算机的缓存了,我们可以事先在缓存中画出图形并保存,然后将缓存中的图像显示出来。(CPU与缓存间传输信息的速度要大于CPU与输入输出设备的)
这样做还有一个好处,由于我们绘制图像时使用了随机函数,导致我们每次调用函数都会改变图像,当其他构件发生变化时,山脉的图像也在跟着变化。运用缓存后,当其他构件发生变化影响到山脉的图像时,我们只需要将缓存中保存的图片重新显示出来就行了,而不需要重新绘制。
*java提供了BufferedImage类供我们使用
太阳
有了山脉,我总觉得还少了点什么,为了让图片更加有意境,我加入了太阳这个元素。但是仅仅是画一个太阳显得很死板,我还想让图像呈现出动态的效果。
· 首先,先确定太阳的画法,使用fillOval()函数,需要注意的是里面的参数是圆外接矩形左上角的点的坐标。每次画出一个太阳之前,都需要将前一个太阳擦除,准确的说是覆盖:用与背景相同的颜色,在前一个太阳所处的位置画圆。
· 然后,就是太阳的轨迹:
我们将轨迹设置为以O为圆心,OA为半径的圆弧,A点的位置可以修改。接着我们设定太阳移动的移动速度sunspeed = 0.5 弧度/次,运动开始相对时间starttime = 35(因为从0开始可能会被挡住,每次运行完+1),我们可以通过三角函数,算出在t时刻P点的坐标(OB - OA* Math.cos(Math.asin(AB/OA) + starttime * sunspeed * PI / 180),600 - 500 * Math.sin(Math.asin(AB/OA) + starttime * sunspeed * PI / 180))
public void showsun() {
// 绘制太阳
// 清除上一个太阳
Color bgcolor = new Color(255, 128, 0);
g.setColor(bgcolor);
g.fillOval((int) (sunx - sunr), (int) (suny - sunr), (int) sunr, (int) sunr);
// 计数
starttime++;
// 计算新太阳的位置
sunx = 400 - 500 * Math.cos(Math.asin(0.6) + starttime * sunspeed * PI / 180);
suny = 600 - 500 * Math.sin(Math.asin(0.6) + starttime * sunspeed * PI / 180);
// 画出太阳
Color suncolor = new Color(255, 0, 0);
g.setColor(suncolor);
g.fillOval((int) (sunx - sunr), (int) (suny - sunr), (int) sunr, (int) sunr);
// 在主画布上画出缓存画布
g.drawImage(buffer, 0, 200, null);
}
最后,我们加入线程休眠
让这段程序每100ms运行一次
while (sun.sunx <= 700) {
// 设置截止条件
// 计时
try {
Thread.sleep(100);
// 设置精度
} catch (InterruptedException ex) {
Logger.getLogger(sun.class.getName()).log(Level.SEVERE, null, ex);
}
// 画出太阳和山脉
sun.showsun();
}
(真不愧是我,数学天才!)
最终运行结果
那么最后就是成品了
尽管可能没有想象中的美观
但掌握这段编程的核心思想还是非常有意义滴
撰文|张振宇
审稿|俞易辰 景笑飏
排版|匡非