Fragments of the Collection
A responsive 3×3 sliding-tile puzzle that uses paintings fetched from the Rijksmuseum public API, a quiet bridge between play and a museum collection.
Artwork images and metadata from the collection of the Rijksmuseum, Amsterdam, via their public API.
Rijksmuseum data services →Team
Project Focus
Role
Tools

Restoration as interaction
Each game pulls a real artwork from the Rijksmuseum collection and cuts it into nine tiles, eight fragments, one empty space. Gameplay becomes a way into a museum archive: arrow keys move the tiles into the gap, and when the grid matches the original, the painting is restored.
The puzzle doesn't simulate a museum experience. It uses one directly: live collection data, real metadata, actual proportions. The game is the archive.

A loop of play and discovery




The rules underneath the play
const randomPage = Math.floor(Math.random() * 100);
const API_URL = `https://www.rijksmuseum.nl/api/en/collection` +
`?key=${API_KEY}&format=json&ps=100&p=${randomPage}` +
`&type=painting&imgonly=True`;

function getValidPaintings(paintingsList) {
let validList = [];
for (let i = 0; i < paintingsList.length; i++) {
let item = paintingsList[i];
if (!item.webImage) continue;
let ratio = item.webImage.width / item.webImage.height;
// anything squarer than 0.8 or wider than 1.5 makes an unreadable 3x3
if (ratio >= 0.8 && ratio <= 1.5) {
validList.push(item);
}
}
return validList;
}// first call filled paintingList; this second one fetches the full record
function fetchSpecificPainting(id) {
let detailUrl =
`https://www.rijksmuseum.nl/api/en/collection/${id}` +
`?key=${API_KEY}&format=json`;
fetch(detailUrl)
.then(response => response.json())
.then(detailData => {
let artDetails = getArtDetails(detailData.artObject);
startGame(artDetails);
})
.catch(err => console.error(err));
}
function getArtDetails(art) {
// the archive is uneven, so every field falls back rather than showing blank
let description = "No description available.";
if (art.label && art.label.description) {
description = art.label.description;
} else if (art.plaqueDescriptionEnglish) {
description = art.plaqueDescriptionEnglish;
}
let date = "Unknown";
if (art.dating && art.dating.presentingDate) {
date = art.dating.presentingDate;
}
return { title: art.title, artist: art.principalOrFirstMaker, description, date };
}
// the grid takes the painting's own proportions
puzzleContainer.style.aspectRatio = `${details.width} / ${details.height}`;
/* every tile carries the WHOLE painting, three times oversized;
its letter decides which ninth of it shows */
.tile { background-size: 300% 300%; }
.tile.A { background-position: left top; }
.tile.B { background-position: center top; }
.tile.C { background-position: right top; }
.tile.D { background-position: left center; }let correctPuzzle = ["A", "B", "C", "D", "E", "F", "G", "H", "P"];
let shuffledPuzzle = ["D", "A", "C", "B", "H", "E", "P", "G", "F"];
function startGame(details) {
userPuzzle = [...shuffledPuzzle]; // a copy, so the original survives replay
playerPosition = userPuzzle.indexOf("P");
moveCount = -1;
updateScreenText(details);
createTiles(details.imageUrl);
updatePuzzle();
}// laid out as the board is: each cell lists the directions it allows
let movementPossibilities = [
["r", "d"], ["l", "r", "d"], ["l", "d"],
["u", "r", "d"], ["u", "d", "l", "r"], ["l", "u", "d"],
["u", "r"], ["l", "u", "r"], ["l", "u"]
];document.addEventListener('keydown', (keyEvent) => {
let updatedPosition = playerPosition;
// ArrowLeft pulls the tile on the RIGHT into the gap, so the gap moves right
if (keyEvent.key === "ArrowLeft" &&
movementPossibilities[playerPosition].includes("r")) {
updatedPosition = playerPosition + 1;
}
if (updatedPosition !== playerPosition) {
swapTiles(playerPosition, updatedPosition);
playerPosition = updatedPosition;
updatePuzzle();
}
});
function updateScreenText(details) {
document.getElementById("artwork-title").innerText = details.title;
document.getElementById("artist-name").innerText = details.artist;
document.getElementById("artwork-date").innerText = details.date;
document.getElementById("description").innerText = details.description;
document.getElementById("artwork-reference-image").src = details.imageUrl;
}© 2026 Ayşe Ceren Seçkin