Wednesday, June 1, 2022

Paints and fills

The Java 2D graphics API has three main types of paints: ordinary colors, gradients, and texture paints. I will demonstrate texture paints by creating using some stars.

Drawing some stars

In order to draw a star I had the idea to first draw a polygon, then around each of the lines in the polygon create a line going away from it relative to the center that is perpindicular to it with respect to the midpoint. The first class computes the perpindicular lines from the midpoint.
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;

public class OrdinaryLine {
    double m;
    double b;

    public OrdinaryLine(double m, double b) {
        if (m == 0) {
            System.out.println(m);
        }

        this.m = m;
        this.b = b;
    }

    public static double slope(Line2D.Double line) {
        return (line.getY2() - line.getY1()) /
                (line.getX2() - line.getX1());
    }

    public static double yIntercept(Line2D.Double line) {
        var m = slope(line);
        return line.getY1() - m*line.getX1();
    }

    public OrdinaryLine(Line2D.Double line) {
        this.m = slope(line);
        this.b = yIntercept(line);
    }

    public String toString() {
        return "y = " + Double.toString(this.m) + "x + " + Double.toString(this.b);
    }

    public OrdinaryLine perpindicular(Point2D.Double containedPoint) {
        var slope = -(1/this.m);
        var yIntercept = containedPoint.y - slope* containedPoint.x;
        return new OrdinaryLine(slope, yIntercept);
    }

    public double getY(double x) {
        return this.m*x + this.b;
    }

    // y = mx+b
    // x = (y-b)/m
    public double getX(double y) {
        return (y - this.b) / this.m;
    }

    public Point2D.Double getPointByX(double x) {
        return new Point2D.Double(x, getY(x));
    }

    public Point2D.Double getPointByY(double y){
        return new Point2D.Double(getX(y), y);
    }

    public static Point2D.Double midpoint(Line2D line) {
        return new Point2D.Double(
                (line.getX2() + line.getX1()) / 2,
                (line.getY2() + line.getY1()) / 2
        );
    }

    public static OrdinaryLine getPerpendicularLine(Line2D.Double line) {
        return (new OrdinaryLine(line)).perpindicular(midpoint(line));
    }

    public static Point2D.Double getUnitVectorByPoints(Point2D.Double point1, Point2D.Double point2) {
        Point2D.Double v = new Point2D.Double(point1.getX() - point2.getX(), point1.getY() - point2.getY() );
        var length = Math.sqrt(v.x * v.x + v.y * v.y);
        return new Point2D.Double(v.getX()/length, v.getY()/length);
    }

    public Point2D.Double directionVector() {
        return getUnitVectorByPoints(this.getPointByX(1), this.getPointByX(0));
    }

    public static Point2D.Double getPointAwayFrom(Line2D.Double line, Point2D.Double center, double distance) {
        var mid = OrdinaryLine.midpoint(line);

        Point2D.Double point1;
        Point2D.Double point2;

        // vertical and horizontal lines are not ordinary lines
        if (line.getY1() == line.getY2()) {
            point1 = new Point2D.Double(mid.x, mid.y + distance);
            point2 = new Point2D.Double(mid.x, mid.y - distance);
        } else if (line.getX1() == line.getX2()) {
            point1 = new Point2D.Double(mid.x + distance, mid.y);
            point2 = new Point2D.Double(mid.x - distance, mid.y);
        } else {
            var p = OrdinaryLine.getPerpendicularLine(line);
            var v = p.directionVector();
            point1 = new Point2D.Double(mid.x + distance * v.x, mid.y + distance * v.y);
            point2 = new Point2D.Double(mid.x - distance * v.x, mid.y - distance * v.y);
        }

        return (point1.distance(center) > point2.distance(center)) ? point1 : point2;
    }

}

The second utility class computes the array of points of vertices in a polygon using familiar trigonometry and the methods in the Math class.
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;

public class PolygonData {
    public static Point2D.Double[] points(int vertices, Rectangle bounds) {
        double step = 2 * Math.PI / vertices;
        double xradius = bounds.getWidth()/2;
        double yradius = bounds.getHeight()/2;
        var rval = new Point2D.Double[vertices];

        for(int i = 0; i < vertices; i++) {
            rval[i] = new Point2D.Double(
                    bounds.x + xradius + Math.cos(i * step) * xradius,
                    bounds.y + yradius + Math.sin(i * step) * yradius
            );
        }

        return rval;
    }

    public static Line2D.Double[] lines(int vertices, Rectangle bounds) {
        var points = points(vertices, bounds);
        var lines = new Line2D.Double[vertices];

        for(int i = 0; i < points.length; i++) {
            lines[i] = (i == points.length-1) ? new Line2D.Double(points[i], points[0]) : new Line2D.Double(points[i], points[i+1]);
        }

        return lines;
    }
}

The actual fun of using Java 2D comes with the class below which is responsible for drawing the stars.
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;

public class StarCreator {
    public static BufferedImage star() {
        var img = new BufferedImage(800,800, BufferedImage.TYPE_INT_ARGB);
        var g = img.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // do the drawing
        var center = new Point2D.Double(400,400);

        var lines = PolygonData.lines(5, new Rectangle(250,250,350, 350));
        Path2D path = new Path2D.Double();

        for(Line2D.Double line : lines) {
            var chosenPoint = OrdinaryLine.getPointAwayFrom(line, center, 250);
            path.append(new Line2D.Double(line.getP1(), chosenPoint), true);
            path.append(new Line2D.Double(line.getP2(), chosenPoint), true);
        }

        g.setPaint(
                new RadialGradientPaint(
                        new Point2D.Float((float) 0.5*800, (float)0.5*800),
                        400.0f,
                        new float[]{ 0.0f, 1.0f},
                        new Color[]{new Color(0x00FFFF), new Color(0xBF00FF)}
                )
        );

        g.fill(path);

        g.dispose();
        return img;
    }
}

You can tune the parameters in the StarCreator however you want to create different kinds of shapes and different graphics, such as stars with more sides like below. In particular, this can be utilised to create a starry texture.

Using texture paints

The next example demonstrates the use of TexturePaint.
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;

public class Application {

    public static BufferedImage makeImage() {
        var img = new BufferedImage(600,600, BufferedImage.TYPE_INT_ARGB);
        var g = img.createGraphics();
        var s = StarCreator.star();

        var texturePaint = new TexturePaint(s,new Rectangle(0,0,150,150));
        g.setPaint(texturePaint);
        g.fillRect(0,0,800,800);

        g.dispose();
        return img;
    }

    public static void main(String[] args) {
        var img = StarCreator.star();
        var f = new JFrame();
        f.setTitle("Image viewer");
        f.setSize(img.getWidth() + 100, img.getHeight() + 100);
        f.setLocationRelativeTo(null);
        var label = new JLabel(new ImageIcon(img));
        f.add(label);
        f.setVisible(true);
    }

}

Here is the desired picture containing a bunch of stars.
I can think of a few other cases you could use TexturePaint. There is a lot that can be done with the Java 2D API. As its still part of the Java standard library, I make it my business to familiarize myself with it.

No comments:

Post a Comment