Put the illustration back together.
Drag a piece onto another to swap them.
puzzlescript
(function(){ function init(){ var root = document.getElementById('pzh'); if (!root || root.dataset.ready) return; root.dataset.ready = '1'; var IMG = root.getAttribute('data-img'); // on narrow screens hard mode uses fewer pieces var small = window.matchMedia('(max-width:600px)'); function levels(){ return { easy:{cols:3,rows:3}, hard: small.matches ? {cols:4,rows:3} : {cols:5,rows:4} }; } var board = root.querySelector('.pzh-board'); var timeEl = root.querySelector('.pzh-time'); var movesEl = root.querySelector('.pzh-moves'); var bestEl = root.querySelector('.pzh-best'); var winText = root.querySelector('.pzh-win-text'); var live = root.querySelector('.pzh-sr'); var hardBtn = root.querySelector('[data-act="hard"]'); var peekBtn = root.querySelector('[data-act="peek"]'); root.style.setProperty('--img', 'url("' + IMG + '")'); // read proportions from the image var probe = new Image(); probe.onload = function(){ if (probe.naturalWidth) root.style.setProperty('--ratio', probe.naturalWidth + ' / ' + probe.naturalHeight); }; probe.onerror = function(){ var msg = document.createElement('p'); msg.style.cssText = 'position:absolute;inset:0;margin:0;display:grid;place-items:center;padding:16px;text-align:center;font-size:1rem;background:#fff;z-index:10;'; msg.textContent = 'Image not found: ' + IMG; board.appendChild(msg); }; probe.src = IMG; var level = 'easy', cols = 3, rows = 3; var order = [], pieces = []; var moves = 0, startAt = 0, tick = null, solved = false, selected = null; function fmt(ms){ var s = Math.floor(ms/1000); return String(Math.floor(s/60)).padStart(2,'0') + ':' + String(s%60).padStart(2,'0'); } function getBest(){ try { return JSON.parse(localStorage.getItem('pzh-best-' + cols + 'x' + rows)); } catch(e){ return null; } } function setBest(v){ try { localStorage.setItem('pzh-best-' + cols + 'x' + rows, JSON.stringify(v)); } catch(e){} } function showBest(){ var b = getBest(); if (b && b.ms){ bestEl.hidden = false; bestEl.textContent = 'Best: ' + fmt(b.ms) + ', ' + b.moves + ' moves'; } else bestEl.hidden = true; } function shuffled(n){ var a; do { a = []; for (var i = 0; i < n; i++) a.push(i); for (var k = n-1; k > 0; k--){ var j = Math.floor(Math.random()*(k+1)); var t = a[k]; a[k] = a[j]; a[j] = t; } } while (a.filter(function(v,i){ return v === i; }).length > Math.floor(n/5)); return a; } function build(){ var L = levels()[level]; cols = L.cols; rows = L.rows; root.style.setProperty('--cols', cols); root.style.setProperty('--rows', rows); pieces.forEach(function(p){ p.remove(); }); pieces = []; for (var id = 0; id < cols*rows; id++){ var c = id % cols, r = Math.floor(id / cols); var el = document.createElement('button'); el.type = 'button'; el.className = 'pzh-piece'; el.dataset.id = id; var face = document.createElement('span'); face.className = 'pzh-face'; face.style.setProperty('--bx', (cols > 1 ? c/(cols-1)*100 : 0) + '%'); face.style.setProperty('--by', (rows > 1 ? r/(rows-1)*100 : 0) + '%'); el.appendChild(face); bindPiece(el); board.appendChild(el); pieces.push(el); } reset(); } function place(){ order.forEach(function(id, cell){ var el = pieces[id]; el.style.setProperty('--c', cell % cols); el.style.setProperty('--r', Math.floor(cell / cols)); el.setAttribute('aria-label', 'Piece in row ' + (Math.floor(cell/cols)+1) + ', column ' + (cell%cols+1)); }); } function reset(){ clearInterval(tick); tick = null; startAt = 0; moves = 0; solved = false; clearSelection(); root.classList.remove('is-solved'); pieces.forEach(function(p){ p.disabled = false; }); order = shuffled(cols*rows); place(); timeEl.textContent = '00:00'; movesEl.textContent = '0'; showBest(); } function startClock(){ if (tick) return; startAt = Date.now(); tick = setInterval(function(){ timeEl.textContent = fmt(Date.now() - startAt); }, 250); } function swapCells(a, b){ if (a === b) return; startClock(); var t = order[a]; order[a] = order[b]; order[b] = t; moves++; movesEl.textContent = moves; place(); if (order.every(function(v,i){ return v === i; })) win(); } function win(){ solved = true; clearInterval(tick); tick = null; var ms = Date.now() - startAt; timeEl.textContent = fmt(ms); root.classList.add('is-solved'); pieces.forEach(function(p){ p.disabled = true; }); var prev = getBest(), isBest = !prev || ms < prev.ms; if (isBest) setBest({ms:ms, moves:moves}); winText.textContent = 'Solved in ' + fmt(ms) + ' with ' + moves + ' moves' + (isBest && prev ? ', new best' : '') + '.'; live.textContent = winText.textContent; showBest(); } function cellOf(el){ return order.indexOf(+el.dataset.id); } function bindPiece(el){ el.addEventListener('pointerdown', function(e){ if (solved || e.button > 0) return; e.preventDefault(); el.setPointerCapture(e.pointerId); var rect = board.getBoundingClientRect(); var sx = e.clientX, sy = e.clientY, moved = false, target = null; function onMove(ev){ var dx = ev.clientX - sx, dy = ev.clientY - sy; if (!moved && Math.hypot(dx,dy) < 5) return; if (!moved){ moved = true; el.classList.add('is-dragging'); clearSelection(); } el.style.setProperty('--dx', dx + 'px'); el.style.setProperty('--dy', dy + 'px'); var cx = Math.min(cols-1, Math.max(0, Math.floor((ev.clientX - rect.left) / (rect.width/cols)))); var cy = Math.min(rows-1, Math.max(0, Math.floor((ev.clientY - rect.top) / (rect.height/rows)))); var t = pieces[order[cy*cols + cx]]; if (target && target !== t) target.classList.remove('is-target'); target = (t !== el) ? t : null; if (target) target.classList.add('is-target'); } function onUp(){ el.removeEventListener('pointermove', onMove); el.removeEventListener('pointerup', onUp); el.removeEventListener('pointercancel', onUp); if (target) target.classList.remove('is-target'); if (!moved){ tapSelect(el); return; } var from = cellOf(el); el.classList.remove('is-dragging'); el.style.removeProperty('--dx'); el.style.removeProperty('--dy'); if (target) swapCells(from, cellOf(target)); } el.addEventListener('pointermove', onMove); el.addEventListener('pointerup', onUp); el.addEventListener('pointercancel', onUp); }); el.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); tapSelect(el); } if (e.key === 'Escape') clearSelection(); }); } function tapSelect(el){ if (solved) return; if (!selected){ selected = el; el.classList.add('is-selected'); live.textContent = 'Piece selected. Choose where to move it.'; return; } if (selected === el){ clearSelection(); return; } var a = cellOf(selected), b = cellOf(el); clearSelection(); swapCells(a, b); } function clearSelection(){ if (selected) selected.classList.remove('is-selected'); selected = null; } root.querySelector('[data-act="shuffle"]').addEventListener('click', reset); root.querySelector('[data-act="again"]').addEventListener('click', reset); hardBtn.addEventListener('click', function(){ level = level === 'easy' ? 'hard' : 'easy'; hardBtn.setAttribute('aria-pressed', level === 'hard'); build(); }); if (small.addEventListener) small.addEventListener('change', function(){ if (level === 'hard') build(); }); function peekOn(){ if (!solved) root.classList.add('is-peeking'); } function peekOff(){ root.classList.remove('is-peeking'); } peekBtn.addEventListener('pointerdown', peekOn); ['pointerup','pointerleave','pointercancel','blur'].forEach(function(ev){ peekBtn.addEventListener(ev, peekOff); }); peekBtn.addEventListener('keydown', function(e){ if (e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); peekOn(); } }); peekBtn.addEventListener('keyup', peekOff); build(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();})();