How To Create A Sliding Puzzle in Webflow

CREATING THE PUZZLE

First, create your image. It should be 3×3 squares. Resulting in 9 boxes. The last needs to be a transparent image. Export and save.

Put in a container and give it a class of ” puzzle-container “

Next drop in the grid, create a 3 x 3 grid. Ensure the cell is the same size as your image square.

Rename the cell class to ” puzzle-piece “

Then in the custom attributes add:

  • Name: data-id
  • Value: 1

Do that for all the cell class of ” puzzle-piece ” but change the value to correspond to the puzzle place, 1 for the 1st piece, 2 for the second piece and so on. HOWEVER, for the last transparent piece. Give it a value of 0.

Then, drop the images in to the cells in the correct order.

Give the image of a class ” puzzle image “

Ensure the images have the alt class “Puzzle Piece 1”, the next “Puzzle Piece 2” and so on.

Add this code in the head

<style>

/* Hide the empty slot */
.puzzle-piece[data-id="0"] {
  display: none;
}

/* Optional: Hover effect */
.puzzle-piece:hover {
  opacity: 0.8;
}

/* Responsive design adjustments */
@media (max-width: 600px) {
  .puzzle-container {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1; /* Maintain square aspect ratio */
  }
}
</style>

Add this to the body. NOTE: Where I put “URL OF IMAGE 1” etc put the url of the actual image into it.

<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
  const gridSize = 3; // 3x3 grid
  const totalPieces = gridSize * gridSize;

  const puzzleContainer = document.querySelector('.puzzle-container');
  const pieces = Array.from(document.querySelectorAll('.puzzle-piece'));
  const congratsMessage = document.querySelector('.congrats-message');
  const restartButton = document.getElementById('restart-button');
  const closeButton = document.getElementById('close-button');
  const moveCountElement = document.querySelector('.move-count');
  const timerCountElement = document.querySelector('.timer-count'); // Select the timer-count element

  // Map piece numbers to image URLs
  const imageSources = {
    1: 'URL OF IMAGE 1',
    2: 'URL OF IMAGE 2',
    3: 'URL OF IMAGE 3',
    4: 'URL OF IMAGE 4',
    5: 'URL OF IMAGE 5',
    6: 'URL OF IMAGE 6',
    7: 'URL OF IMAGE 7',
    8: 'URL OF IMAGE 8',
  };

  let positions = [];
  let moveCount = 0; // Initialize move count
  let timerInterval = null;
  let elapsedTime = 0; // Time in seconds
  let isTimerStarted = false; // Flag to track if timer has started

  // Function to update the move count display
  function updateMoveCount() {
    if (moveCountElement) {
      moveCountElement.textContent = `Moves: ${moveCount}`;
      console.log(`Move count updated: ${moveCount}`);
    } else {
      console.error('Move count element not found. Ensure there is a div with class "move-count".');
    }
  }

  // Function to update the timer display
  function updateTimerCount() {
    if (timerCountElement) {
      timerCountElement.textContent = `Time: ${elapsedTime}s`;
      console.log(`Timer updated: ${elapsedTime}s`);
    } else {
      console.error('Timer count element not found. Ensure there is a div with class "timer-count".');
    }
  }

  // Function to start the timer
  function startTimer() {
    // Prevent multiple timers
    if (isTimerStarted) return;

    isTimerStarted = true;
    console.log('Timer started.');

    // Start a new timer
    timerInterval = setInterval(() => {
      elapsedTime++;
      updateTimerCount();
    }, 1000); // Update every second
  }

  // Function to stop the timer
  function stopTimer() {
    if (timerInterval) {
      clearInterval(timerInterval);
      timerInterval = null;
      console.log('Timer stopped.');
    }
  }

  // Function to initialize the puzzle
  function initPuzzle() {
    // Initialize positions array with IDs from 1 to 8 and 0 for empty slot
    positions = [];
    for (let i = 0; i < totalPieces; i++) {
      positions.push(i === totalPieces - 1 ? 0 : i + 1); // [1,2,3,4,5,6,7,8,0]
    }

    // Shuffle positions to start the game
    positions = shuffle(positions.slice());

    // Ensure that there is exactly one empty slot
    if (positions.filter(id => id === 0).length !== 1) {
      console.error('There must be exactly one empty slot (data-id="0").');
      return;
    }

    // Map shuffled positions to the puzzle pieces
    pieces.forEach((piece, index) => {
      const posIndex = positions[index];
      piece.dataset.position = index.toString(); // Current grid position

      if (posIndex === 0) {
        // Empty slot
        piece.dataset.id = '0';
        // Use a transparent image for the empty slot
        const img = piece.querySelector('img');
        if (img) {
          img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; // 1x1 transparent pixel
          img.alt = '';
        } else {
          console.error(`Image element not found in piece with data-id 0`);
        }
      } else {
        piece.dataset.id = posIndex.toString();

        // Update the image source based on the shuffled position
        const img = piece.querySelector('img');
        if (img) {
          img.src = imageSources[posIndex];
          img.alt = `Puzzle Piece ${posIndex}`;
        } else {
          console.error(`Image element not found in piece with data-id ${posIndex}`);
        }
      }

      // Set the grid position
      setPosition(piece, index);
    });

    // Hide the congratulatory message if it's visible
    if (congratsMessage) {
      congratsMessage.style.display = 'none';
    } else {
      console.error('Congratulatory message div not found. Ensure there is a div with class "congrats-message".');
    }

    // Reset move count and timer
    moveCount = 0;
    updateMoveCount();
    stopTimer();
    isTimerStarted = false;
    elapsedTime = 0;
    updateTimerCount();

    console.log('Puzzle initialized with positions:', positions);
  }

  // Initialize the puzzle
  initPuzzle();

  // Add click event listeners to the pieces
  pieces.forEach(piece => {
    piece.addEventListener('click', () => {
      movePiece(piece);
    });
  });

  // Event listener for the restart button
  if (restartButton) {
    restartButton.addEventListener('click', () => {
      initPuzzle();
    });
  } else {
    console.error('Restart button not found. Ensure there is a button with ID "restart-button".');
  }

  // Event listener for the close button
  if (closeButton) {
    closeButton.addEventListener('click', () => {
      if (congratsMessage) {
        congratsMessage.style.display = 'none';
        console.log('Congratulatory message closed.');
      } else {
        console.error('Congratulatory message div not found.');
      }
    });
  } else {
    console.error('Close button not found. Ensure there is a button with ID "close-button".');
  }

  // Function to set the position of a piece in the grid
  function setPosition(piece, positionIndex) {
    piece.style.gridColumnStart = (positionIndex % gridSize) + 1;
    piece.style.gridRowStart = Math.floor(positionIndex / gridSize) + 1;
    piece.dataset.position = positionIndex.toString();

    // Show the piece
    piece.style.display = '';
  }

  // Function to move a piece if it's adjacent to the empty slot
  function movePiece(piece) {
    const emptyPiece = pieces.find(p => p.dataset.id === '0');
    if (!emptyPiece) {
      console.error('Empty piece not found. Ensure there is a puzzle-piece with data-id="0".');
      return;
    }

    const emptyIndex = parseInt(emptyPiece.dataset.position);
    const pieceIndex = parseInt(piece.dataset.position);

    console.log(`Attempting to move piece ${piece.dataset.id} from position ${pieceIndex} to empty position ${emptyIndex}`);

    const isAdjacent = checkAdjacent(pieceIndex, emptyIndex);
    if (isAdjacent) {
      // Swap positions
      swapPieces(piece, emptyPiece);

      // Increment move count
      moveCount++;
      updateMoveCount();

      // Start the timer on the first move
      if (!isTimerStarted) {
        startTimer();
      }

      // Check if the puzzle is solved
      if (checkWin()) {
        stopTimer(); // Stop the timer when the puzzle is solved
        showCongratsMessage();
      }
    } else {
      console.log(`Piece ${piece.dataset.id} is not adjacent to the empty slot.`);
    }
  }

  // Function to swap two pieces
  function swapPieces(piece1, piece2) {
    const pos1 = parseInt(piece1.dataset.position);
    const pos2 = parseInt(piece2.dataset.position);

    // Swap positions in the positions array
    [positions[pos1], positions[pos2]] = [positions[pos2], positions[pos1]];

    console.log(`Swapped piece ${piece1.dataset.id} (position ${pos1}) with piece ${piece2.dataset.id} (position ${pos2})`);

    // Update dataset positions
    piece1.dataset.position = pos2.toString();
    piece2.dataset.position = pos1.toString();

    // Update positions in the DOM
    setPosition(piece1, pos2);
    setPosition(piece2, pos1);
  }

  // Function to check if two indices are adjacent in the grid
  function checkAdjacent(index1, index2) {
    const row1 = Math.floor(index1 / gridSize);
    const col1 = index1 % gridSize;
    const row2 = Math.floor(index2 / gridSize);
    const col2 = index2 % gridSize;

    // Check if the positions are next to each other
    const isAdjacent = (Math.abs(row1 - row2) + Math.abs(col1 - col2)) === 1;
    console.log(`Checking adjacency between positions ${index1} and ${index2}: ${isAdjacent}`);
    return isAdjacent;
  }

  // Function to check if the puzzle is solved
  function checkWin() {
    const correctOrder = [1, 2, 3, 4, 5, 6, 7, 8, 0];
    for (let i = 0; i < positions.length; i++) {
      if (positions[i] !== correctOrder[i]) {
        return false;
      }
    }
    console.log('Puzzle solved!');
    return true;
  }

  // Function to show the congratulatory message
  function showCongratsMessage() {
    if (congratsMessage) {
      congratsMessage.style.display = 'flex'; // Use 'flex' to match the CSS styling
      console.log('Congratulatory message displayed.');
    } else {
      console.error('Congratulatory message div not found. Ensure there is a div with class "congrats-message".');
    }
  }

  // Function to shuffle the positions array
  function shuffle(array) {
    let currentIndex = array.length, randomIndex;

    // While there remain elements to shuffle
    while (currentIndex !== 0) {
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex--;

      // Swap elements
      [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
    }

    // Ensure the puzzle is solvable
    if (!isSolvable(array)) {
      // Swap two tiles (excluding the empty slot) to make it solvable
      if (array[0] !== 0 && array[1] !== 0) {
        [array[0], array[1]] = [array[1], array[0]];
        console.log('Shuffled array was unsolvable. Swapped first two tiles to make it solvable.');
      } else {
        [array[1], array[2]] = [array[2], array[1]];
        console.log('Shuffled array was unsolvable. Swapped second and third tiles to make it solvable.');
      }
    }

    console.log('Shuffled positions:', array);
    return array;
  }

  // Function to check if the shuffled puzzle is solvable
  function isSolvable(array) {
    let inversions = 0;
    for (let i = 0; i < array.length; i++) {
      for (let j = i + 1; j < array.length; j++) {
        if (array[i] && array[j] && array[i] > array[j]) {
          inversions++;
        }
      }
    }

    const emptyRow = gridSize - Math.floor(array.indexOf(0) / gridSize);

    if (gridSize % 2 !== 0) {
      // Odd grid size, solvable if inversions are even
      const solvable = inversions % 2 === 0;
      console.log(`Odd grid size. Inversions: ${inversions}. Solvable: ${solvable}`);
      return solvable;
    } else {
      // Even grid size
      if (emptyRow % 2 === 0) {
        const solvable = inversions % 2 !== 0;
        console.log(`Even grid size. Empty row from bottom: ${emptyRow}. Inversions: ${inversions}. Solvable: ${solvable}`);
        return solvable;
      } else {
        const solvable = inversions % 2 === 0;
        console.log(`Even grid size. Empty row from bottom: ${emptyRow}. Inversions: ${inversions}. Solvable: ${solvable}`);
        return solvable;
      }
    }
  }
});
</script>

CONGRATULATIONS MESSAGE

I also added the function to give a congratulations message and to restart the puzzle or not.

Create a Div with the class “congrats-message”

Throw another Div within it and class the class “congrats-content”

Put your message in there.

Then create a button for restart, and give it an ID (which can be found in the settings) of “restart-button”

Then create another button but with the ID “close-button”

ADDING STATS

I also included stats for the puzzle, which are a timer and number of moves.

Timer

Create a div with the class “timer”, then throw a paragraph within it, give the class “timer-count” for the paragraph. Throw in dummy text, like timer.

Moves

Create a div with the class “move-counter”, then throw a paragraph within it, give it the class “move-count” for the paragraph. Throw in dummy text, like move count.

And that should be it!


Comments

Leave a Reply