brickblock
This commit is contained in:
@@ -8,6 +8,7 @@ A lightweight, browser-based tool to build a tile library and paint reusable pat
|
|||||||
- Support rectangular tiles (multi-cell)
|
- Support rectangular tiles (multi-cell)
|
||||||
- Pin a floor tile row at the bottom
|
- Pin a floor tile row at the bottom
|
||||||
- Fill any row with a chosen tile
|
- Fill any row with a chosen tile
|
||||||
|
- Apply a brick-block layout across rows
|
||||||
- Auto-save tile library in the browser
|
- Auto-save tile library in the browser
|
||||||
- Import/export the tile library as JSON
|
- Import/export the tile library as JSON
|
||||||
- Export JSON for handing off to a tiler
|
- 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.
|
- Shift-click or right click to erase a tile.
|
||||||
- Enable “Floor row” to lock the bottom row to a chosen 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 “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.
|
- Tile library changes auto-save to your browser storage.
|
||||||
- If you use image tiles, keep images square for best results.
|
- If you use image tiles, keep images square for best results.
|
||||||
|
|||||||
120
app.js
120
app.js
@@ -19,6 +19,11 @@ const floorTileSelect = document.getElementById("floor-tile");
|
|||||||
const fillRowInput = document.getElementById("fill-row");
|
const fillRowInput = document.getElementById("fill-row");
|
||||||
const fillRowTileSelect = document.getElementById("fill-row-tile");
|
const fillRowTileSelect = document.getElementById("fill-row-tile");
|
||||||
const fillRowButton = document.getElementById("fill-row-button");
|
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 applyGridButton = document.getElementById("apply-grid");
|
||||||
const clearGridButton = document.getElementById("clear-grid");
|
const clearGridButton = document.getElementById("clear-grid");
|
||||||
|
|
||||||
@@ -148,11 +153,13 @@ const setActiveTile = (tileId) => {
|
|||||||
const renderFloorOptions = () => {
|
const renderFloorOptions = () => {
|
||||||
floorTileSelect.innerHTML = "";
|
floorTileSelect.innerHTML = "";
|
||||||
fillRowTileSelect.innerHTML = "";
|
fillRowTileSelect.innerHTML = "";
|
||||||
|
brickTileSelect.innerHTML = "";
|
||||||
const placeholder = document.createElement("option");
|
const placeholder = document.createElement("option");
|
||||||
placeholder.value = "";
|
placeholder.value = "";
|
||||||
placeholder.textContent = "Select tile";
|
placeholder.textContent = "Select tile";
|
||||||
floorTileSelect.append(placeholder);
|
floorTileSelect.append(placeholder);
|
||||||
fillRowTileSelect.append(placeholder.cloneNode(true));
|
fillRowTileSelect.append(placeholder.cloneNode(true));
|
||||||
|
brickTileSelect.append(placeholder.cloneNode(true));
|
||||||
|
|
||||||
tiles.forEach((tile) => {
|
tiles.forEach((tile) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
@@ -160,6 +167,7 @@ const renderFloorOptions = () => {
|
|||||||
option.textContent = tile.name;
|
option.textContent = tile.name;
|
||||||
floorTileSelect.append(option);
|
floorTileSelect.append(option);
|
||||||
fillRowTileSelect.append(option.cloneNode(true));
|
fillRowTileSelect.append(option.cloneNode(true));
|
||||||
|
brickTileSelect.append(option.cloneNode(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (floorTileId && !tiles.some((tile) => tile.id === floorTileId)) {
|
if (floorTileId && !tiles.some((tile) => tile.id === floorTileId)) {
|
||||||
@@ -167,6 +175,7 @@ const renderFloorOptions = () => {
|
|||||||
}
|
}
|
||||||
floorTileSelect.value = floorTileId ?? "";
|
floorTileSelect.value = floorTileId ?? "";
|
||||||
fillRowTileSelect.value = activeTileId ?? "";
|
fillRowTileSelect.value = activeTileId ?? "";
|
||||||
|
brickTileSelect.value = activeTileId ?? "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTiles = () => {
|
const renderTiles = () => {
|
||||||
@@ -274,18 +283,75 @@ const drawGridLines = () => {
|
|||||||
patternContext.strokeStyle = gridLineColor;
|
patternContext.strokeStyle = gridLineColor;
|
||||||
patternContext.lineWidth = 1;
|
patternContext.lineWidth = 1;
|
||||||
|
|
||||||
|
const lastRowIndex = config.rows - 1;
|
||||||
|
const floorTile = floorTileId ? getTileById(floorTileId) : null;
|
||||||
|
|
||||||
for (let row = 0; row <= config.rows; row += 1) {
|
for (let row = 0; row <= config.rows; row += 1) {
|
||||||
|
if (row === 0 || row === config.rows) {
|
||||||
patternContext.beginPath();
|
patternContext.beginPath();
|
||||||
patternContext.moveTo(0, row * config.tileSize + 0.5);
|
patternContext.moveTo(0, row * config.tileSize + 0.5);
|
||||||
patternContext.lineTo(patternCanvas.width, row * config.tileSize + 0.5);
|
patternContext.lineTo(patternCanvas.width, row * config.tileSize + 0.5);
|
||||||
patternContext.stroke();
|
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) {
|
for (let col = 0; col <= config.cols; col += 1) {
|
||||||
|
if (col === 0 || col === config.cols) {
|
||||||
patternContext.beginPath();
|
patternContext.beginPath();
|
||||||
patternContext.moveTo(col * config.tileSize + 0.5, 0);
|
patternContext.moveTo(col * config.tileSize + 0.5, 0);
|
||||||
patternContext.lineTo(col * config.tileSize + 0.5, patternCanvas.height);
|
patternContext.lineTo(col * config.tileSize + 0.5, patternCanvas.height);
|
||||||
patternContext.stroke();
|
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();
|
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 paintArea = (row, col, tileId, width = 1, height = 1) => {
|
||||||
const maxRow = floorEnabled ? config.rows - 2 : config.rows - 1;
|
const maxRow = floorEnabled ? config.rows - 2 : config.rows - 1;
|
||||||
if (row > maxRow || col >= config.cols) {
|
if (row > maxRow || col >= config.cols) {
|
||||||
@@ -516,6 +617,9 @@ applyGridButton.addEventListener("click", () => {
|
|||||||
gridRowsInput.value = config.rows;
|
gridRowsInput.value = config.rows;
|
||||||
gridColsInput.value = config.cols;
|
gridColsInput.value = config.cols;
|
||||||
gridSizeInput.value = config.tileSize;
|
gridSizeInput.value = config.tileSize;
|
||||||
|
fillRowInput.max = String(config.rows);
|
||||||
|
brickStartInput.max = String(config.rows);
|
||||||
|
brickEndInput.max = String(config.rows);
|
||||||
initGrid();
|
initGrid();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
@@ -550,6 +654,16 @@ fillRowButton.addEventListener("click", () => {
|
|||||||
fillRowWithTile(rowIndex, tileId);
|
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) =>
|
const readFileAsDataUrl = (file) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -744,6 +858,9 @@ importJsonButton.addEventListener("click", () => {
|
|||||||
gridSizeInput.value = config.tileSize;
|
gridSizeInput.value = config.tileSize;
|
||||||
floorEnabledInput.checked = floorEnabled;
|
floorEnabledInput.checked = floorEnabled;
|
||||||
fillRowInput.value = "1";
|
fillRowInput.value = "1";
|
||||||
|
fillRowInput.max = String(config.rows);
|
||||||
|
brickStartInput.max = String(config.rows);
|
||||||
|
brickEndInput.max = String(config.rows);
|
||||||
|
|
||||||
tiles = normalizeTiles(payload.tiles);
|
tiles = normalizeTiles(payload.tiles);
|
||||||
|
|
||||||
@@ -824,3 +941,6 @@ loadLibraryFromStorage();
|
|||||||
renderTiles();
|
renderTiles();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
|
fillRowInput.max = String(config.rows);
|
||||||
|
brickStartInput.max = String(config.rows);
|
||||||
|
brickEndInput.max = String(config.rows);
|
||||||
|
|||||||
19
index.html
19
index.html
@@ -97,6 +97,25 @@
|
|||||||
</label>
|
</label>
|
||||||
<button id="fill-row-button">Fill row</button>
|
<button id="fill-row-button">Fill row</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>
|
||||||
|
Brick start row
|
||||||
|
<input id="brick-start" type="number" min="1" max="100" value="1" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Brick end row
|
||||||
|
<input id="brick-end" type="number" min="1" max="100" value="6" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Brick tile
|
||||||
|
<select id="brick-tile"></select>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="brick-stagger" type="checkbox" checked />
|
||||||
|
Stagger rows
|
||||||
|
</label>
|
||||||
|
<button id="brick-apply">Apply brick</button>
|
||||||
|
</div>
|
||||||
<div class="control-actions">
|
<div class="control-actions">
|
||||||
<button id="apply-grid">Apply grid</button>
|
<button id="apply-grid">Apply grid</button>
|
||||||
<button id="clear-grid">Clear grid</button>
|
<button id="clear-grid">Clear grid</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user