[ ] ppuzzle.dev
solver online
/* section.000 :: prelude */

a puzzle is a function awaiting input.

ppuzzle.dev is a generative puzzle constructor. We treat every grid, lattice, and constraint network as a small program — a deterministic system that resolves to one state and one state only when its arguments converge.

build2026.03
runtimeCSS + JS
surface10 x 10
periodalgorithmic
scroll to compile _
/* section.001 :: construct() */

construct()
shapes from rules

A puzzle begins as an empty domain — a grid of undefined cells, a graph of unconstrained nodes. We seed it with a small set of axioms and let propagation do the rest. Difficulty is not authored; it is a consequence of the constraint topology.

// generate a 10x10 lattice
const lattice = Array.from({ length: 100 }, (_, i) => ({
  row: Math.floor(i / 10),
  col: i % 10,
  phase: (i * 80) + 1000,
  state: 'undefined'
}));
/* section.010 :: solve() */

solve()
convergence as motion

We do not animate the answer; we animate the process of arriving at it. A solver is a sequence of rotations applied to a state vector. When the vector stops moving, the puzzle is solved. The visual on the right is one such solver running in real time — 100 cells, 100 phases, one shared coordinate system.

θ(i, t) = (i · ω₀ + φ_scroll) mod 2π /* per-cell rotation as a function of index and scroll */

The moiré that emerges between adjacent cells is not an effect — it is the by-product of a slightly mis- aligned period. The same phenomenon that ruins a halftone print is the one that gives the lattice its living quality.

/* the only line of JS that touches the grid */
window.addEventListener('scroll', () => {
  document.documentElement.style.setProperty(
    '--scroll-progress',
    window.scrollY / document.body.scrollHeight
  );
});
/* section.011 :: observe() */

observe()
the lattice as a clock

Read the grid the way you would read a clock made of gears. Each cell turns at its own rate. Patterns travel diagonally. Hue drifts as you scroll, because hue is bound to the same scalar that drives every other animation in the page.

void #0D1117 /* background */
base #1A2332 /* grid cell fill */
green #00E676 /* solver active */
blue #448AFF /* propagation */
amber #FFB300 /* contradiction */
text #C9D1D9 /* primary copy */
mute #6E7681 /* secondary copy */
/* section.100 :: iterate() */

iterate()
five passes of refinement

  1. 01 seed choose a hash and project it onto the lattice
  2. 02 propagate walk every constraint, mark every implication
  3. 03 rotate advance each cell by its own per-index phase
  4. 04 render composite the lattice over the void background
  5. 05 observe scroll — the only input the solver listens to
cells 100
phases 100
animations 100
scripts 001
/* section.101 :: manual */

manual()
operating the page

Move the document. The grid responds to window.scrollY; nothing else. There are no cookies, no forms, no calls to anywhere outside this runtime. The page is a closed system. If you want a different image, scroll to a different position.

/* Q */ where does the moiré come from?
From the difference in rotation period between any two adjacent cells. It is not drawn; it is observed.
/* Q */ is the pattern random?
No. It is fully deterministic. The same scroll position produces the same image, every time.
/* Q */ what is the puzzle?
Reading the lattice and predicting the next state. The state is small enough to hold in mind — just barely.
// end of document // 100 cells, 1 scroll handler, 0 dependencies // ppuzzle.dev :: 2026