diff --git a/README.md b/README.md index 74723da..51952b9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A lightweight, browser-based tool to build a tile library and paint reusable pat - Support rectangular tiles (multi-cell) - Pin a floor tile row at the bottom - Fill any row with a chosen tile +- Apply a brick-block layout across rows - Auto-save tile library in the browser - Import/export the tile library as JSON - Export JSON for handing off to a tiler @@ -59,5 +60,6 @@ Open `index.html` directly in your browser, or use the optional local server scr - Shift-click or right click to erase a tile. - Enable “Floor row” to lock the bottom row to a chosen tile. - Use “Fill row” to quickly paint a full row with a tile. +- Use “Apply brick” to stagger rows in a brick pattern. - Tile library changes auto-save to your browser storage. - If you use image tiles, keep images square for best results. diff --git a/app.js b/app.js index ef3aef3..6e95017 100644 --- a/app.js +++ b/app.js @@ -19,6 +19,11 @@ const floorTileSelect = document.getElementById("floor-tile"); const fillRowInput = document.getElementById("fill-row"); const fillRowTileSelect = document.getElementById("fill-row-tile"); const fillRowButton = document.getElementById("fill-row-button"); +const brickStartInput = document.getElementById("brick-start"); +const brickEndInput = document.getElementById("brick-end"); +const brickTileSelect = document.getElementById("brick-tile"); +const brickStaggerInput = document.getElementById("brick-stagger"); +const brickApplyButton = document.getElementById("brick-apply"); const applyGridButton = document.getElementById("apply-grid"); const clearGridButton = document.getElementById("clear-grid"); @@ -148,11 +153,13 @@ const setActiveTile = (tileId) => { const renderFloorOptions = () => { floorTileSelect.innerHTML = ""; fillRowTileSelect.innerHTML = ""; + brickTileSelect.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Select tile"; floorTileSelect.append(placeholder); fillRowTileSelect.append(placeholder.cloneNode(true)); + brickTileSelect.append(placeholder.cloneNode(true)); tiles.forEach((tile) => { const option = document.createElement("option"); @@ -160,6 +167,7 @@ const renderFloorOptions = () => { option.textContent = tile.name; floorTileSelect.append(option); fillRowTileSelect.append(option.cloneNode(true)); + brickTileSelect.append(option.cloneNode(true)); }); if (floorTileId && !tiles.some((tile) => tile.id === floorTileId)) { @@ -167,6 +175,7 @@ const renderFloorOptions = () => { } floorTileSelect.value = floorTileId ?? ""; fillRowTileSelect.value = activeTileId ?? ""; + brickTileSelect.value = activeTileId ?? ""; }; const renderTiles = () => { @@ -274,18 +283,75 @@ const drawGridLines = () => { patternContext.strokeStyle = gridLineColor; patternContext.lineWidth = 1; + const lastRowIndex = config.rows - 1; + const floorTile = floorTileId ? getTileById(floorTileId) : null; + for (let row = 0; row <= config.rows; row += 1) { - patternContext.beginPath(); - patternContext.moveTo(0, row * config.tileSize + 0.5); - patternContext.lineTo(patternCanvas.width, row * config.tileSize + 0.5); - patternContext.stroke(); + if (row === 0 || row === config.rows) { + patternContext.beginPath(); + patternContext.moveTo(0, row * config.tileSize + 0.5); + patternContext.lineTo(patternCanvas.width, row * config.tileSize + 0.5); + patternContext.stroke(); + continue; + } + + let segmentStart = 0; + for (let col = 0; col < config.cols; col += 1) { + const topId = grid[row - 1][col]; + const bottomId = grid[row][col]; + const shouldSkip = topId && bottomId && topId === bottomId; + if (shouldSkip) { + if (segmentStart < col) { + patternContext.beginPath(); + patternContext.moveTo(segmentStart * config.tileSize, row * config.tileSize + 0.5); + patternContext.lineTo(col * config.tileSize, row * config.tileSize + 0.5); + patternContext.stroke(); + } + segmentStart = col + 1; + } + } + if (segmentStart < config.cols) { + patternContext.beginPath(); + patternContext.moveTo(segmentStart * config.tileSize, row * config.tileSize + 0.5); + patternContext.lineTo(patternCanvas.width, row * config.tileSize + 0.5); + patternContext.stroke(); + } } for (let col = 0; col <= config.cols; col += 1) { - patternContext.beginPath(); - patternContext.moveTo(col * config.tileSize + 0.5, 0); - patternContext.lineTo(col * config.tileSize + 0.5, patternCanvas.height); - patternContext.stroke(); + if (col === 0 || col === config.cols) { + patternContext.beginPath(); + patternContext.moveTo(col * config.tileSize + 0.5, 0); + patternContext.lineTo(col * config.tileSize + 0.5, patternCanvas.height); + patternContext.stroke(); + continue; + } + + let segmentStart = 0; + for (let row = 0; row < config.rows; row += 1) { + const leftId = grid[row][col - 1]; + const rightId = grid[row][col]; + const isFloorRow = floorEnabled && row === lastRowIndex; + const floorSkip = + isFloorRow && floorTile && floorTile.width > 1 && col % floorTile.width !== 0; + const shouldSkip = (leftId && rightId && leftId === rightId) || floorSkip; + + if (shouldSkip) { + if (segmentStart < row) { + patternContext.beginPath(); + patternContext.moveTo(col * config.tileSize + 0.5, segmentStart * config.tileSize); + patternContext.lineTo(col * config.tileSize + 0.5, row * config.tileSize); + patternContext.stroke(); + } + segmentStart = row + 1; + } + } + if (segmentStart < config.rows) { + patternContext.beginPath(); + patternContext.moveTo(col * config.tileSize + 0.5, segmentStart * config.tileSize); + patternContext.lineTo(col * config.tileSize + 0.5, patternCanvas.height); + patternContext.stroke(); + } } }; @@ -430,6 +496,41 @@ const fillRowWithTile = (rowIndex, tileId) => { syncPatternPreview(); }; +const fillBrickRows = (startRow, endRow, tileId, stagger) => { + const tile = getTileById(tileId); + if (!tile) { + return; + } + const maxRow = floorEnabled ? config.rows - 2 : config.rows - 1; + const start = clampNumber(startRow, 0, maxRow); + const end = clampNumber(endRow, start, maxRow); + const tileWidth = clampNumber(tile.width, 1, config.cols); + + for (let row = start; row <= end; row += 1) { + const offset = stagger && (row - start) % 2 === 1 ? Math.floor(tileWidth / 2) : 0; + const toRemove = new Set(); + for (let col = 0; col < config.cols; col += 1) { + const placementId = grid[row][col]; + if (placementId) { + toRemove.add(placementId); + } + } + toRemove.forEach((placementId) => clearPlacement(placementId)); + + let col = 0; + if (offset > 0) { + addPlacement(row, 0, tile.id, clampNumber(offset, 1, config.cols), 1); + col = offset; + } + for (; col < config.cols; col += tileWidth) { + const remaining = config.cols - col; + addPlacement(row, col, tile.id, clampNumber(tileWidth, 1, remaining), 1); + } + } + renderCanvas(); + syncPatternPreview(); +}; + const paintArea = (row, col, tileId, width = 1, height = 1) => { const maxRow = floorEnabled ? config.rows - 2 : config.rows - 1; if (row > maxRow || col >= config.cols) { @@ -516,6 +617,9 @@ applyGridButton.addEventListener("click", () => { gridRowsInput.value = config.rows; gridColsInput.value = config.cols; gridSizeInput.value = config.tileSize; + fillRowInput.max = String(config.rows); + brickStartInput.max = String(config.rows); + brickEndInput.max = String(config.rows); initGrid(); renderCanvas(); syncPatternPreview(); @@ -550,6 +654,16 @@ fillRowButton.addEventListener("click", () => { fillRowWithTile(rowIndex, tileId); }); +brickApplyButton.addEventListener("click", () => { + const startRow = Number(brickStartInput.value) - 1; + const endRow = Number(brickEndInput.value) - 1; + const tileId = brickTileSelect.value || activeTileId; + if (!tileId) { + return; + } + fillBrickRows(startRow, endRow, tileId, brickStaggerInput.checked); +}); + const readFileAsDataUrl = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); @@ -744,6 +858,9 @@ importJsonButton.addEventListener("click", () => { gridSizeInput.value = config.tileSize; floorEnabledInput.checked = floorEnabled; fillRowInput.value = "1"; + fillRowInput.max = String(config.rows); + brickStartInput.max = String(config.rows); + brickEndInput.max = String(config.rows); tiles = normalizeTiles(payload.tiles); @@ -824,3 +941,6 @@ loadLibraryFromStorage(); renderTiles(); renderCanvas(); syncPatternPreview(); +fillRowInput.max = String(config.rows); +brickStartInput.max = String(config.rows); +brickEndInput.max = String(config.rows); diff --git a/index.html b/index.html index 44bbffa..6a0df55 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,25 @@ +
+ + + + + +