6
\$\begingroup\$

Here's my attempt to draw Koch snowflake. My initial state is a line instead of an equilateral triangle so that I am able to fit maximum detail in a mobile phone window. My thought process was:

  • Start with two points A, B separated distance r.
  • Draw three points between them, P1, P2, P3.
  • P1 at distance r/3 from A on line AB.
  • P2 at r/3 from the P1 at 60 degrees from the AB.
  • P3 at 2*r/3 from A on line AB.

Code

public class KochView extends View {

    List<PointF> points = new LinkedList<>();
    private int depth = 6;

    public KochView(Context context) {
        super(context);
    }

    public KochView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }

    @Override
    protected void onDraw(final Canvas canvas) {

        final int H = getHeight();
        final int W = getWidth();

        final Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setAlpha((int) (255 * 0.99));
        paint.setStrokeWidth(1.0f);

        // Adjust start/end points for the Koch 'snowflake' depending on whether we are in
        // portrait...
        if (H > W) {
            points.add(new PointF(0.05f * W, 0.01f * H));
            points.add(new PointF(0.05f * W, 0.99f * H));
        }// or landscape mode...
        else {
            points.add(new PointF(0.01f * W, 0.95f * H));
            points.add(new PointF(0.99f * W, 0.95f * H));
        }

        // The current algo does not draw start and end points, so we draw them manually...
        drawPoint(canvas, paint, points.get(0));
        drawPoint(canvas, paint, points.get(1));

        doPairWise(points, new Action<PointF>() {
            @Override
            public List<PointF> exec(PointF first, PointF second) {

                // Find the distance between the given two points
                double r = dist(first, second);

                // And the angle they subtend w.r.t the x-axis
                float th = (float) (Math.atan2(second.y - first.y, second.x - first.x));

                // Get the first point according the Koch snowflake iteration definition. I.e. at
                // 1/3 rd the distance, on the line joining th two points.
                PointF p1 = getPoint(first, (float) (r / 3), th);
                // Get the second point, at 1/3 distance from the *first* point, at an angle of
                // 60 degrees from the line joining the given points,
                PointF p2 = getPoint(p1, (float) (r / 3), (float) (th - PI / 3));
                // Get the third point, at 2/3 the distance, on the line joining the given points.
                PointF p3 = getPoint(first, (float) (2 * r / 3), th);

                // Draw all three...
                drawPoint(canvas, paint, p1);
                drawPoint(canvas, paint, p2);
                drawPoint(canvas, paint, p3);

                // Add them to a list...
                List<PointF> more = new LinkedList<>();
                more.add(p1);
                more.add(p2);
                more.add(p3);

                // Return the list of points for further iteration
                return more;
            }
        });

    }

    /**
     * Get a point at radial distance 'r' and a given angle 't' w.r.t a given point.
     *
     * @param src The source point.
     * @param r   The radial distance in pixels.
     * @param t   The angle in radians.
     * @return The new point.
     */
    private PointF getPoint(PointF src, float r, float t) {
        PointF result = new PointF();
        result.x = (int) (src.x + r * Math.cos(t));
        result.y = (int) (src.y + r * Math.sin(t));
        return result;
    }

    /**
     * Recursively do an action on consecutive pair from a list. Here the action is drawing three
     * more points in between them to plat a Koch snowflake.
     *
     * @param list   The list of items to make the pairs from.
     * @param action The action to do. In this case 'action' should draw the points on the canvas
     *               and return them for calculation of future iterations.
     */
    private void doPairWise(List<PointF> list, Action<PointF> action) {

        // New points are added to this list alongwith existing ones and this is used as the list
        // of points for the next iteration.
        List<PointF> tmp = new LinkedList<>();

        // Depth to terminate the recursion at
        if (--depth <= 0) {
            return;
        }

        int size = list.size();

        for (int i = 0; i < size - 1; i += 1) {
            final List<PointF> more = action.exec(list.get(i), list.get(i + 1));
            // Add the starting point to the list of all points...
            tmp.add(list.get(i));
            // ...add the three points in the iteration of a Koch snowflake.
            tmp.addAll(more);
        }

        // finally add the last point to all points.
        tmp.add(list.get(size - 1));

        // Repeat the above process with the new list and the same "action"
        doPairWise(tmp, action);
    }

    private interface Action<T> {
        /**
         * Do something with a pair of objects. Optionally, return a list of the same type.
         *
         * @param first  First object to do smth with.
         * @param second Second object to smth with.
         * @return Optionally return a list of same type as inputs.
         */
        List<T> exec(T first, T second);
    }

    /**
     * Convenience method. to draw a point on a canvas with a give paint.
     *
     * @param canvas The canvas to draw the point on.
     * @param paint  The paint to draw the point with.
     * @param point  The point to draw.
     */
    private static void drawPoint(Canvas canvas, Paint paint, PointF point) {
        canvas.drawPoint(point.x, point.y, paint);
    }

    /**
     * Returns the Euclidean distance between two points.
     *
     * @param p1 First point.
     * @param p2 Second point.
     * @return The distance, as the crow flies, between the given points.
     */
    private static double dist(PointF p1, PointF p2) {
        return dist(p1.x, p2.x, p1.y, p2.y);
    }

    private static double dist(double x1, double x2, double y1, double y2) {
        return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    }
}

Output:

Depth = 6

Depth = 6

Depth = 8

Depth = 8

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Some minor issues, nothing serious...

  • Try to calculate the points before you draw and store them to easily redraw them (and not calculate over again).

  • What's the purpose of the Action interface, what does exec do? A more suitable name would be really helpful (maybe IntersectingArea or addBranches - but my english is bad., do snow flakes have branches or arms or... I dont know).

  • Additionally why do you use a generic Action<> interface - it's absouletly bound to be used for Points (or PointF) and I do not see any usage in any other types of parameters.

  • Maybe you should get away totally from that interface and declare class, because i don't see any other usage for that interface. it's for your implementation only, it's specific for your cause, hence make a class (no interface). especially when you read the documentation of the interface: ' do something with a pair of objects ' (???).

  • Why do you cast the destination of the translation of a PointF into int in the method getPoint() (and why don't you rename it into translateRadial) ... you can still cast it down into int when you're drawing the point, so cast it down to float .

  • I do not know why you declare a method drawPoint() when there is no difference between the usage in onDraw() - make paint and canvas member vairables and change the method into:

Code:

private void drawPoint(PointF p) {
    canvas.drawPoint(point.x, point.y, paint); //canvas is member, paint is member
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.