← Back to all articles

guides · 25 July 2026 · 10 min read

How I Built the JPW Games Browser Jigsaw

A code-level tour of how I turn an image into shuffled pieces, validate drops, save progress, and keep browser-only APIs out of server rendering.

By JPW Games Developer · Updated 25 July 2026

I built the game around a small data model

Each puzzle in JPW Games begins as an image and a catalogue record. The record contains an ID for the URL, a title, an image path, a category, and a difficulty. When I open a game route, I look up that record and generate a board in the browser.

For each generated piece, I store its row and column, its correct row and column, its image data, whether it has been placed, and whether it came from the outside edge. That is enough information to shuffle the pieces while preserving the answer.

I use canvas to divide the source image

My PuzzleGenerator loads the selected image into an HTML image element. I take the smaller of its width and height and use a centred square crop. I draw that crop onto an off-screen canvas, divide the canvas into equal rows and columns, and copy each rectangle to a smaller piece canvas.

Calling toDataURL() on the piece canvas gives me an image that the React interface can render as a draggable item. The current production pieces are square. The generator contains early code for tabs and blanks, but I do not describe that unfinished path as a live feature.

The centred crop is a deliberate compromise. A square board is predictable on phones and desktops, but a wide or tall source photo loses content near its outer edges. I therefore need to choose source images whose important subject remains clear in the centre. A future improvement would let me store a focal point per image instead of assuming the centre is always correct.

I map difficulty to a real grid

The live game maps four labels to square grids:

  • Easy becomes 5 by 5, or 25 pieces.
  • Medium becomes 8 by 8, or 64 pieces.
  • Hard becomes 10 by 10, or 100 pieces.
  • Expert becomes 12 by 12, or 144 pieces.

The catalogue is now the source of truth for both page copy and game generation. Each difficulty stores its actual square-board count, and the generator reads that same value instead of calculating a separate hidden count.

After generating the pieces, I shuffle them with a Fisher-Yates-style loop. I copy the array first, then swap each position with a randomly chosen earlier position. The correct coordinates stay on every piece.

I validate a drop instead of accepting proximity

The board is a grid of drop zones. When I drag a piece over a cell, the interface compares the piece's stored correct row and column with that cell. A correct match plays confirmation feedback and moves the piece into the placed collection. An incorrect drop plays different feedback and leaves the piece available.

This design is intentionally strict. I do not calculate a physical-looking snap radius because the user drops onto a known grid cell. The benefit is a clear rule and simple state. The trade-off is that the experience feels more like image-tile reconstruction than an irregular cardboard jigsaw.

I also guard against duplicate placement. Before adding a piece to the placed array, I check whether its ID is already there. That matters because asynchronous UI events and the hint animation can otherwise attempt to place the same item twice.

I support mouse and touch through separate backends

Desktop browsers and touchscreens produce different input events. I use React DnD with its HTML5 backend for mouse-oriented devices and its touch backend when touch capability is present. On touch, I add a short start delay and movement tolerance so a tap or small finger movement is less likely to become an accidental drag.

For the larger supported boards, I also provide manual zoom behaviour. Dragging a puzzle piece and moving the board are competing gestures, so the interface tracks whether a piece is being dragged before applying board movement.

I keep progress in the browser

While a game is active, I save the remaining pieces, placed pieces, hint state, total count, and puzzle ID to localStorage. On a later render, I restore only when the saved puzzle ID matches the current route and the saved structure looks valid.

This is local browser persistence, not an online account or cloud save. Clearing site storage can remove it, and it should not be presented as cross-device synchronisation.

I had to respect server rendering

The canvas generator calls document.createElement, so it cannot be constructed while Next.js is rendering on the server. An earlier version created it too early and could fail because document does not exist in Node.js.

I moved construction into the browser-side game initialisation callback. That callback runs from an effect or a user action after the client is available. This is a practical example of the boundary Next.js documents between Server and Client Components: browser APIs must remain on the client side.

I designed the hint to be finite

The current hint can be used once. It selects up to ten remaining pieces and places them at their stored correct positions with short staggered delays. If fewer than ten remain, it completes those remaining positions.

That is more substantial than a subtle visual clue, so I describe it accurately. I also use a ref to check whether a selected piece is still available when its delayed placement runs. Without that check, a player could manually place the piece during the delay and trigger a duplicate update.

What I would improve next

My highest-value improvements are not decorative. I would add automated tests for restore and hint races, improve focal cropping, audit keyboard interaction, and replace the unused tab-shape code with either a completed implementation or removal.

I would also build real offline support before advertising it. A browser cache happens to retain some resources, but dependable offline behaviour requires an intentional service-worker and cache strategy. The current site does not include that layer, which is why I removed earlier offline claims from the site.

Technical references