Drawing a heart shape with the Java 2D API

In this article, I am going to show you something a little fun, how to draw a nice heart shape using only two Bézier curves with the Java 2D API. This is also a good opportunity to explain some concepts about Bézier curves in an enjoyable way.

Introduction to Bézier curves

Bézier curves are “parametric” curves used in graphics design and in modern computer graphics. These curves are named after Pierre Bézier, a French engineer who used them extensively in his work in the automotive field for Renault cars.

A Bézier curve is defined by a set of points P0, P1Pn, where n indicates the “order” of the curve. The first and last points are always the endpoints, while intermediate points are called control points. The control points generally do not lie on the curve, they are used to control the shape and smoothness of the curve using mathematical formulas.

Although there can in theory be various orders of Bézier curves, in computer graphics only two orders are generally used:

  • a “quadratic” Bézier curve (2° order) with points P0 (endpoint), P1 (control point) and P2 (endpoint)
  • a “cubic” Bézier curve (3° order) with points P0 (endpoint), P1/P2 (control points) and P3 (endpoint)

In computer programming there are many 2D toolkits/environments, just to name a few: the Java 2D API, the HTML canvas, PDF files, SVG files, the Processing environment, the Cairo library and so on. In most of these environments, it is easily possible to draw cubic and/or quadratic Bézier curves. Higher degree Bézier curves (from the 4° order onwards) are generally not implemented because they are computationally more expensive to evaluate.

I won’t go much further with the theory, you can read the good Wikipedia page https://en.wikipedia.org/wiki/Bézier_curve. And if you are very mathematically inclined, there is a great free online book: “A Primer on Bézier Curves” written by Mike Kamermans. Keep in mind, anyway, that reading this book is absolutely NOT required to understand this article!

Introduction to Desmos

If you want to quickly “play” with Bézier curves, there is a useful website called Desmos (https://www.desmos.com). Desmos offers many types of interactive charts that you can try, modify and share. If you go here, you will find a nice playground for a cubic Bézier curve:

https://www.desmos.com/calculator/ebdtbxgbq0?lang=en

Try to move the two endpoints (cyan color) and the two control points (gray color). If you have never used Bézier curves so far, you can immediately notice that the generated curve is always very smooth and pleasant. This is one of the nice properties of Bézier curves in general.

Now that you have some exposure to Bézier curves, I want to propose an interesting question:

Can we use two (and only two!) cubic Bézier curves to draw a simple but nice heart shape?

The answer is yes! However, it’s important to clarify that you cannot think to draw all kinds of heart shapes using only two cubic Bézier curves. You may need four, six or more curves for complex heart shapes. And possibly also with some straight lines. In practice, with two cubic Bézier curves, you can certainly draw a basic but still enjoyable heart shape. This is the main goal of this article in the next sections.

Drawing a (half) heart on Desmos

You can draw only one cubic Bézier curve on the linked page on Desmos. This is not a problem, since the heart shape will be symmetric. So, it is sufficient to draw the curve for one side of the heart (e.g. the left side).

Let’s imagine that the full heart shape lies in a containing box of 1 x 1 unit. The following is the curve I drew on Desmos:

Heart shape on Desmos

If you want to draw your own left (or right) heart side on Desmos, you may get a different result in which the control points are located at different positions. This is not a big issue. The most important thing is that you draw a nice curve that can reasonably represent one side of a heart shape.

Important
In the above screenshot you can notice that the bottom point of the heart shape is located in the origin (0, 0) and that I used a bounding box of 1 x 1 unit. This is absolutely NOT accidental!

The code shown in the HeartShapePanel class draws the heart shape on a Swing JPanel component, which is contained into a JFrame. Since the frame is resizable, I wanted to make the heart shape resizable too. But beware, here is the important issue: if the heart shape changes size, also the control points must be scaled accordingly. Otherwise, you would draw a completely different curve! This is the reason for this specific coordinates setup. In this way, it is very easy and natural to get the delta-x and delta-y of the points that will be used as multiplication factors.

Given my drawing on Desmos, the factors are as follows:

  • Control point 1: dx = 0.968 ; dy = 0.672
  • Control point 2: dx = 0.281 ; dy = 1.295
  • Top endpoint: dy = 0.850

You will see exactly these values in the Java code!

Drawing a heart in Java 2D

Now it’s time to draw the heart shape in Java. The example code is a simple standalone Swing application that uses the Java 2D API to draw the heart shape. Java 2D was introduced a long time ago in JDK 1.2 to provide a more sophisticated graphics environment for both AWT and Swing toolkits.

In Java 2D, the central element is the java.awt.Graphics2D class, which is an extension of the “old” java.awt.Graphics class available since JDK 1.0. Graphics2D provides many methods for drawing different types of figures (lines, rectangles, ellipses, shapes, etc…).

Shapes are represented at an abstract level by the java.awt.Shape interface. There are various implementations of this interface, among these there is the Path2D abstract class and the two concrete subclasses Path2D.Float and Path2D.Double. The only difference between these two classes is the precision of the coordinates stored into the object (clearly, float vs double).

A Path2D can contain three types of primitives:

  • straight lines (lineTo methods)
  • “quadratic” Bézier curves (quadTo methods)
  • “cubic” Bézier curves (curveTo methods)

In the example code, I created the heart shape using a Path2D.Float and two invocations of the curveTo method.

This is clearly not an introductory article on Java 2D, so if this explanation is not sufficient, I suggest you to read the Oracle’s tutorial: Trail: 2D Graphics (The Java™ Tutorials)

Java code: HeartShapePanel.java

The following HeartShapePanel class extends JPanel to draw the heart shape using the classic custom painting technique as described in the Oracle’s tutorial Lesson: Performing Custom Painting (The Java™ Tutorials).

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import javax.swing.JPanel;

public class HeartShapePanel extends JPanel {
    private static final long serialVersionUID = 1L;   // mainly for "picky" IDEs

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        int margin = 5;   // just some margin around the heart shape
        int panelWidth = getWidth();
        int panelHeight = getHeight();
        int boxSize = Math.min(panelWidth, panelHeight) - margin * 2;
        float boxX = (panelWidth - boxSize) / 2.0f;
        float boxY = (panelHeight - boxSize) / 2.0f;

        if (boxSize > 0) {
            Path2D heartPath = createHeartPath(boxX, boxY, boxSize, boxSize);

            // activates the "antialiasing" feature
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            // sets the color and fills the shape
            g2d.setColor(Color.RED);
            g2d.fill(heartPath);
        }
    }

    private Path2D createHeartPath(float x, float y, float width, float height) {
        float beX = x + width / 2;  // bottom endpoint X
        float beY = y + height;     // bottom endpoint Y

        float c1DX = width  * 0.968f;  // delta X of control point 1
        float c1DY = height * 0.672f;  // delta Y of control point 1
        float c2DX = width  * 0.281f;  // delta X of control point 2
        float c2DY = height * 1.295f;  // delta Y of control point 2
        float teDY = height * 0.850f;  // delta Y of top endpoint

        Path2D.Float heartPath = new Path2D.Float();
        heartPath.moveTo(beX, beY);       // bottom endpoint
        // left side of heart
        heartPath.curveTo(
                beX - c1DX, beY - c1DY,   // control point 1
                beX - c2DX, beY - c2DY,   // control point 2
                beX       , beY - teDY);  // top endpoint
        // right side of heart
        heartPath.curveTo(
                beX + c2DX, beY - c2DY,   // control point 2
                beX + c1DX, beY - c1DY,   // control point 1
                beX       , beY);         // bottom endpoint
        return heartPath;
    }
}

Java code: HeartShapeDemo.java

The following HeartShapeDemo class is a simple “main” class to start the application. This code requires at least Java 8 just because I used a lambda expression for invokeLater (not very important, you can use an anonymous inner class instead).

import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class HeartShapeDemo {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            HeartShapePanel heartShapePanel = new HeartShapePanel();
            heartShapePanel.setBackground(Color.WHITE);

            JFrame frame = new JFrame("Heart shape in Swing/Java 2D");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 300);
            frame.add(heartShapePanel);
            frame.setLocationRelativeTo(null);  // centers on screen
            frame.setVisible(true);
        });
    }
}

You can use an IDE (e.g. Eclipse or IntelliJ IDEA) to build and run the application, or you can compile and run using the JDK directly from a command prompt. In the latter case, it’s sufficient to execute:

javac -cp . HeartShapeDemo.java

and then:

java -cp . HeartShapeDemo

Note that the -cp . option (sets the “classpath” to the current directory) is just to make sure there’s no problem with the CLASSPATH environment variable (if it is set in your environment).

The final result

When you run the application, this is the result you should see (note, screenshot captured on Windows 10):

Heart shape in Swing/Java 2D

Pretty nice, isn’t it?

You can resize the frame to any size (from 50×50 pixels to, say, 1000×1000 pixels) and the heart shape will be resized too. Note that the heart shape will always keep the aspect-ratio (it will not be stretched). This is due to the logic in paintComponent that calculates the squared box size whatever the panel size is.

Conclusions

I hope this article helped you to better understand the usage of Bézier curves in computer graphics, especially in the Java/Swing environment. It’s important to remember that all concepts exposed in this article can also be applied to any other 2D toolkit/environment.

You can draw the same heart shape in PDF/SVG files, in JavaFX, in the HTML canvas, in the Processing environment, in native applications with the Cairo library (C) or with the Qt framework (C++), and so on. In practice, you can do this anywhere you can draw Bézier curves using dedicated functions/methods or directives.

Similar Posts