2d HTML5 game: How to dynamically add collision shapes to individual tiles?

I’m trying to write a multiplayer shooter game in HTML5 with Canvas. I have a function that loads 50 x 50 px map tiles like this in game.js:

const rotationKeys = {0: 0, 1: 90, 2: 180, 3: 270}

const tilemap = "0C0003 0W1003 0W0003 0W2003 0W3003 0W4003 0W0003".split(" "); // example

let tiles = {};

let collisionData = {}

function loadTiles() {
    for (const tileIndex in tilemap) {
        const rawTile = tilemap[tileIndex].substring(0, 3);
        const tileImg = new Image();
        tileImg.src = `sprites/maps/tiles/${rawTile}.png`;
        tiles[rawTile] = tileImg;
    }
}

function drawMap() {
    const tileSize = 50;
    gameSettings.mapWidth = 35;
    gameSettings.mapHeight = 24;

    const startX = Math.max(0, Math.floor(camera.x / (tileSize * scaleFactor)));
    const startY = Math.max(0, Math.floor(camera.y / (tileSize * scaleFactor)));

    const endX = Math.min(gameSettings.mapWidth, startX + Math.ceil(canvas.width / (tileSize * scaleFactor)) + 1);
    const endY = Math.min(gameSettings.mapHeight, startY + Math.ceil(canvas.height / (tileSize * scaleFactor)) + 1);
  
    for (let y = startY; y < endY; y++) {
      for (let x = startX; x < endX; x++) {
        const index = y * gameSettings.mapWidth + x;
        const tile = tilemap[index];
        const rotation = tile[3]; // Get rotation type for this tile
        const flip = tile[4]; // Get flip type for this tile
        const collision = parseInt(tile[5]); // Get collision type for this tile
  
        // Apply rotation transformation
        ctx.save(); // Save the current canvas state
        ctx.translate((x + 0.5) * tileSize * scaleFactor, (y + 0.5) * tileSize * scaleFactor); // Translate to the center of the tile
        ctx.rotate(rotationKeys[rotation] * Math.PI / 180); // Rotate by the specified angle (converted to radians)
  
        // Apply flip transformation
        var xScale = 1;
        var yScale = 1;
        if (flip == 1 || flip == 3) {
          xScale = -1;
        }
        if (flip == 2 || flip == 3) {
          yScale = -1;
        }
        ctx.scale(xScale, yScale);
  
        try {
            ctx.drawImage(tiles[tile.substring(0, 3)], -0.5 * tileSize * scaleFactor, -0.5 * tileSize * scaleFactor, tileSize * scaleFactor, tileSize * scaleFactor); // Draw the rotated image
        } catch (error) {
            console.error(`Failed to draw tile at (${x}, ${y}):`, error);
            console.log("Responsible tile:", tile);
        }
        ctx.restore(); // Restore the saved canvas state to undo the rotation transformation
      }
    }
}

There are 9 types of collision, where 0, 1 and 2 means the entire tile can be walked on. Where 3, 4 means that the entire tile is an obstacle (a wall for example). And the rest are basic rectangle shapes that are applied to the top of the 50×50 tile. The width of some shapes is 52, which exceed the tile size itself, resulting in extra edges. There is one shape that is a mixture of a rectangle and square.

Naturally, the collisions also need to go through the flip and rotation transformations, just like the tiles do.

My question is how can I do this? We should be able to detect that collision and not allow the player to go any further. He may continue to walk against the collision, it just shouldn’t let it walk on it.

I’ve tried something like this, but that didnt work as expected (misplacement and rotations/flips didnt work):

function generateCollisions(tileName, rotation, flip, collision) {
  // Check if collision data for this tile name already exists
  if (!(tileName in collisionData)) {

    // Determine collision shape based on collision type
    let collisionShape = null;

    switch (collision) {
      case 0:
      case 1:
      case 2:
        // The entire tile is walkable
        collisionShape = null; // No need to define a collision shape for walkable tiles
        break;
      case 3:
      case 4:
        // The entire tile is an obstacle
        collisionShape = { x: -1, y: -1, width: 52, height: 52 };
        break;
      case 5:
        collisionShape = { x: -1, y: 0, width: 52, height: 38 };
        break;
      case 6:
        collisionShape = { x: -1, y: 0, width: 52, height: 26 };
        break;
      case 7:
        collisionShape = { x: -1, y: 0, width: 52, height: 13 };
        break;
      case 8:
        collisionShape = { x: 0, y: 0, width: 26, height: 26 };
        break;
      case 9:
        collisionShape = { x: 24, y: 24, width: 26, height: 26 }; // this is a more complicated shape, should actually be a mix of rectangle of longer height and squre on top
        break;
    }

    if (collisionShape !== null && typeof collisionShape === 'object') {
      // Apply rotation and flip transformations to the collision shape
      const angleInRadians = rotationKeys[rotation] * Math.PI / 180;

      // Apply flip transformations
      if (flip === 1 || flip === 3) {
        collisionShape.x = 50 - collisionShape.x - collisionShape.width;
      }
      if (flip === 2 || flip === 3) {
        collisionShape.y = 50 - collisionShape.y - collisionShape.height;
      }

      // Apply rotation transformations
      const rotatedX = collisionShape.x * Math.cos(angleInRadians) - collisionShape.y * Math.sin(angleInRadians);
      const rotatedY = collisionShape.x * Math.sin(angleInRadians) + collisionShape.y * Math.cos(angleInRadians);

      // Update the collision shape with the transformed coordinates
      collisionShape.x = rotatedX;
      collisionShape.y = rotatedY;
    }

    collisionData[tileName] = collisionShape;
  }
}