How do I Organize Multiple Methods in a Compact Source File?

Java 25 makes it easier than ever to write small, focused programs without ceremony. Thanks to JEP 512: Compact Source Files and Instance Main Methods (finalized in Java 25), you can skip the enclosing class declaration entirely and still define multiple methods and fields in a single source file. This is perfect for scripts, learning exercises, and quick utilities.

Let me walk you through the best practices for organizing methods in these compact files.


1. The Basic Structure

A compact source file has an implicit top-level class. You just write your main method (instance-style, no static required) and add helper methods around it:

void main() {
    println("Welcome!");
    greet("Alice");
    println("Sum = " + add(3, 4));
}

void greet(String name) {
    println("Hello, " + name + "!");
}

int add(int a, int b) {
    return a + b;
}

No public class Foo { ... } wrapper is needed. The compiler generates it for you.


2. Put main First (or Make It Easy to Find)

For readability, keep the entry point at the top of the file so a reader immediately sees the program’s flow:

void main() {
    var user = askName();
    var total = computeTotal(10, 20, 30);
    printReport(user, total);
}

// --- helpers below ---

String askName() {
    return "Guest";
}

int computeTotal(int... values) {
    int sum = 0;
    for (int v : values) sum += v;
    return sum;
}

void printReport(String user, int total) {
    println("User : " + user);
    println("Total: " + total);
}

This mirrors how many scripting languages read: entry point first, details after.


3. Group Related Methods Together

When your file grows, cluster methods by responsibility and separate the groups with comment banners:

void main() {
    var nums = List.of(1, 2, 3, 4, 5);
    println("Sum      = " + sum(nums));
    println("Average  = " + average(nums));
    println("Uppercase: " + shout("hello"));
}

// ---------- Math helpers ----------

int sum(List<Integer> xs) {
    return xs.stream().mapToInt(Integer::intValue).sum();
}

double average(List<Integer> xs) {
    return xs.stream().mapToInt(Integer::intValue).average().orElse(0);
}

// ---------- String helpers ----------

String shout(String s) {
    return s.toUpperCase() + "!";
}

If a group becomes large, that is a strong signal it should be extracted into its own class or file.


4. Use Instance Fields for Shared State

Compact files support instance fields, so you don’t need to pass configuration through every method call:

final String appName = "DemoApp";
final int maxRetries = 3;

void main() {
    banner();
    run();
}

void banner() {
    println("=== " + appName + " ===");
}

void run() {
    for (int i = 1; i <= maxRetries; i++) {
        println("Attempt " + i);
    }
}

Prefer final fields to keep the file predictable and easy to reason about.


5. Leverage the Auto-Imported java.base and IO Methods

Java 25 auto-imports common utilities and gives you top-level print, println, and readln (from java.lang.IO). This keeps helper methods short:

void main() {
    var name = readln("Your name: ");
    println(greeting(name));
}

String greeting(String name) {
    return "Hello, " + (name.isBlank() ? "stranger" : name) + "!";
}

No import statements, no System.out.println — the file stays compact.


6. Keep Methods Small and Single-Purpose

Because there is no class boundary to hide behind, discipline matters more. Follow these guidelines:

Guideline Why it matters in a compact file
One responsibility per method Compensates for the flat structure
Short method names, descriptive The file reads top-to-bottom like a script
Prefer pure functions Easier to reason about without a class scope
Extract when > ~15 lines Prevents the file from becoming a wall of code

7. Order Methods by “Newspaper Style”

Arrange methods so the reader moves from high-level to low-level, like a newspaper article:

void main() {           // headline: what the program does
    processOrder();
}

void processOrder() {   // section: main steps
    validate();
    charge();
    ship();
}

void validate() { /* ... */ }   // details
void charge()   { /* ... */ }
void ship()     { /* ... */ }

Readers rarely need to jump around — they simply scroll down for more detail.


8. When to Stop Using a Compact File

Compact source files shine for small, self-contained programs. Migrate to a regular class (or multiple classes) when you notice:

  • Multiple unrelated groups of methods
  • Need for multiple types (records, enums beyond simple helpers)
  • Reuse from other files
  • Unit tests targeting individual methods

You can promote a compact file by simply wrapping everything in public class Name { ... } and adding public static void main(String[] args) — the migration is mechanical.


Complete Example

Here is a compact file that puts all the tips together:

final String title = "Tip Calculator";

void main() {
    banner();
    double bill = 84.50;
    double tipPct = 0.18;

    double tip = tipAmount(bill, tipPct);
    double total = bill + tip;

    printLine("Bill",  bill);
    printLine("Tip",   tip);
    printLine("Total", total);
}

// ---------- domain logic ----------

double tipAmount(double bill, double pct) {
    return round2(bill * pct);
}

double round2(double v) {
    return Math.round(v * 100.0) / 100.0;
}

// ---------- output helpers ----------

void banner() {
    println("=== " + title + " ===");
}

void printLine(String label, double value) {
    println(String.format("%-6s: %8.2f", label, value));
}

Run it directly with:

java TipCalculator.java

Summary

  • Put main first, helpers below.
  • Group related methods and separate groups with comment banners.
  • Use instance fields for shared, mostly-final state.
  • Rely on auto-imports and top-level IO methods to stay concise.
  • Follow newspaper order — general to specific.
  • Graduate to a regular class once the file grows beyond a single concern.

Compact source files let you write real, multi-method programs with almost zero boilerplate — as long as you keep the file focused and well-organized.

How do I Read User Input in a Compact Java 25 Program?

Java 25 (via JEP 512: Compact Source Files and Instance Main Methods, finalized in Java 25) makes reading user input dramatically simpler. You no longer need a class declaration, a public static void main(String[] args) signature, or even System.out.println / Scanner boilerplate.

The New IO Class

Java 25 introduces the java.lang.IO class, which is automatically imported in compact source files. It provides three convenient static methods:

Method Purpose
IO.print(x) Print without newline
IO.println(x) Print with newline
IO.readln(prompt) Print a prompt and read a line from stdin

Compact Example

Here’s a compact Java 25 program that reads user input:

void main() {
    String username = IO.readln("Username: ");
    String password = IO.readln("Password: ");
    int result = Integer.parseInt(IO.readln("What is 2 + 2: "));

    if (username.equals("admin") && password.equals("secret") && result == 4) {
        IO.println("Welcome to Java Application");
    } else {
        IO.println("Invalid username or password, access denied!");
    }
}

Key Points

  • No class declaration required — the file becomes an implicitly declared class.
  • No String[] args — you can just write void main().
  • No import statements — java.base and java.lang.IO are auto-imported.
  • No Scanner — IO.readln(...) handles the prompt and line read in one call.
  • The method can be void main() or void main(String[] args); static is optional.

Running It

Save the code as Login.java and run it directly (no compile step needed):

java Login.java

When You Still Need Scanner

IO.readln only returns String. If you need typed input like int, double, etc., you either:

  1. Parse it yourself (as shown above with Integer.parseInt), or
  2. Fall back to java.util.Scanner for its nextInt(), nextDouble(), etc.

For most simple interactive programs, IO.readln + parsing is the cleanest approach in Java 25.

How do I Use an Instance main Method in Java 25?

Java 25 finalizes JEP 512: Compact Source Files and Instance Main Methods, which was previewed in earlier JDK releases. This feature makes Java far more approachable for beginners and reduces boilerplate for small programs and scripts.

What Changed?

Traditionally, every Java program required this ceremony:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

With Java 25, the main method no longer needs to be:

  • public
  • static
  • Declared with a String[] args parameter

The New Rules

The JVM launcher now looks for a main method in this order of preference:

  1. static void main(String[] args) — traditional form
  2. static void main() — no parameters
  3. void main(String[] args) — instance method with args
  4. void main() — instance method, no args simplest form

If an instance main method is found, the JVM will implicitly create an instance of the enclosing class using its no-argument constructor, then invoke main on it.

Example 1: The Simplest Instance main

public class Hello {
    void main() {
        System.out.println("Hello from an instance main!");
    }
}

That’s the entire program. No static, no String[] args, no public.

Run it with:

java Hello.java

Example 2: Instance main With Fields and Helper Methods

Because main is now an instance method, it can freely use instance fields and call other instance methods without needing static everywhere:

public class Greeter {
    private final String greeting = "Hello";

    void main() {
        greet("Java 25");
        greet("Developers");
    }

    void greet(String name) {
        System.out.println(greeting + ", " + name + "!");
    }
}

Notice that greet is also a plain instance method — no static modifier needed.

Example 3: Combined With Compact Source Files

Java 25 also allows you to omit the enclosing class entirely (Compact Source File):

void main() {
    System.out.println("No class declaration required!");
}

Save this as Demo.java and run:

java Demo.java

The compiler implicitly wraps the code in a synthetic class for you.

Example 4: Instance main With Arguments

If you still need command-line arguments, just declare them:

public class Echo {
    void main(String[] args) {
        for (String arg : args) {
            System.out.println("Argument: " + arg);
        }
    }
}

Requirements & Caveats

  • The enclosing class must have an accessible no-argument constructor (the default one is fine if you don’t declare any constructor).
  • The class cannot be abstract.
  • If both a static and an instance main exist, the static one wins (per the resolution order above).
  • The instance main method cannot be private. It must be at least package-private.
  • To run a single source file directly (java Foo.java), you don’t need to compile first — the launcher handles it.

Why This Matters

  • Lower barrier for beginners — no need to explain public, static, String[] args, or classes on day one.
  • Cleaner scripts — small utilities and experiments become much more concise.
  • Smoother learning curve — students can gradually introduce classes, static, and access modifiers as they progress, rather than all at once.

Quick Comparison

Style Java ≤ 20 Java 25
public static void main(String[] args) ✅ Required ✅ Still works
static void main() ❌ ✅
void main(String[] args) ❌ ✅
void main() ❌ ✅
No class declaration ❌ ✅ (Compact Source File)

Instance main methods, combined with compact source files, make Java 25 one of the most beginner-friendly releases in the language’s history — while remaining fully backward compatible with every existing Java program.

How do I Run My First Java 25 Program Without Creating a Class?

Java 25 makes it official: you can now write and run a program without declaring a class, without public static void main(String[] args), and even without import statements for common APIs. This feature is called Compact Source Files and Instance Main Methods (JEP 512, finalized in Java 25).

Let’s walk through it step by step.

1. Prerequisites

  • JDK 25 installed and available on your PATH
  • Verify with:
java --version
javac --version

Both should report version 25.

2. Write the Program

Create a plain text file named Hello.java. That’s it — no class, no public static, no ceremony:

void main() {
    IO.println("Hello, Java 25!");
}

A few things worth noticing:

  • There is no class declaration. The compiler wraps the code in an implicitly declared class for you.
  • main is an instance method (not static) and takes no arguments (the String[] args parameter is optional now).
  • IO.println(...) comes from the new java.io.IO class, which is auto-imported in compact source files — no System.out.println and no import needed.

3. Run It Directly with java

Since JDK 11, you can run a single-file source program directly. In Java 25, this works beautifully with the new compact form:

java Hello.java

Expected output:

Hello, Java 25!

No javac step is required. The launcher compiles the file in memory and runs it.

4. A Slightly Richer Example

You can still read input, do logic, and use any Java API — just without the boilerplate:

void main() {
    var name = IO.readln("What is your name? ");
    IO.println("Welcome, " + name + "!");

    for (int i = 1; i <= 3; i++) {
        IO.println("Count: " + i);
    }
}

Run it the same way:

java Hello.java

5. When You Outgrow It

Compact source files are meant for learning, scripting, and quick experiments. When your program grows, you can gradually add:

  1. A String[] args parameter to main when you need CLI arguments.
  2. Additional methods and fields directly in the file (they become members of the implicit class).
  3. Finally, an explicit class declaration — at which point you have a regular Java source file.

The transition is smooth because the language rules are a strict superset of traditional Java.

6. Common Pitfalls

Issue Cause Fix
error: class ... is public, should be declared in a file named ... You added public to a helper class in the same file Remove public — the implicit class is unnamed
IO cannot be resolved You’re not on JDK 25 (or using an older preview flag) Upgrade to JDK 25; no --enable-preview needed anymore
main not found Wrong signature (e.g., returns int) Use void main() or void main(String[] args)

Summary

To run your first Java 25 program without creating a class:

  1. Install JDK 25.
  2. Create Hello.java containing just a void main() method.
  3. Use IO.println(...) — no imports needed.
  4. Run it with java Hello.java.

This is the shortest path from “I have JDK installed” to “my program is running” that Java has ever offered — perfect for beginners and for quick prototypes alike.

Taming a Dragon With Your Mouse: Building Dragon Cursor Chase in a Single Java File

There’s something delightful about tiny, self‑contained graphics demos. No frameworks, no build systems, no node_modules folder the size of a small moon — just one .java file, javac, and a window that does something you didn’t expect from Swing.

DragonCursorChaseMinimal is exactly that: a neon dragon made of 22 glowing circles that slithers after your cursor, blinks lazily at you, and breathes fire when you click. It fits in about 150 lines. Let’s take a tour of how it works and why the tricks inside it are worth stealing for your own doodles.

The idea in one paragraph

The dragon is a chain of points. The head chases the mouse with a simple spring‑like ease. Every other body segment just follows the one in front of it at a fixed distance. Render each point as a glowing circle, rotate a stylized head on top of the first point, and sprinkle particles when the user clicks. That’s the whole trick.

The body: a follow‑the‑leader chain

The state of the dragon is two parallel arrays of coordinates:

static final int N = 22;
final double[] x = new double[N], y = new double[N];

The head (index 0) does the actual chasing — a classic exponential smoothing toward the target:

x[0] += (mouseX - x[0]) * .28;
y[0] += (mouseY - y[0]) * .28;

That .28 is the “springiness”. Lower values make a lazier, more elegant dragon; higher values make a caffeinated one.

The rest of the body is where the magic happens. Each segment is pulled toward the previous one, but clamped to a fixed distance of 19 pixels:

for (int i = 1; i < N; i++) {
    double dx = x[i] - x[i - 1], dy = y[i] - y[i - 1];
    double d = Math.max(.001, Math.hypot(dx, dy));
    x[i] = x[i - 1] + dx / d * 19;
    y[i] = y[i - 1] + dy / d * 19;
}

This is sometimes called a distance constraint or a one‑pass rope solver. It gives you smooth, snake‑like body motion for basically free. The Math.max(.001, …) guard is a tiny but important detail — it prevents a division by zero when two segments occupy the same point (which happens on the very first frame).

Getting the head to point the right way

Because we saved the head’s previous position, we can compute its heading with a single atan2:

angle = Math.atan2(y[0] - oldY, x[0] - oldX);

That angle is then used both to rotate the head graphics and to aim the fire breath. It’s a nice example of how one derived value can unify several visual effects.

Painting the body: glow for the price of one extra oval

Each segment is drawn twice — once large and translucent for the halo, once smaller and opaque for the core:

g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
g.fillOval((int) x[i] - r - 5, (int) y[i] - r - 5, (r + 5) * 2, (r + 5) * 2);
g.setColor(c);
g.fillOval((int) x[i] - r, (int) y[i] - r, r * 2, r * 2);

That’s a poor‑man’s bloom effect. No shaders, no compositing tricks, just two ovals per segment. The radius shrinks along the body (5 + 15 * t) so the tail tapers naturally, and the hue drifts slightly with Color.getHSBColor(.44 + .10 * t, …) for a subtle gradient from teal to sea‑green.

The loop iterates backwards (for (int i = N - 1; i > 0; i--)), so bigger segments closer to the head end up painted on top of smaller tail segments. Painter’s algorithm at its most literal.

The head: a rotated coordinate system

Rather than doing trigonometry to place each eye, ear, and nostril, the code creates a child Graphics2D, translates it to the head position, and rotates it:

Graphics2D h = (Graphics2D) g.create();
h.translate(x[0], y[0]);
h.rotate(angle);

Now every subsequent drawing call — the snout Path2D, the triangular ears, the eyes — can be written in the dragon’s own local space, with x pointing forward. Notice how the two ears are just mirrored polygons:

h.fillPolygon(new int[]{-12, -22, 1}, new int[]{-17, -39, -20}, 3);
h.fillPolygon(new int[]{-12, -22, 1}, new int[]{ 17,  39,  20}, 3);

The two eyes get a cheap blinking animation by squishing their vertical radius with a sine wave:

double eyeH = 6 * Math.max(.12, Math.abs(Math.sin(time * .22)));

The Math.max(.12, …) keeps the eyes from ever fully closing, so the dragon looks alert rather than sleepy. And crucially, h.dispose() is called when we’re done — always dispose the graphics contexts you create(), or you’ll leak state into the parent.

Fire breath: particles with a lifetime

Clicking sets a timestamp:

public void mousePressed(MouseEvent e) {
    fireUntil = System.currentTimeMillis() + 550;
}

For 550 milliseconds after the click, each frame spawns four Flame particles, each with:

  • a position offset forward from the head by 38 pixels along angle,
  • a velocity fanned out by up to ±0.275 radians from the heading,
  • a random lifetime between 28 and 46 frames.

The particle physics is trivial but tuned:

void update() {
    x += vx;
    y += vy;
    vx *= .965;
    vy = vy * .965 + .025;
    life--;
}

Horizontal velocity decays; vertical velocity decays and gets a gentle downward tug. Result: flames shoot forward, slow down, and drift down like hot embers.

Rendering each flame is a single circle whose size, color, and alpha are all functions of remaining life:

float age = f.life / (float) f.maxLife;
int r = Math.max(2, (int) (18 * age));
Color c = Color.getHSBColor(.02f + .12f * age, 1, 1);

Young flames are bigger, more yellow, and more opaque. Old flames shrink into small, dim red pixels before vanishing. That single hue interpolation from .02 (red) to .14 (orange‑yellow) is doing a lot of aesthetic heavy lifting.

The background: one gradient to rule them all

The dark deep‑blue backdrop is drawn once per frame as a radial gradient:

g.setPaint(new RadialGradientPaint(
        new Point2D.Double(getWidth() / 2.0, getHeight() / 2.0),
        Math.max(getWidth(), getHeight()) * .7f,
        new float[]{0, 1},
        new Color[]{new Color(22, 35, 76), new Color(3, 5, 14)}));
g.fillRect(0, 0, getWidth(), getHeight());

It’s the cheapest way to make the scene feel like it has depth. The neon colors of the dragon pop against it precisely because the corners fade almost to black.

The animation loop: javax.swing.Timer at 60 fps

There’s no thread management, no game loop, no Thread.sleep in a run() method. Just:

new Timer(16, e -> update()).start();

A javax.swing.Timer fires its callback on the Event Dispatch Thread every 16 ms — roughly 60 fps. Inside update() we move the dragon, tick the particles, then call repaint(). Because everything runs on the EDT, there are no synchronization concerns between input (mouse events) and rendering. For a demo of this size, it’s the right tool.

Why this pattern is worth stealing

A few takeaways that generalize beyond dragons:

  1. Chains of points + a distance constraint are a shockingly good approximation of ropes, snakes, tentacles, and hair. One loop, no physics library.
  2. Translate + rotate a child Graphics2D whenever you’re drawing a directional object. Trying to bake rotation into every coordinate by hand is a recipe for off‑by‑one‑radian bugs.
  3. Two‑pass “halo + core” drawing gives you a convincing glow without touching any compositing APIs or BufferedImages.
  4. Particles = position + velocity + life. That’s genuinely all you need for 90% of “juice” effects.
  5. javax.swing.Timer is fine. For interactive art at 60 fps, the EDT will not let you down.

Running it

Save the file as DragonCursorChaseMinimal.java and, from the same folder:

javac DragonCursorChaseMinimal.java
java DragonCursorChaseMinimal

A 720×1280 window opens. Move your mouse. The dragon follows. Click and hold — it breathes fire in whichever direction it’s currently pointed. Let go and the flames drift, cool, and disappear.

That’s it. One file, one dragon, zero dependencies. Sometimes the best way to remember why you liked programming is to make something that has no business existing and put it on your screen for an afternoon.

The Complete Code

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class DragonCursorChaseMinimal extends JPanel {
    static final int N = 22;
    final double[] x = new double[N], y = new double[N];
    final List<Flame> fire = new ArrayList<>();
    final Random random = new Random();
    double mouseX = 360, mouseY = 640, angle, time;
    long fireUntil;

    DragonCursorChaseMinimal() {
        setPreferredSize(new Dimension(720, 1280));
        setBackground(new Color(4, 7, 20));
        for (int i = 0; i < N; i++) {
            x[i] = mouseX;
            y[i] = mouseY + i * 19;
        }

        MouseAdapter mouse = new MouseAdapter() {
            public void mouseMoved(MouseEvent e) {
                aim(e);
            }

            public void mouseDragged(MouseEvent e) {
                aim(e);
            }

            public void mousePressed(MouseEvent e) {
                fireUntil = System.currentTimeMillis() + 550;
            }

            void aim(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        };
        addMouseMotionListener(mouse);
        addMouseListener(mouse);
        new Timer(16, e -> update()).start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Dragon Cursor Chase");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane(new DragonCursorChaseMinimal());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    void update() {
        double oldX = x[0], oldY = y[0];
        x[0] += (mouseX - x[0]) * .28;
        y[0] += (mouseY - y[0]) * .28;
        angle = Math.atan2(y[0] - oldY, x[0] - oldX);

        for (int i = 1; i < N; i++) {
            double dx = x[i] - x[i - 1], dy = y[i] - y[i - 1];
            double d = Math.max(.001, Math.hypot(dx, dy));
            x[i] = x[i - 1] + dx / d * 19;
            y[i] = y[i - 1] + dy / d * 19;
        }

        if (System.currentTimeMillis() < fireUntil) emitFire();
        for (Iterator<Flame> it = fire.iterator(); it.hasNext(); ) {
            Flame f = it.next();
            f.update();
            if (f.life <= 0) it.remove();
        }
        time += .05;
        repaint();
    }

    void emitFire() {
        for (int i = 0; i < 4; i++) {
            double a = angle + (random.nextDouble() - .5) * .55;
            double speed = 7 + random.nextDouble() * 6;
            fire.add(new Flame(x[0] + Math.cos(angle) * 38,
                    y[0] + Math.sin(angle) * 38,
                    Math.cos(a) * speed, Math.sin(a) * speed,
                    28 + random.nextInt(18)));
        }
    }

    protected void paintComponent(Graphics raw) {
        super.paintComponent(raw);
        Graphics2D g = (Graphics2D) raw.create();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g.setPaint(new RadialGradientPaint(
                new Point2D.Double(getWidth() / 2.0, getHeight() / 2.0),
                Math.max(getWidth(), getHeight()) * .7f,
                new float[]{0, 1},
                new Color[]{new Color(22, 35, 76), new Color(3, 5, 14)}));
        g.fillRect(0, 0, getWidth(), getHeight());

        for (int i = N - 1; i > 0; i--) {
            double t = 1 - i / (double) N;
            int r = (int) (5 + 15 * t);
            Color c = Color.getHSBColor((float) (.44 + .10 * t), .82f, .95f);
            g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
            g.fillOval((int) x[i] - r - 5, (int) y[i] - r - 5, (r + 5) * 2, (r + 5) * 2);
            g.setColor(c);
            g.fillOval((int) x[i] - r, (int) y[i] - r, r * 2, r * 2);
        }

        Graphics2D h = (Graphics2D) g.create();
        h.translate(x[0], y[0]);
        h.rotate(angle);
        h.setColor(new Color(40, 255, 190, 65));
        h.fillOval(-32, -27, 68, 54);
        h.setColor(new Color(35, 210, 150));
        h.fillRoundRect(-25, -20, 56, 40, 22, 22);

        Path2D snout = new Path2D.Double();
        snout.moveTo(18, -12);
        snout.lineTo(40, 0);
        snout.lineTo(18, 12);
        snout.closePath();
        h.setColor(new Color(70, 240, 170));
        h.fill(snout);

        h.setColor(new Color(160, 255, 230));
        h.fillPolygon(new int[]{-12, -22, 1}, new int[]{-17, -39, -20}, 3);
        h.fillPolygon(new int[]{-12, -22, 1}, new int[]{17, 39, 20}, 3);

        double eyeH = 6 * Math.max(.12, Math.abs(Math.sin(time * .22)));
        h.setColor(Color.WHITE);
        h.fill(new Ellipse2D.Double(2, -14, 12, eyeH));
        h.fill(new Ellipse2D.Double(2, 8, 12, eyeH));
        h.setColor(new Color(10, 20, 25));
        h.fillOval(8, -13, 4, 4);
        h.fillOval(8, 9, 4, 4);
        h.dispose();

        for (Flame f : fire) {
            float age = f.life / (float) f.maxLife;
            int r = Math.max(2, (int) (18 * age));
            Color c = Color.getHSBColor(.02f + .12f * age, 1, 1);
            g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (210 * age)));
            g.fillOval((int) f.x - r / 2, (int) f.y - r / 2, r, r);
        }

        g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, Math.max(18, getWidth() / 27)));
        g.setColor(new Color(240, 250, 255, 220));
        centre(g, "MOVE THE CURSOR", 56);
        g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, Math.max(13, getWidth() / 45)));
        g.setColor(new Color(190, 215, 235, 175));
        centre(g, "Click to breathe fire • Java Swing • 1 file", 84);
        g.dispose();
    }

    void centre(Graphics2D g, String text, int y) {
        g.drawString(text, (getWidth() - g.getFontMetrics().stringWidth(text)) / 2, y);
    }

    static class Flame {
        double x, y, vx, vy;
        int life, maxLife;

        Flame(double x, double y, double vx, double vy, int life) {
            this.x = x;
            this.y = y;
            this.vx = vx;
            this.vy = vy;
            this.life = this.maxLife = life;
        }

        void update() {
            x += vx;
            y += vy;
            vx *= .965;
            vy = vy * .965 + .025;
            life--;
        }
    }
}