65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 编写的带线条/多边形的 and 分形树

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (5投票s)

2020 年 6 月 3 日

CPOL

7分钟阅读

viewsIcon

12555

downloadIcon

208

一种基于几何变换的算法,用于绘制简单的分形树

引言

这是一个简单的 Java 程序,它通过使用线条(经典分形树)或填充的多边形(“升级版”分形树)来绘制分形树。我的想法来自 关于分形的一个维基百科页面

树的绘制方式是,每一迭代,前一迭代的每个形状都会分裂成两个新形状,角度始终相同,并且长度和厚度在每次迭代中乘以一个因子。

该程序包含两个 JFrame

第一个是进行绘制的地方;它由 main 方法控制——重绘在一个无限循环中完成,该循环由一些参数控制,并且调用 paint 方法来绘制树。

这是树的一些截图

还有一个第二个 JFrame——这个里面包含 JRadioButtonJTextBoxJCheckBox 等组件。所有这些组件都接受控制树绘制方式的参数输入。

用法

控件框参数

总共有 11 个参数控制树的绘制。所有更改的参数将在当前绘制完成后立即应用;如果树已经完全绘制完毕,那么一个新绘制循环将启动,并在参数更改后立即使用更改后的参数。

  • 重复 - 决定树是无限循环重绘,还是只绘制一次(并且仅在参数发生变化时重绘)
  • 形状 - 决定绘制树的基本形状(线条或多边形)
  • 颜色 - 打开一个 JColorChooser 控件——它影响树的颜色(仅适用于多边形)
  • 迭代次数 - 决定算法的深度级别(绘制了多少层分支)
  • 厚度(基准) - 树的第一根(基准)分支的厚度
  • 厚度因子 - 每一层分支厚度乘以的因子(接受 > 1,但建议使用 < 1)
  • 长度(基准) - 树的第一根(基准)分支的长度
  • 长度因子 - 每一层分支长度乘以的因子(接受 > 1,但应使用 < 1)
  • 分裂角度 ° - 每个分支分裂成两个下一级分支的角度(以度为单位)
  • 每个形状后暂停 - 表示算法在每个下一个形状之前将暂停输入的毫秒数
  • 每次迭代后暂停 - 表示算法在每个下一个迭代之前将暂停输入的毫秒数

设置参数

在默认设置下(打开应用程序后),树将以线条形式循环绘制。分裂角度为 60°,树将绘制 10 次迭代(分支级别),下一级线条的长度将按 0.8 的因子缩短。默认绘制速度设置为每次迭代之间暂停 500 毫秒。

如果您选择“重复”选项为“”,则树将完成绘制,并且不会进一步重绘。在这种状态下,只有当任何参数发生更改时,树才会重绘。

如果您想尝试修改参数,请注意,程序将在整个树绘制完成后立即采用更改。没有“确认”更改(即“失去焦点”事件或“应用”按钮)。

文本框中的输入没有限制;如果输入了无效值(例如 NaN 值),则在输入有效文本之前,该更改将不被考虑。请注意您在 textbox 中输入的数字,因为如果未考虑某些隐含的逻辑限制,树可能会失真。

  • 非常多的迭代次数可能会减慢应用程序的速度,并且一旦达到更高的迭代次数,就会导致分支过短。
  • 选择基准厚度和长度时,请考虑框架的大小(600x600)。
  • 厚度和长度因子应保持在 1 以下。
  • 不要关闭所有暂停,否则树的绘制速度会非常快,以至于变得不可见。

代码

两个 JFrame 都由 main 方法绘制,但控件框是由自定义类 MyControlFrame 构建的,该类扩展了 JFrame 类。此框架的所有组件,包括操作监听器,都在其构造方法中实例化。

MyControlFrame 类包含 private 变量,用于保存绘制算法中使用的控件参数。这些变量由 main 方法通过调用框架的 public getter 方法进行填充和提取。

int iter = 10;
int thick = 50;
double thickF = 0.6;
int len = 100;
double lenF = 0.8;
double angle = 60;
int sleepEachShapeMillis = 20;
int sleepEachIterMillis = 500;

即,用于获取角度的方法

public double getAngleChoice()
{
    try
    {
        angle = Double.parseDouble(txtAngle.getText());
    }
    catch(NumberFormatException ex)
    {
    }
    return angle;
}

主循环

创建两个框架后,程序会运行一个无限循环,在该循环中,所有变量会从控件框中不断读取,并在每次迭代后与前一个值进行比较。如果参数有任何变化,boolean isChanged 将被设置为 true,告知程序在下一次迭代中应使用新参数重绘树。如果参数 repeat 设置为“”,则树将在主循环的每次迭代中重绘。

boolean isChanged = true;
repeat = control.getRepeatChoice();
shape = control.getShapeChoice();
color = control.getColorChoice();
iter = control.getIterChoice();
thick = control.getThickChoice();
thickFactor = control.getThickFChoice();
len = control.getLenChoice();
lenFactor = control.getLenFChoice();
angle = control.getAngleChoice();
sleepEachShapeMillis = control.getSleepEachShapeChoice();
sleepEachIterMillis = control.getSleepEachIterChoice();
// (re)draw in loop
while(true)
{
    if((repeat == RepeatEnum.YES) || isChanged)
    {
        try {
            frame.repaint();
            paint(panel, shape, color, iter, thick, thickFactor, 
                  len, lenFactor, angle, sleepEachShapeMillis, sleepEachIterMillis);
            Thread.sleep(10);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            break;
            //e.printStackTrace();
        }
        isChanged = false;
    }
    repeat = control.getRepeatChoice();
    // check if any control input was changed
    shapeNEW = control.getShapeChoice();
    colorNEW = control.getColorChoice();
    iterNEW = control.getIterChoice();
    thickNEW = control.getThickChoice();
    thickFactorNEW = control.getThickFChoice();
    lenNEW = control.getLenChoice();
    lenFactorNEW = control.getLenFChoice();
    angleNEW = control.getAngleChoice();
    sleepEachShapeMillisNEW = control.getSleepEachShapeChoice();
    sleepEachIterMillisNEW = control.getSleepEachIterChoice();
    if(shapeNEW != shape || colorNEW != color || iterNEW != iter || 
       thick != thickNEW || thickFactor != thickFactorNEW || len != lenNEW || 
       lenFactor != lenFactorNEW || angle != angleNEW || 
       sleepEachShapeMillis != sleepEachShapeMillisNEW || 
       sleepEachIterMillis != sleepEachIterMillisNEW)
        isChanged = true;
    shape = shapeNEW;
    color = colorNEW;
    iter = iterNEW;
    thick = thickNEW;
    thickFactor = thickFactorNEW;
    len = lenNEW;
    lenFactor = lenFactorNEW;
    angle = angleNEW;
    sleepEachShapeMillis = sleepEachShapeMillisNEW;
    sleepEachIterMillis = sleepEachIterMillisNEW;
}

paint 方法

此方法会在主框架中重绘树。它接收主框架中绘制树的 JPanel 组件作为参数,并且还接收控件框中的所有参数(共 10 个)。

public static void paint(JPanel panel, ShapeEnum shape, Color color, int maxIter, 
int thickness, double thickFactor, int length, double lenFactor, double angle, 
int sleepEachShapeMillis, int sleepEachIterMillis);

该方法首先从面板检索图形上下文,并应用一些渲染提示以使绘图更美观。

Graphics2D g2d = (Graphics2D) panel.getGraphics();
RenderingHints rh = new RenderingHints(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
        );
g2d.addRenderingHints(rh);
g2d.setColor(color);

使用两个 ArrayList 来保存当前分支级别(迭代)的最后一个点集合以及最后一个角度集合。在此上下文中,角度标记了最后一个迭代的每条线与纵线之间的角度(垂直线)。

循环开始前,使用初始参数绘制初始线条(或多边形)。(对于多边形树,初始多边形与其他多边形略有不同(它是矩形而不是梯形)。)

然后,进入一个 for 循环,该循环将运行设定的迭代次数。

首先,调整线条/多边形的长度。

length *= lenFactor;

接下来,在一个 while 循环中读取所有最后一个点,并为每个点绘制两个额外的线条/多边形——一个朝正方向,一个朝负方向。为了实现这一点,角度 ArrayList(theta)中与当前点对应的角度将增加(或减少)delta,即分裂角度的一半。(如果您看上面的图,可以看到对于 60° 的角度,每个后续分支会增加(或减去)30° 的 delta。)

theta = (int)(tmpThetas.get(tmpPoints.indexOf(pt)) + Math.pow(direction, i) * delta);

此时,绘制线条或多边形。

线条上方的点的 xy 参数使用勾股定理计算;因为我们知道上方点的坐标以及需要旋转的角度。

  • Δx = length * cos(90°-theta) = length * sin(theta)
  • Δy = length * sin(90°-theta) = length * cos(theta)
  • x' = x + Δx = x + length * sin(theta)
  • y' = y - Δy = y - length * cos(theta)

对于多边形,每次迭代保存的点是多边形上侧中间的点。

多边形的所有四个点都是通过对上侧中间点应用旋转(就像线条一样),然后将上下侧中间点“向左”或“向右”平移 thickness / 2 来计算的。每层多边形的厚度在每个上侧点的最终变换中进一步减小,以获得梯形。

// create rectangle
// top left point
rect.xpoints[0] = (int)(pt.x + length * 
                  Math.sin(Math.toRadians(theta)));         // rotate
rect.xpoints[0] -= (int)((thickness / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // translate
rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // reduce thickness
rect.ypoints[0] = (int)(pt.y - length * 
                  Math.cos(Math.toRadians(theta)));         // rotate
rect.ypoints[0] -= (int)((thickness / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // translate
rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // reduce thickness
// bottom left point
rect.xpoints[1] = pt.x - (int)((thickness / 2) * 
                  Math.sin(Math.toRadians(90 - theta)));    // translate
rect.ypoints[1] = pt.y - (int)((thickness / 2) * 
                  Math.cos(Math.toRadians(90 - theta)));    // translate
// bottom right point
rect.xpoints[2] = pt.x + (int)((thickness / 2) * 
                  Math.sin(Math.toRadians(90 - theta)));    // translate
rect.ypoints[2] = pt.y + (int)((thickness / 2) * 
                  Math.cos(Math.toRadians(90 - theta)));    // translate
// top right point
rect.xpoints[3] = (int)(pt.x + length * 
                  Math.sin(Math.toRadians(theta)));         // rotate
rect.xpoints[3] += (int)((thickness / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // translate
rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // reduce thickness
rect.ypoints[3] = (int)(pt.y - length * 
                  Math.cos(Math.toRadians(theta)));         // rotate
rect.ypoints[3] += (int)((thickness / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // translate
rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // reduce thickness
rect.invalidate();

最后一步,厚度乘以厚度减小因子。

thickness *= thickFactor;

这是该方法的完整代码

public static void paint(JPanel panel, ShapeEnum shape, Color color, 
       int maxIter, int thickness, double thickFactor, int length, double lenFactor, 
       double angle, int sleepEachShapeMillis, int sleepEachIterMillis) {
 
        // Retrieve the graphics context
    Graphics2D g2d = (Graphics2D) panel.getGraphics();
    RenderingHints rh = new RenderingHints(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON
            );
    g2d.addRenderingHints(rh);
    g2d.setColor(color);
    
    int w = panel.getSize().width;
    int h = panel.getSize().height;
    
    boolean sleepEachShape = sleepEachShapeMillis == 0 ? false : true;
    boolean sleepEachIter = sleepEachIterMillis == 0 ? false : true;
    
    double delta, theta;
    
    Line2D line = new Line2D.Double();
    Polygon rect = new Polygon();
    Point pt = new Point();
    Point pt2;
    ArrayList<Point> startPoints, tmpPoints;
    ArrayList<Double> thetas, tmpThetas;
    
    Iterator<Point> listIterator;
    int direction = -1;
    
    delta = angle / 2;
    theta = 0;
    
    startPoints = new ArrayList<Point>();
    tmpPoints = new ArrayList<Point>();
    thetas = new ArrayList<Double>();
    tmpThetas = new ArrayList<Double>();
    
    // draw initial rectangle
    if(sleepEachIter)
        try {
            Thread.sleep(sleepEachIterMillis);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    rect.npoints = 4;
    rect.xpoints = new int[4];
    rect.ypoints = new int[4];
    rect.xpoints[0] = w / 2 - thickness / 2;
    rect.xpoints[1] = w / 2 + thickness / 2;
    rect.xpoints[2] = w / 2 + thickness / 2;
    rect.xpoints[3] = w / 2 - thickness / 2;
    rect.ypoints[0] = h;
    rect.ypoints[1] = h;
    rect.ypoints[2] = (int)(h - length);
    rect.ypoints[3] = (int)(h - length);
    if(shape == ShapeEnum.LINE)
    {
        line.setLine(w / 2, h, w / 2, h - length);
        g2d.drawLine((int)line.getX1(), 
        (int)line.getY1(), (int)line.getX2(), (int)line.getY2());
    }
    else
    {
        g2d.fillPolygon(rect);
    }
    startPoints.add(new Point(w / 2, (int)(h - length)));
    thetas.add(theta);
    
    direction = -1;
    for(int iteration = 0; iteration < maxIter; iteration++)
    {
        if(sleepEachIter)
            try {
                Thread.sleep(sleepEachIterMillis);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        length *= lenFactor;
        tmpPoints = startPoints;
        tmpThetas = thetas;
        startPoints = new ArrayList<Point>();
        thetas = new ArrayList<Double>();
        listIterator = tmpPoints.iterator();
        while(listIterator.hasNext())     // for all starting points
        {
            pt = listIterator.next();
            rect = new Polygon();
            line = new Line2D.Double();
            rect.npoints = 4;
            for(int i = 0; i < 2; i++)    // for both directions
            {
                // raise the rotation angle from previous rotation by delta
                theta = (int)(tmpThetas.get(tmpPoints.indexOf(pt)) + 
                        Math.pow(direction, i) * delta);
                if(shape == ShapeEnum.LINE)
                {
                    pt2 = new Point();
                    pt2.x = (int)(pt.x + length * Math.sin(Math.toRadians(theta)));
                    pt2.y = (int)(pt.y - length * Math.cos(Math.toRadians(theta)));
                    line.setLine(pt.x, pt.y, pt2.x, pt2.y);
                }
                else
                {
                    rect.xpoints = new int[4];
                    rect.ypoints = new int[4];
                    
                    // create rectangle
                    // top left point
                    rect.xpoints[0] = (int)(pt.x + length * 
                                      Math.sin(Math.toRadians(theta)));       // rotate
                    rect.xpoints[0] -= (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.ypoints[0] = (int)(pt.y - length * 
                                       Math.cos(Math.toRadians(theta)));      // rotate
                    rect.ypoints[0] -= (int)((thickness / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // translate
                    rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    // bottom left point
                    rect.xpoints[1] = pt.x - (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.ypoints[1] = pt.y - (int)((thickness / 2) * 
                                      Math.cos(Math.toRadians(90 - theta)));  // translate
                    // bottom right point
                    rect.xpoints[2] = pt.x + (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.ypoints[2] = pt.y + (int)((thickness / 2) * 
                                      Math.cos(Math.toRadians(90 - theta)));  // translate
                    // top right point
                    rect.xpoints[3] = (int)(pt.x + length * 
                                       Math.sin(Math.toRadians(theta)));      // rotate
                    rect.xpoints[3] += (int)((thickness / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // translate
                    rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.ypoints[3] = (int)(pt.y - length * 
                                      Math.cos(Math.toRadians(theta)));       // rotate
                    rect.ypoints[3] += (int)((thickness / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // translate
                    rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.invalidate();
                }
                
                // save the new starting point
                startPoints.add(new Point(
                                    (int)(pt.x + length * Math.sin(Math.toRadians(theta))),
                                    (int)(pt.y - length * Math.cos(Math.toRadians(theta)))
                                ));
                thetas.add(theta);
                
                if(sleepEachShape)
                    try {
                        Thread.sleep(sleepEachShapeMillis);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                if(shape == ShapeEnum.LINE)
                {
                    g2d.drawLine((int)line.getX1(), (int)line.getY1(), 
                                 (int)line.getX2(), (int)line.getY2());
                }
                else
                {
                    // draw the rectangle
                    g2d.fillPolygon(rect);
                    g2d.draw(rect);
                }
            }
        }
        thickness *= thickFactor; // change thickness
    }
}

历史

  • 2020 年 6 月 3 日:初始版本
© . All rights reserved.