You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, gridSize = 4) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Using the tool description "Любимая игра" ("Favorite game"), we transform
// the image into an interactive classic 15-Puzzle (Sliding Puzzle) game.
// Validate inputs
let cols = parseInt(gridSize);
if (isNaN(cols) || cols < 2 || cols > 20) cols = 4;
const rows = cols;
const numTiles = cols * rows;
// Determine canvas dimensions while maintaining interactivity limit
const maxDim = 600;
let w = originalImg.width;
let h = originalImg.height;
if (w > maxDim || h > maxDim) {
const aspect = w / h;
if (w > h) { w = maxDim; h = maxDim / aspect; }
else { h = maxDim; w = maxDim * aspect; }
}
// Ensure perfect division integers to avoid floating pixel gaps
const tileW = Math.floor(w / cols);
const tileH = Math.floor(h / rows);
w = tileW * cols;
h = tileH * rows;
canvas.width = w;
canvas.height = h;
canvas.style.cursor = 'pointer';
canvas.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
canvas.style.borderRadius = '4px';
let board = Array.from({ length: numTiles }, (_, i) => i);
let emptyPos = numTiles - 1; // Start empty space at the very end
let isSolved = false;
function swap(i, j) {
let temp = board[i];
board[i] = board[j];
board[j] = temp;
}
// Shuffle the board with valid simulated sliding moves to guarantee solvability
function shuffle() {
let lastPos = -1;
// The higher the loops, the safer the shuffle but over 1000 is more than enough
for (let i = 0; i < cols * cols * 100; i++) {
let emptyCol = emptyPos % cols;
let emptyRow = Math.floor(emptyPos / cols);
let moves = [];
if (emptyCol > 0) moves.push(emptyPos - 1);
if (emptyCol < cols - 1) moves.push(emptyPos + 1);
if (emptyRow > 0) moves.push(emptyPos - cols);
if (emptyRow < rows - 1) moves.push(emptyPos + cols);
// Do not undo the immediately previous move to ensure a deep shuffle
moves = moves.filter(m => m !== lastPos);
if (moves.length === 0) continue;
let nextPos = moves[Math.floor(Math.random() * moves.length)];
swap(emptyPos, nextPos);
lastPos = emptyPos;
emptyPos = nextPos;
}
let solvedCount = 0;
for (let i = 0; i < numTiles; i++) {
if (board[i] === i) solvedCount++;
}
// Very unlikely, but reshuffle if randomly perfectly solved
if (solvedCount === numTiles) shuffle();
}
shuffle();
function draw() {
ctx.clearRect(0, 0, w, h);
let correctCount = 0;
for (let i = 0; i < numTiles; i++) {
if (board[i] === i) correctCount++;
let tileId = board[i];
// Don't draw the empty slot until it's solved
if (tileId === numTiles - 1 && !isSolved) {
// Draw a dark background block for the hole
ctx.fillStyle = '#111';
ctx.fillRect((i % cols) * tileW, Math.floor(i / cols) * tileH, tileW, tileH);
continue;
}
let sx = (tileId % cols) * (originalImg.width / cols);
let sy = Math.floor(tileId / cols) * (originalImg.height / rows);
let sWidth = originalImg.width / cols;
let sHeight = originalImg.height / rows;
let dx = (i % cols) * tileW;
let dy = Math.floor(i / cols) * tileH;
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, dx, dy, tileW, tileH);
if (!isSolved) {
// Draw tile borders to make it feel like 3D pieces
ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';
ctx.lineWidth = 1;
ctx.strokeRect(dx, dy, tileW, tileH);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(dx + 1, dy + tileH - 1);
ctx.lineTo(dx + 1, dy + 1);
ctx.lineTo(dx + tileW - 1, dy + 1);
ctx.stroke();
}
}
// Check if solved
if (correctCount === numTiles && !isSolved) {
isSolved = true;
// Draw one last time to fill the empty tile, then overlay winning text
setTimeout(() => draw(), 50);
return;
}
// If solved, overlay victory
if (isSolved) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, w, h);
const fontSize = Math.max(30, Math.floor(w / 12));
ctx.fillStyle = '#FFD700'; // Gold Color
ctx.font = `bold ${fontSize}px "Segoe UI", sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// "ПОБЕДА" relates accurately to "Любимая игра"
ctx.fillText('ПОБЕДА!', w / 2, h / 2 - fontSize * 0.3);
ctx.fillStyle = '#FFFFFF';
ctx.font = `bold ${Math.max(14, Math.floor(fontSize / 2.5))}px "Segoe UI", sans-serif`;
ctx.fillText('Click to play again', w / 2, h / 2 + fontSize * 0.8);
}
}
draw();
canvas.addEventListener('click', function(e) {
if (isSolved) {
// Reset and play again on click
isSolved = false;
shuffle();
draw();
return;
}
const rect = canvas.getBoundingClientRect();
// Adjust coordinate mappings for responsive CSS scale factors
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
const col = Math.floor(clickX / tileW);
const row = Math.floor(clickY / tileH);
// Prevent clicks physically out of canvas mathematical bounds
if (col < 0 || col >= cols || row < 0 || row >= rows) return;
const clickedPos = row * cols + col;
const emptyCol = emptyPos % cols;
const emptyRow = Math.floor(emptyPos / cols);
// A valid slider piece allows exactly 1 block of manhattan distance to the empty piece
const isAdjacent = Math.abs(col - emptyCol) + Math.abs(row - emptyRow) === 1;
if (isAdjacent) {
swap(clickedPos, emptyPos);
emptyPos = clickedPos; // Update the memory state
draw();
}
});
// Provide some minimal help instructions on hover / title
canvas.setAttribute('title', 'Sliding Game: Click adjacent tiles to move into the empty space!');
return canvas;
}
Apply Changes