AI-Assisted Paint Application
Built a JavaFX drawing application around MVC and object-oriented design patterns, with undo/redo, multiple drawing strategies, and AI-generated JSON instructions for rendering shapes to the canvas.
Overview
I built this desktop paint application as a study in maintainable object-oriented design, not just canvas rendering. It supports seven drawing modes, fill and outline styles, line thickness and opacity controls, undo and redo, PNG export, persistent themes, colour-blind palettes, keyboard shortcuts, and prompt-generated drawings.
The main design goal was to keep input handling, drawing state, and rendering independent. That separation made it possible to add tools and alternate input paths without turning the canvas into one large event handler.
The simplest mental model is: controllers interpret input, strategies or commands decide what that input means, the model stores the result, and the observed canvas redraws the model. No mouse handler draws directly to the final canvas state.
How an interaction moves through MVC
JavaFX events enter through small controllers. Mouse input selects a drawing strategy, menu and keyboard input execute shared commands, and prompt input delegates generation to the model. These paths update the same drawing state, after which the canvas redraws every stored Drawable in order. The canvas view never decides how a circle is resized or how undo works; it renders the resulting state.
- Input Mouse, keyboard, or menu
- Controller Translates the JavaFX event
- Behaviour Applies a strategy or command
- Model Owns drawables and history
- View Observes state and redraws
Extensible drawing tools
The Strategy pattern gives every tool the same mouse lifecycle: press, drag, move, and release. A factory chooses the active strategy from the selected mode, so the canvas controller does not need shape-specific branches. Adding another tool means implementing one strategy and one Drawable, then registering the new mode.
Drag-based shapes create a provisional object on press, update their geometry while dragging, and become fixed on release. Freehand strokes use linked points with explicit endpoints, while polylines and triangles collect successive clicks and display a live hover preview before completion.
interface StrategyPaint {
void mousePressed();
void mouseDragged();
void mouseMoved();
void mouseReleased();
}
switch (mode) {
case "circle" -> new StrategyCircle(activeModel, mouseEvent);
case "squiggle" -> new StrategySquiggle(activeModel, mouseEvent);
case "polyline" -> new StrategyPolyline(activeModel, mouseEvent);
}
Model-driven rendering
The model is the source of truth for the ordered drawable collection and active tool settings. Every Drawable owns its geometry and rendering behaviour; shape types also carry colour, opacity, thickness, and fill state where applicable. This polymorphic boundary lets the canvas render mixed objects through one loop.
The canvas observes both drawing-state and palette changes. Either source triggers the same redraw routine: clear the canvas, restore the active background, then render the collection in order. This covers live previews, completed shapes, undo, theme changes, and generated drawings.
public void update(Observable source, Object event) {
GraphicsContext g2d = this.getGraphicsContext2D();
ColourPalette palette = ColourPalette.getInstance();
String canvasColour = palette.getColour("canvas").getHex();
g2d.clearRect(0, 0, this.getWidth(), this.getHeight());
g2d.setFill(Color.web(canvasColour));
g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
for (Drawable drawable : this.model.getDrawings()) {
drawable.draw(g2d);
}
}
Undo, redo, and application commands
Menu actions and keyboard shortcuts share Command objects, so Ctrl/Cmd+Z and the Undo menu item execute the same behaviour. The model keeps active and removed drawables as lightweight history stacks. A freehand stroke is treated as one logical action by walking backward through its points until an endpoint is reached, rather than undoing one sampled point at a time.
Clear Canvas is also reversible: the full drawing list moves into history, and the next undo restores it as one operation. Export snapshots the JavaFX canvas with its current background and writes a PNG selected through the native file chooser.
interface MenuCommand {
void execute();
}
class UndoCommand implements MenuCommand {
public void execute() {
if (model.isPrevActionClear()) model.undoClearDrawings();
else if (!model.getDrawings().isEmpty()) model.removeLast();
}
}
AI-generated drawings
Prompt generation is an alternate producer of model objects, not a separate rendering system. The app requests structured JSON containing rectangles, ovals, triangles, and polylines, checks the response shape, and converts each entry into the same Drawable hierarchy used by manual tools.
Generated shapes are scheduled as short JavaFX keyframes, creating a progressive drawing effect while keeping UI updates on the JavaFX application thread. Because generated and manual shapes share the same collection, they participate in redraw, undo, palette-aware rendering, and export without a second rendering path.
Themes and accessible colour
A shared palette provides semantic colours for the canvas and controls. It supports multiple themes and protanopia, deuteranopia, and tritanopia variants, persists the selected preferences locally, and notifies observing views when the palette changes. A small builder keeps theme-specific colour definitions readable while the rest of the application requests colours by role rather than hard-coded hex values.
How the pieces fit together
A mouse event reaches a controller, which asks the factory for the active drawing strategy. That strategy creates or updates a Drawable in the model. The model then notifies the canvas, which clears and redraws the ordered collection. Menu and keyboard actions reach the same model through Command objects, so actions such as undo and export behave consistently regardless of how they were triggered.
Each pattern solves a specific problem: Strategy isolates tool behaviour, Factory selects the active tool, Command unifies application actions, Observer keeps views synchronized, and Singleton plus Builder centralize palette configuration. Prompt-generated shapes join the same Drawable collection, so the AI feature extends the existing architecture instead of creating a second canvas system.
- Java
- JavaFX
- MVC
- Design Patterns
- REST APIs