Ayşe Ceren Seçkin
← Selected Works

Fragments of the Collection

Creative Coding · 2025

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

Solo

Project Focus

Creative CodingUI/UX DesignProblem Solving

Role

Creative CodingUI/UX DesignProblem Solving

Tools

JavaScriptHTML/CSSRijksmuseum API
Fragments of the Collection
Concept

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.

The Interaction

A loop of play and discovery

Start
01
Start
A painting is retrieved from the Rijksmuseum archive and prepared as a fragmented puzzle state before interaction begins.
Play
02
Play
Tiles slide into the empty space, gradually reconstructing the artwork through constrained movement and rearrangement.
Read
03
Read
A collapsible reference panel displays the artwork's title, artist, date, and description alongside the reconstruction.
Finish
04
Finish
Once all fragments return to their original position, the artwork is restored and the system transitions to a new painting.
0:00 / 0:00
Game Logic

The rules underneath the play

A Different Hundred Each Time
Each session queries a random page of the collection instead of the first results, so the pool of candidate paintings changes between plays rather than drawing from the same hundred works.
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`;
Artwork Selection
Artworks are filtered before entering the system to exclude extreme proportions that would produce unstable or unreadable puzzle states.
Artwork SelectionArtwork Selection
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;
}
Two Requests, One Painting
The collection endpoint returns a hundred works at a time, but only a thumbnail and a title for each, so the chosen painting is requested a second time by its own id for the full record. The first call decides what to play, the second supplies what to read. Where the archive holds no description or no date, the card says so rather than showing an empty field.
// 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 };
}
Reconstruction Logic
The puzzle grid adapts to the original dimensions of each painting rather than forcing a fixed format, preserving the spatial composition during reconstruction.
Reconstruction Logic
// 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; }
Guaranteed Solvability
The game opens from one fixed arrangement, checked by hand, rather than a random shuffle. Only half of the possible tile arrangements in a sliding puzzle can ever be solved, so shuffling at random would have handed roughly every second player a painting that could not be restored.
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();
}
Constrained Movement
Each of the nine positions carries its own list of the directions it may move in. The grid is really a flat array of nine, so without that table a tile on the right edge would slide off it and reappear at the start of the row below.
// 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"]
];
Pushing Tiles, Not the Gap
The arrow keys move a tile into the empty space rather than moving the space itself. Pressing left pushes the piece on the right into the gap, so the player thinks about the fragment they want to move rather than about the empty tile.
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();
    }
});
Parallel Reference
A collapsible reference panel displays the original artwork alongside metadata during gameplay, functioning both as a reconstruction aid and a secondary information layer.
Parallel Reference
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