递归分形--山脉
繪制面板
public class DrawPanel extends JFrame{static Graphics g;ImageObserver imageObserver;public Graphics getG() {return g;}public void ShowUi() {//流式布局this.setLayout(new FlowLayout());Button button1 = new Button("山脈");Button button2 = new Button("三角");Button button3 = new Button("矩形");Button button4 = new Button("3D");Dimension dimension = new Dimension(20,20);button1.setSize(dimension);button2.setSize(dimension);button3.setSize(dimension);button4.setSize(dimension);this.setSize(600, 500);this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);this.setLocationRelativeTo(null);this.add(button1);this.add(button2);this.add(button3);this.add(button4);this.setVisible(true);//得到畫布,一定要設置在可見之后this.g = this.getGraphics();//每種圖形的動作監聽ButtonAction buttonAction = new ButtonAction(this.g, this);Triangle triangle = new Triangle(g);Rect rect = new Rect(g);Draw3D draw3D = new Draw3D(g);button1.addActionListener(buttonAction);button2.addActionListener(triangle);button3.addActionListener(rect);button4.addActionListener(draw3D);}public void paint(Graphics g){super.paint(g);Color c = new Color(9, 7, 7, 60);g.setColor(c);g.fillRect(0,0, this.getWidth(), this.getHeight());Color color = new Color(129, 62, 19, 255);Font font = new Font("楷體", Font.BOLD, 20);g.setFont(font);g.setColor(color);String s = "Coding like poetry";g.drawString(s, 200, 100);}注意:
- button需要流式布局,傳入dimension確定大小
- Graphics對象從JFrame中獲取
綁定按鍵事件
其中以下動作是綁定按鍵事件:
ButtonAction buttonAction = new ButtonAction(this.g, this);Triangle triangle = new Triangle(g);Rect rect = new Rect(g);Draw3D draw3D = new Draw3D(g);button1.addActionListener(buttonAction);button2.addActionListener(triangle);button3.addActionListener(rect);button4.addActionListener(draw3D);先創建每個監聽器對象,其中每個對象都繼承自ActionListener,并且傳入一個Graphics對象用于畫圖
paint方法
用于繪制畫布,設置背景文字等
隨著窗口的加載而加載,也就是說在拖動窗口時,會再次調用此方法
繪制山脈RandomNoise
public class RandomNoise {private int[] xLabel;private int[] yLabel;public void draw(Graphics g, int startX, int endX, int startY, int endY, int deep, int range) {range *= 0.62;//定義填充xLabel = new int[] {startX, endX, startX,endX,startX };yLabel = new int[] {startY, endY, 500, 500, startY};if(deep-- < 0) {Color color = new Color(71, 76, 56, 100);g.setColor(color);g.drawLine(startX, startY, endX, endY);g.fillPolygon(xLabel, yLabel, 5);return;}//count和遞歸次數的關系int midX = startX + ((endX - startX) / 2);int midY = startY + ((endY - startY) / 2);//取得隨機數,記錄遞歸次數midY = swift(midY, range);//左邊draw(g, startX, midX, startY, midY, deep, range);//右邊draw(g, midX, endX, midY, endY, deep,range);}public int swift(int y, double count) {return y += (Math.random() * 2-1) * count;} }注意:
- 在遞歸函數壓棧的時候,是劃分點
- 基條件內些了畫圖方法,所以彈棧的時候是開始劃線
效果:
繪制三角形
遞歸思想兩大要點:
三角形demo:
public void drawTri(Graphics g, int x1, int y1, int x2, int y2, int x3, int y3,int deep, double range, boolean flag) {xLabel = new int[] {x1, x2, x3};yLabel = new int[] {y1, y2, y3};int swiftY1 = 0;int swiftY2 = 0;int swiftY3 = 0;if(deep-- < 0) {Color color = new Color(31, 191, 167, 190);g.fillPolygon(xLabel, yLabel, 3);g.setColor(color);g.drawPolygon(xLabel, yLabel, 3);return;}range *= 0.62;count++;//通過初始點,和長度計算三個點//取中點,x三個,y一個int midX1 = (x1 + x2) / 2;int midX2 = (x2 + x3) / 2;int midX3 = (x3 + x1) / 2;int midY1 = (y1 + y2) / 2;int midY2 = (y2 + y3) / 2;int midY3 = (y3 + y1) / 2;//通過震蕩,如果已經被劃分過,則不用劃分,檢測是否被劃分的機制是什么?if (flag) {//拿到之前的中點,如何拿到之前的中點?swiftY1 = midY1;swiftY2 = midY2;swiftY3 = midY3;} else{swiftY1 = swift(midY1, range);swiftY2 = swift(midY2, range);swiftY3 = swift(midY3, range);}drawTri(g, x1, y1, midX1, swiftY1, midX3, swiftY3, deep, range, false);drawTri(g, midX1, swiftY1, x2, y2, midX2, swiftY2, deep, range,false);drawTri(g, midX3, swiftY3, midX2, swiftY2, x3, y3, deep, range, false);//需要遞歸中間三角, 這樣的子問題不一樣 // drawTri(g, midX2, swiftY2, midX3, swiftY3, midX1, swiftY1, deep, range, true);}問題:遞歸中間的倒三角形
沒有遞歸中間的倒三角:
倒三角遞歸的時候,問題在于三角形邊中點已經被處理了,再處理必然會導致兩次結果不相同:
導致中間有斷層
初步解決辦法:
給倒三角遞歸的一部分打上標記。即,flag = true。
正三角flag = false
通過查看遞歸過程,在到倒三角遞歸時,依然在選取中點,震蕩值。所以會造成中間的誤差。
如何在此次遞歸時能夠調用之前已經震蕩好的值?
- 維護點和邊之間的關系。
方案一:map
if(flag){swiftY1 = map.get(midX1);swiftY2 = map.get(midX2);swiftY3 = map.get(midX3);} else{swiftY1 = swift(midY1,range);swiftY2 = swift(midY2,range);swiftY3 = swift(midY3,range);map.put(midX1,swiftY1);map.put(midX2,swiftY2);map.put(midX3,swiftY3);}把X中點和處理后的Y點通過映射確定。
map.put(midX1,swiftY1);map.put(midX2,swiftY2);map.put(midX3,swiftY3);通過遞歸函數確定倒三角形的midX1就是x1 和 y1的值
drawTri(g, midX2, swiftY2, midX3, swiftY3, midX1, swiftY1, deep, range, true);public void drawTri(Graphics g, int x1, int y1, int x2, int y2, int x3, int y3,int deep, double range, boolean flag) {問題:取某條邊的中點是不準確的,可能會有兩個相等的中點,此時哈希沖突了,覆蓋了原來的值,任然會有空隙。
改進:方案二 - 邊對象作為Key
問題1:在存入對象時,new一個新對象放在map中,但是如何取出對象?
問題2:在遞歸倒三角的時候,flag為true,但是倒三角中還有正三角,那么此時就無法遞歸正三角,
所以取消標志位flag
第一層是沒有影響的,因為傳入的時候flag為true;但是到了第二層沒有辦法遞歸正的三角形。
檢測核心代碼如下:
if(map.containsKey(lineObject.getLine(x1, y1, x2, y2) )) {swiftY1 = map.get(lineObject.getLine(x1, y1, x2, y2));} else if (map.containsKey(lineObject.getLine(x2, y2, x3, y3))) {swiftY2 = map.get(lineObject.getLine(x2, y2, x3, y3));} else if(map.containsKey(lineObject.getLine(x2, y2, x3, y3)) ) {swiftY3 = map.get(lineObject.getLine(x3, y3, x1, y1));} else{swiftY1 = swift(midY1,range);swiftY2 = swift(midY2,range);swiftY3 = swift(midY3,range);}//邊對象對應,震蕩點//存入邊對象map.put(new LineObject(x1, y1, x2, y2), swiftY1);map.put(new LineObject(x2, y2, x3, y3), swiftY2);map.put(new LineObject(x3, y3, x1, y1), swiftY3);問題3:
map.containsKey(lineObject.getLine(x1, y1, x2, y2)檢查的是對象地址而不是中間的值,所以判斷會跳過:
最外層的點確定好以后,開始遞歸頂三角,然后頂三角中的倒三角,此時倒三角傳入的值是已經處理過的,
問題4:鍵的匹配出現問題,該如何查找map中的鍵 ?
此時鍵是一個LineObject對象,map如何查找?
通過迭代器遍歷鍵對象,再比較四個點:
public boolean verify(int x1, int y1, int x2, int y2) {Iterator<Object> iterator = map.keySet().iterator();while(iterator.hasNext()) {LineObject next = (LineObject) iterator.next();if(next.x1 == x1 && next.x2 == x2 && next.y1 == y1 && next.y2 == y2) {shiftY = map.get(next);return true;}}return false;}在遞歸函數中的校驗部分如下:
if(verify(x2, y2, x1, y1)){shiftY1 = shiftY;} else{shiftY1 = shift(midY1,range);}if (verify(x3, y3, x2, y2)) {shiftY2 = shiftY;} else {shiftY2 = shift(midY2,range);}if(verify(x1, y1, x3, y3)) {shiftY3 = shiftY;} else{shiftY3 = shift(midY3,range);}此時傳入的點一定要對應。
完成效果
效果如圖:
改進問題4
在問題4的改進方法中,用迭代器遍歷鍵,效率是非常低下的。
在遞歸層數逐漸增加的時候,山脈出現的速度會非常慢
在Java編程思想中有這樣一句話:
當鍵為一個對象時,一定要重寫hashCode和equals方法
也就是說一定要通過鍵對象去查找值,如果是迭代器遍歷,那么用HashMap來做什么呢?
所以重寫hashCode和equals:
@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;LineObject that = (LineObject) o;return x1 == that.x1 &&y1 == that.y1 &&x2 == that.x2 &&y2 == that.y2;}@Overridepublic int hashCode() {return Objects.hash(x1, y1, x2, y2);}再修改校驗機制:
public boolean verify(int x1, int y1, int x2, int y2) {LineObject lineObject = new LineObject(x1, y1, x2, y2);if(map.containsKey(lineObject)){shiftY = map.get(lineObject);return true;}return false;}此時可以看到和之間的不同就是
- 新建一個對象,拿這個對象去查找;而不是用迭代器去比較,時間復雜度大大降低
初步修改參數(遞歸層數,去除填充)得到的圖形如下:
山脈群
通過拼接多個三角形就能形成圖下效果:
各位小伙伴多多交流 😃
個人網站:blog.vanguo996.cn
vx:
總結
- 上一篇: Swift如何实现与JSON互转
- 下一篇: Excel图表制作(二):单选框和下拉菜