Taps can be unreliable inside this app's built-in browser. For the full experience, use its menu to open this page in your browser.
Skip to content

A Jigsaw That Showed The Answer

A devlog by Neeraj Kotwani, who builds every game on Elipar.

Jigsaw Safari is a grid-tile picture puzzle for the Kids category. Twelve hand-drawn animal scenes, three rounds per game at two by two, three by three and four by four. Straight-cut squares cropped out of one scene rather than true interlocking tabs, which is a deliberate scope choice — real interlocking edges need generated tab and socket pairs shared between every neighbour, and plenty of commercial kids' jigsaw apps use exactly this grid-tile mechanic.

It took three passes to notice that the board was showing you the finished picture the whole time.

Placing a piece changed nothing

Each empty slot carried a faint ghost of its own crop of the scene, as a placement guide. The slot element also had the same background image, at the same size and the same position — and the slot is fully opaque.

So the carefully tuned 28%-opacity ghost was being layered over a 100% copy of itself. The empty board rendered the complete finished picture at full brightness from the first frame, and dropping a piece into it produced no visible change at all beyond a one-pixel seam.

I measured it rather than describing it: filling a slot changed 0.0% of that slot's interior pixels, mean channel delta 0.06 out of 255. After the fix, 100.0% and 134.

A jigsaw where placing a piece is invisible has no feedback loop. You cannot see your own progress; the only signal left is a numeric counter.

This had been half-diagnosed twice

The uncomfortable part is that the symptom had been noticed before and explained wrongly, twice.

An earlier pass observed that the assembled picture "reads as a picture rather than a puzzle" and fixed the seam hairline between placed pieces. That was a real improvement and it is still in. It was not the cause, and the symptom survived it.

A later note described the board's ghost as "deliberately faint at 28% opacity". That sentence was describing the code. The pixels were at 100%. I had read the stylesheet and believed it.

A pixel diff was the only instrument that could settle this. It throws no error, logs nothing, and a screenshot of a partly solved board looks entirely plausible — the picture is supposed to be there. Screenshot the empty board, screenshot it one piece later, and diff the cell that changed.

The board now carries no scene at all. Every empty slot is a dark well, and the reference lives in a separate thumbnail beside the board that you can tap to enlarge. There is a comment on the stylesheet rule saying that putting a background image back on the slot reinstates the bug.

The drag was broken in two different ways

Both found by driving real pointer events end to end, and both invisible to tests that called the drop logic directly.

First: the dragged piece had pointer-events: none, to stop it shadowing the board underneath during the hit test. But the piece carried its own move and release listeners — and that property stops an element receiving any pointer event, including the ones that end its own drag. A real drag-and-release therefore never completed.

Every debug-hook test passed, because they call the drop function directly and skip the pointer machinery entirely. That is exactly why it shipped. A real mouse drag found it immediately: zero of four pieces placed. The hit test never needed that property anyway — it compares slot rectangles rather than asking what is under the cursor.

Second, and stranger: after fixing that, a real mouse drag worked and a real touch drag still placed nothing. Logging each event's target showed the press landing on the piece correctly, while the touch-originated move and release events landed on whatever was visually underneath instead of being retargeted to the capturing element.

Rather than fight that, the move, release and cancel listeners moved off the piece and onto the document, filtered by pointer id. That sidesteps the question of whether capture retargets correctly, and it is also immune to the piece being reparented mid-drag, which per-element listeners are not.

The tray scrolled on the same axis as the drag

The tray was a wrapping grid in a short box, so it scrolled vertically — the same direction as "drag this piece up onto the board". Those two gestures are then indistinguishable, and the drag always won, because the press lifted the piece immediately.

Measured: a real swipe starting on a piece scrolled the tray zero pixels. The same swipe starting on the thin gap between pieces scrolled sixty. With the tray packed, a finger almost always lands on a piece — so from round two onward, the pieces you could not see were the pieces you could not reach. Five or six of sixteen visible in the last round.

The tray is a single horizontal row now, which makes the two gestures orthogonal: sideways pans, upward picks up. A press only arms a drag and commits once it passes an eight-pixel slop.

I first tried handing the split to the browser with touch-action: pan-x, and it loses every diagonal drag: a piece bound for a far corner travels further across than up, Chrome reads that as a pan and takes the gesture. Zero of two corner drags placed. The handlers arbitrate it themselves instead, with two deliberate asymmetries that each exist because a test failed — any upward movement past the slop is a drag whatever the horizontal delta is doing, and a pan additionally requires that you have not moved up much, because otherwise whichever axis crosses the slop first wins and a shallow diagonal locks into a pan one frame before it would have become the drag you meant.

Jigsaw Safari is free in a browser, no account, no download. The board is empty until you fill it.

We use cookies to keep games running and, once ads are live, to show relevant content. See our Privacy Policy for details.