/* * Copyright 2017 Laszlo Balazs-Csiki * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License, version 3 as published by the Free * Software Foundation. * * Pixelitor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>. */ package pixelitor.filters; import net.jafama.FastMath; import pixelitor.filters.gui.GradientParam; import pixelitor.filters.gui.GroupedRangeParam; import pixelitor.filters.gui.IntChoiceParam; import pixelitor.filters.gui.IntChoiceParam.Value; import pixelitor.filters.gui.ParamSet; import pixelitor.filters.gui.RangeParam; import pixelitor.filters.gui.ShowOriginal; import pixelitor.utils.BasicProgressTracker; import pixelitor.utils.ProgressTracker; import pixelitor.utils.ReseedSupport; import java.awt.BasicStroke; import java.awt.Color; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.util.Random; import static pixelitor.filters.gui.RandomizePolicy.IGNORE_RANDOMIZE; /** * Renders a fractal tree */ public class FractalTree extends FilterWithParametrizedGUI { public static final String NAME = "Fractal Tree"; private static final Color BROWN = new Color(140, 100, 73); private static final Color GREEN = new Color(31, 125, 42); private static final int QUALITY_BETTER = 1; private static final int QUALITY_FASTER = 2; private final RangeParam iterations = new RangeParam("Age (Iterations)", 1, 10, 17); private final RangeParam angle = new RangeParam("Angle", 1, 20, 45); private final RangeParam randomnessParam = new RangeParam("Randomness", 0, 40, 100); private final GroupedRangeParam width = new GroupedRangeParam("Width", new RangeParam[]{ new RangeParam("Overall", 100, 100, 300), new RangeParam("Trunk", 100, 200, 500), }, false); private final RangeParam zoom = new RangeParam("Zoom", 10, 100, 200); private final RangeParam curvedness = new RangeParam("Curvedness", 0, 10, 50); private final GroupedRangeParam physics = new GroupedRangeParam("Physics", "Gravity", "Wind", -100, 0, 100, false); private final IntChoiceParam quality = new IntChoiceParam("Quality", new Value[]{ new Value("Better", QUALITY_BETTER), new Value("Faster", QUALITY_FASTER) }, IGNORE_RANDOMIZE); // precalculated objects for the various depths private Stroke[] widthLookup; private Color[] colorLookup; private Physics[] physicsLookup; private boolean doPhysics; private boolean leftFirst; private boolean hasRandomness; private final GradientParam colors = new GradientParam("Colors", new float[]{0.25f, 0.75f}, new Color[]{BROWN, GREEN}, IGNORE_RANDOMIZE); private double defaultLength; private double randPercent; private double lengthDeviation; private double angleDeviation; private ProgressTracker pt; public FractalTree() { super(ShowOriginal.NO); setParamSet(new ParamSet( iterations, zoom, randomnessParam, curvedness, angle, physics.setLinkable(false), width.setLinkable(false), colors, quality ).withAction(ReseedSupport.createAction())); } @Override public BufferedImage doTransform(BufferedImage src, BufferedImage dest) { ReseedSupport.reInitialize(); Random rand = ReseedSupport.getRand(); leftFirst = true; defaultLength = zoom.getValue() / 10.0; randPercent = randomnessParam.getValue() / 100.0; hasRandomness = randomnessParam.getValue() > 0; lengthDeviation = defaultLength * randPercent; angleDeviation = 10.0 * randPercent; Graphics2D g = dest.createGraphics(); if (quality.getValue() == QUALITY_BETTER) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } int maxDepth = iterations.getValue(); widthLookup = new Stroke[maxDepth + 1]; colorLookup = new Color[maxDepth + 1]; int gravity = physics.getValue(0); int wind = physics.getValue(1); if (gravity != 0 || wind != 0) { doPhysics = true; physicsLookup = new Physics[maxDepth + 1]; } else { doPhysics = false; physicsLookup = null; } for (int depth = 1; depth <= maxDepth; depth++) { float w1 = depth * width.getValueAsPercentage(0); double trunkWidth = (double) width.getValueAsPercentage(1); double base = Math.pow(trunkWidth, 1.0 / (maxDepth - 1)); double w2 = Math.pow(base, depth - 1); float strokeWidth = (float) (w1 * w2); float zoomedStrokeWidth = (strokeWidth * zoom.getValue()) / zoom.getDefaultValue(); widthLookup[depth] = new BasicStroke(zoomedStrokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); // colors float where = ((float) depth) / iterations.getValue(); int rgb = colors.getValue().getColor(1.0f - where); colorLookup[depth] = new Color(rgb); if (doPhysics) { physicsLookup[depth] = new Physics(gravity, wind, strokeWidth); } } float c = curvedness.getValueAsPercentage(); if (rand.nextBoolean()) { c = -c; } int drawTreeCalls = 2; for (int i = 1; i < maxDepth; i++) { drawTreeCalls *= 2; } drawTreeCalls--; pt = new BasicProgressTracker(NAME, drawTreeCalls); drawTree(g, src.getWidth() / 2.0, src.getHeight(), 270 + calcAngleRandomness(rand), maxDepth, rand, c); g.dispose(); pt.finish(); return dest; } private void drawTree(Graphics2D g, double x1, double y1, double angle, int depth, Random rand, float c) { if (depth == 0) { return; } int nextDepth = depth - 1; c = -c; // change the direction of the curvature in each iteration if (doPhysics) { angle = adjustPhysics(angle, depth); } double angleRad = Math.toRadians(angle); double x2 = x1 + FastMath.cos(angleRad) * depth * calcRandomLength(rand); double y2 = y1 + FastMath.sin(angleRad) * depth * calcRandomLength(rand); g.setStroke(widthLookup[depth]); if (quality.getValue() == QUALITY_BETTER) { if (depth == 1) { g.setColor(colorLookup[depth]); } else { g.setPaint(new GradientPaint( (float) x1, (float) y1, colorLookup[depth], (float) x2, (float) y2, colorLookup[(nextDepth)])); } } else { g.setColor(colorLookup[depth]); } connectPoints(g, x1, y1, x2, y2, c); int split = this.angle.getValue(); double leftBranchAngle = angle - split + calcAngleRandomness(rand); double rightBranchAngle = angle + split + calcAngleRandomness(rand); pt.unitDone(); leftFirst = !leftFirst; if (leftFirst) { drawTree(g, x2, y2, leftBranchAngle, nextDepth, rand, c); drawTree(g, x2, y2, rightBranchAngle, nextDepth, rand, c); } else { drawTree(g, x2, y2, rightBranchAngle, nextDepth, rand, c); drawTree(g, x2, y2, leftBranchAngle, nextDepth, rand, c); } } private double adjustPhysics(double angle, int depth) { assert doPhysics; // make sure we have the angle in the range 0-360 angle += 720; angle = angle % 360; Physics p = physicsLookup[depth]; if (angle < 90) { angle += (90 - angle) * p.gravityStrength; angle -= (angle / 90.0) * p.windStrength; } else if (angle < 180) { angle -= (angle - 90) * p.gravityStrength; angle -= (180 - angle) * p.windStrength; } else if (angle < 270) { angle -= (270 - angle) * p.gravityStrength; angle += (angle - 180) * p.windStrength; } else if (angle <= 360) { angle += (angle - 270) * p.gravityStrength; angle += (360 - angle) * p.windStrength; } else { throw new IllegalStateException("angle = " + angle); } return angle; } private static void connectPoints(Graphics2D g, double x1, double y1, double x2, double y2, float c) { if (c == 0) { Line2D.Double line = new Line2D.Double(x1, y1, x2, y2); g.draw(line); } else { Path2D.Double path = new Path2D.Double(); path.moveTo(x1, y1); double dx = x2 - x1; double dy = y2 - y1; // center point double cx = x1 + dx / 2.0; double cy = y1 + dy / 2.0; // We calculate only one Bezier control point, // and use it for both. // The normal vector is -dy, dx. double ctrlX = cx - dy * c; double ctrlY = cy + dx * c; path.curveTo(ctrlX, ctrlY, ctrlX, ctrlY, x2, y2); g.draw(path); } } private double calcAngleRandomness(Random rand) { if (!hasRandomness) { return 0; } return -angleDeviation + rand.nextDouble() * 2 * angleDeviation; } private double calcRandomLength(Random rand) { if (!hasRandomness) { return defaultLength; } double minLength = defaultLength - lengthDeviation; return (minLength + 2 * lengthDeviation * rand.nextDouble()); } private static class Physics { public final double gravityStrength; public final double windStrength; private Physics(int gravity, int wind, float strokeWidth2) { double effectStrength = 0.02 / strokeWidth2; gravityStrength = effectStrength * gravity; windStrength = effectStrength * wind; } } }