init
This commit is contained in:
727
app.js
Normal file
727
app.js
Normal file
@@ -0,0 +1,727 @@
|
||||
const tileForm = document.getElementById("tile-form");
|
||||
const tileNameInput = document.getElementById("tile-name");
|
||||
const tileColorInput = document.getElementById("tile-color");
|
||||
const tileWidthInput = document.getElementById("tile-width");
|
||||
const tileHeightInput = document.getElementById("tile-height");
|
||||
const tileImageInput = document.getElementById("tile-image");
|
||||
const tileList = document.getElementById("tile-list");
|
||||
const activeTileName = document.getElementById("active-tile-name");
|
||||
const activeTileSwatch = document.getElementById("active-tile-swatch");
|
||||
|
||||
const gridRowsInput = document.getElementById("grid-rows");
|
||||
const gridColsInput = document.getElementById("grid-cols");
|
||||
const gridSizeInput = document.getElementById("grid-size");
|
||||
const floorEnabledInput = document.getElementById("floor-enabled");
|
||||
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 applyGridButton = document.getElementById("apply-grid");
|
||||
const clearGridButton = document.getElementById("clear-grid");
|
||||
|
||||
const patternCanvas = document.getElementById("pattern-canvas");
|
||||
const patternContext = patternCanvas.getContext("2d");
|
||||
|
||||
const exportJsonButton = document.getElementById("export-json");
|
||||
const copyJsonButton = document.getElementById("copy-json");
|
||||
const exportPngButton = document.getElementById("export-png");
|
||||
const importJsonButton = document.getElementById("import-json");
|
||||
const refreshJsonButton = document.getElementById("refresh-json");
|
||||
const patternJson = document.getElementById("pattern-json");
|
||||
|
||||
let tiles = [];
|
||||
let grid = [];
|
||||
let activeTileId = null;
|
||||
let floorEnabled = false;
|
||||
let floorTileId = null;
|
||||
const placements = new Map();
|
||||
const imageCache = new Map();
|
||||
|
||||
const config = {
|
||||
rows: Number(gridRowsInput.value),
|
||||
cols: Number(gridColsInput.value),
|
||||
tileSize: Number(gridSizeInput.value),
|
||||
};
|
||||
|
||||
const gridLineColor = "#e4e8f2";
|
||||
const gridBackground = "#ffffff";
|
||||
|
||||
const createId = () =>
|
||||
(crypto && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: `tile-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
|
||||
|
||||
const getTileById = (tileId) => tiles.find((item) => item.id === tileId);
|
||||
const createPlacementId = () =>
|
||||
(crypto && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: `placement-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const applyFloorRow = () => {
|
||||
if (!floorEnabled || !floorTileId) {
|
||||
return;
|
||||
}
|
||||
const lastRow = config.rows - 1;
|
||||
const tile = getTileById(floorTileId);
|
||||
if (!tile) {
|
||||
return;
|
||||
}
|
||||
for (let col = 0; col < config.cols; col += tile.width) {
|
||||
const width = clampNumber(tile.width, 1, config.cols - col);
|
||||
const x = col * config.tileSize;
|
||||
const y = lastRow * config.tileSize;
|
||||
const tileWidth = width * config.tileSize;
|
||||
if (tile.imageData) {
|
||||
const image = buildTileImage(tile);
|
||||
if (image.complete) {
|
||||
patternContext.drawImage(image, x, y, tileWidth, config.tileSize);
|
||||
} else {
|
||||
image.onload = () => {
|
||||
renderCanvas();
|
||||
};
|
||||
}
|
||||
} else {
|
||||
patternContext.fillStyle = tile.color;
|
||||
patternContext.fillRect(x, y, tileWidth, config.tileSize);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initGrid = () => {
|
||||
grid = Array.from({ length: config.rows }, () =>
|
||||
Array.from({ length: config.cols }, () => null)
|
||||
);
|
||||
placements.clear();
|
||||
};
|
||||
|
||||
const buildTileImage = (tile) => {
|
||||
if (!tile.imageData) {
|
||||
return null;
|
||||
}
|
||||
if (imageCache.has(tile.id)) {
|
||||
return imageCache.get(tile.id);
|
||||
}
|
||||
const image = new Image();
|
||||
image.src = tile.imageData;
|
||||
imageCache.set(tile.id, image);
|
||||
return image;
|
||||
};
|
||||
|
||||
const setActiveTile = (tileId) => {
|
||||
activeTileId = tileId;
|
||||
const tile = getTileById(tileId);
|
||||
if (!tile) {
|
||||
activeTileName.textContent = "None selected";
|
||||
activeTileSwatch.style.background = "repeating-linear-gradient(45deg, #f1f2f7, #f1f2f7 10px, #e8eaf2 10px, #e8eaf2 20px)";
|
||||
activeTileSwatch.style.backgroundImage = "";
|
||||
fillRowTileSelect.value = "";
|
||||
return;
|
||||
}
|
||||
activeTileName.textContent = tile.name;
|
||||
fillRowTileSelect.value = tile.id;
|
||||
if (tile.imageData) {
|
||||
activeTileSwatch.style.backgroundImage = `url(${tile.imageData})`;
|
||||
activeTileSwatch.style.backgroundSize = "cover";
|
||||
activeTileSwatch.style.backgroundColor = "transparent";
|
||||
} else {
|
||||
activeTileSwatch.style.backgroundImage = "";
|
||||
activeTileSwatch.style.backgroundColor = tile.color;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFloorOptions = () => {
|
||||
floorTileSelect.innerHTML = "";
|
||||
fillRowTileSelect.innerHTML = "";
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select tile";
|
||||
floorTileSelect.append(placeholder);
|
||||
fillRowTileSelect.append(placeholder.cloneNode(true));
|
||||
|
||||
tiles.forEach((tile) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = tile.id;
|
||||
option.textContent = tile.name;
|
||||
floorTileSelect.append(option);
|
||||
fillRowTileSelect.append(option.cloneNode(true));
|
||||
});
|
||||
|
||||
if (floorTileId && !tiles.some((tile) => tile.id === floorTileId)) {
|
||||
floorTileId = null;
|
||||
}
|
||||
floorTileSelect.value = floorTileId ?? "";
|
||||
fillRowTileSelect.value = activeTileId ?? "";
|
||||
};
|
||||
|
||||
const renderTiles = () => {
|
||||
tileList.innerHTML = "";
|
||||
tiles.forEach((tile) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "tile-card";
|
||||
|
||||
const swatch = document.createElement("div");
|
||||
swatch.className = "tile-swatch";
|
||||
if (tile.imageData) {
|
||||
swatch.style.backgroundImage = `url(${tile.imageData})`;
|
||||
} else {
|
||||
swatch.style.backgroundColor = tile.color;
|
||||
}
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "tile-meta";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = tile.name;
|
||||
const subtitle = document.createElement("span");
|
||||
const sizeLabel = `${tile.width}x${tile.height}`;
|
||||
subtitle.textContent = tile.imageData
|
||||
? `Image tile · ${sizeLabel}`
|
||||
: `${tile.color.toUpperCase()} · ${sizeLabel}`;
|
||||
meta.append(title, subtitle);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "tile-actions";
|
||||
|
||||
const selectButton = document.createElement("button");
|
||||
selectButton.textContent = "Use";
|
||||
selectButton.addEventListener("click", () => {
|
||||
setActiveTile(tile.id);
|
||||
});
|
||||
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.textContent = "Delete";
|
||||
deleteButton.addEventListener("click", () => {
|
||||
tiles = tiles.filter((item) => item.id !== tile.id);
|
||||
imageCache.delete(tile.id);
|
||||
grid = grid.map((row) => row.map((cell) => (cell === tile.id ? null : cell)));
|
||||
if (floorTileId === tile.id) {
|
||||
floorTileId = null;
|
||||
}
|
||||
if (activeTileId === tile.id) {
|
||||
setActiveTile(tiles[0]?.id ?? null);
|
||||
}
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
actions.append(selectButton, deleteButton);
|
||||
card.append(swatch, meta, actions);
|
||||
tileList.append(card);
|
||||
});
|
||||
renderFloorOptions();
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
patternCanvas.width = config.cols * config.tileSize;
|
||||
patternCanvas.height = config.rows * config.tileSize;
|
||||
};
|
||||
|
||||
const drawGridBackground = () => {
|
||||
patternContext.fillStyle = gridBackground;
|
||||
patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height);
|
||||
};
|
||||
|
||||
const drawTiles = () => {
|
||||
placements.forEach((placement) => {
|
||||
const tile = getTileById(placement.tileId);
|
||||
if (!tile) {
|
||||
return;
|
||||
}
|
||||
const x = placement.col * config.tileSize;
|
||||
const y = placement.row * config.tileSize;
|
||||
const width = placement.width * config.tileSize;
|
||||
const height = placement.height * config.tileSize;
|
||||
if (tile.imageData) {
|
||||
const image = buildTileImage(tile);
|
||||
if (image.complete) {
|
||||
patternContext.drawImage(image, x, y, width, height);
|
||||
} else {
|
||||
image.onload = () => {
|
||||
renderCanvas();
|
||||
};
|
||||
}
|
||||
} else {
|
||||
patternContext.fillStyle = tile.color;
|
||||
patternContext.fillRect(x, y, width, height);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const drawGridLines = () => {
|
||||
patternContext.strokeStyle = gridLineColor;
|
||||
patternContext.lineWidth = 1;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
const renderCanvas = () => {
|
||||
resizeCanvas();
|
||||
drawGridBackground();
|
||||
drawTiles();
|
||||
applyFloorRow();
|
||||
drawGridLines();
|
||||
};
|
||||
|
||||
const getCellFromPointer = (event) => {
|
||||
const rect = patternCanvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const col = Math.floor(x / config.tileSize);
|
||||
const row = Math.floor(y / config.tileSize);
|
||||
if (row < 0 || row >= config.rows || col < 0 || col >= config.cols) {
|
||||
return null;
|
||||
}
|
||||
return { row, col };
|
||||
};
|
||||
|
||||
const clearPlacement = (placementId) => {
|
||||
const placement = placements.get(placementId);
|
||||
if (!placement) {
|
||||
return;
|
||||
}
|
||||
for (let r = placement.row; r < placement.row + placement.height; r += 1) {
|
||||
for (let c = placement.col; c < placement.col + placement.width; c += 1) {
|
||||
if (grid[r]?.[c] === placementId) {
|
||||
grid[r][c] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
placements.delete(placementId);
|
||||
};
|
||||
|
||||
const removePlacementsInArea = (row, col, width, height) => {
|
||||
const affected = new Set();
|
||||
for (let r = row; r < row + height; r += 1) {
|
||||
for (let c = col; c < col + width; c += 1) {
|
||||
const placementId = grid[r]?.[c];
|
||||
if (placementId) {
|
||||
affected.add(placementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
affected.forEach((placementId) => clearPlacement(placementId));
|
||||
};
|
||||
|
||||
const addPlacement = (row, col, tileId, width, height) => {
|
||||
const placementId = createPlacementId();
|
||||
const placement = {
|
||||
id: placementId,
|
||||
tileId,
|
||||
row,
|
||||
col,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
placements.set(placementId, placement);
|
||||
for (let r = row; r < row + height; r += 1) {
|
||||
for (let c = col; c < col + width; c += 1) {
|
||||
grid[r][c] = placementId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fillRowWithTile = (rowIndex, tileId) => {
|
||||
const tile = getTileById(tileId);
|
||||
if (!tile) {
|
||||
return;
|
||||
}
|
||||
if (floorEnabled && rowIndex === config.rows - 1) {
|
||||
return;
|
||||
}
|
||||
const toRemove = new Set();
|
||||
for (let col = 0; col < config.cols; col += 1) {
|
||||
const placementId = grid[rowIndex][col];
|
||||
if (placementId) {
|
||||
toRemove.add(placementId);
|
||||
}
|
||||
}
|
||||
toRemove.forEach((placementId) => clearPlacement(placementId));
|
||||
|
||||
const width = clampNumber(tile.width, 1, config.cols);
|
||||
for (let col = 0; col < config.cols; col += width) {
|
||||
const remaining = config.cols - col;
|
||||
addPlacement(rowIndex, col, tile.id, clampNumber(width, 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) {
|
||||
return;
|
||||
}
|
||||
const clampedWidth = clampNumber(width, 1, config.cols - col);
|
||||
const clampedHeight = clampNumber(height, 1, maxRow - row + 1);
|
||||
removePlacementsInArea(row, col, clampedWidth, clampedHeight);
|
||||
if (tileId) {
|
||||
addPlacement(row, col, tileId, clampedWidth, clampedHeight);
|
||||
}
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
};
|
||||
|
||||
let isPainting = false;
|
||||
let paintMode = "paint";
|
||||
|
||||
patternCanvas.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
patternCanvas.addEventListener("pointerdown", (event) => {
|
||||
isPainting = true;
|
||||
paintMode = event.shiftKey || event.button === 2 ? "erase" : "paint";
|
||||
const cell = getCellFromPointer(event);
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
if (paintMode === "erase") {
|
||||
if (!(floorEnabled && cell.row === config.rows - 1)) {
|
||||
const placementId = grid[cell.row][cell.col];
|
||||
if (placementId) {
|
||||
clearPlacement(placementId);
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
}
|
||||
}
|
||||
} else if (activeTileId) {
|
||||
const tile = getTileById(activeTileId);
|
||||
if (tile) {
|
||||
paintArea(cell.row, cell.col, activeTileId, tile.width, tile.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
patternCanvas.addEventListener("pointermove", (event) => {
|
||||
if (!isPainting) {
|
||||
return;
|
||||
}
|
||||
const cell = getCellFromPointer(event);
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
if (paintMode === "erase") {
|
||||
if (!(floorEnabled && cell.row === config.rows - 1)) {
|
||||
const placementId = grid[cell.row][cell.col];
|
||||
if (placementId) {
|
||||
clearPlacement(placementId);
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
}
|
||||
}
|
||||
} else if (activeTileId) {
|
||||
const tile = getTileById(activeTileId);
|
||||
if (tile) {
|
||||
paintArea(cell.row, cell.col, activeTileId, tile.width, tile.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
patternCanvas.addEventListener("pointerup", () => {
|
||||
isPainting = false;
|
||||
});
|
||||
|
||||
patternCanvas.addEventListener("pointerleave", () => {
|
||||
isPainting = false;
|
||||
});
|
||||
|
||||
applyGridButton.addEventListener("click", () => {
|
||||
config.rows = clampNumber(Number(gridRowsInput.value), 1, 100);
|
||||
config.cols = clampNumber(Number(gridColsInput.value), 1, 100);
|
||||
config.tileSize = clampNumber(Number(gridSizeInput.value), 12, 120);
|
||||
gridRowsInput.value = config.rows;
|
||||
gridColsInput.value = config.cols;
|
||||
gridSizeInput.value = config.tileSize;
|
||||
initGrid();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
clearGridButton.addEventListener("click", () => {
|
||||
initGrid();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
floorEnabledInput.addEventListener("change", () => {
|
||||
floorEnabled = floorEnabledInput.checked;
|
||||
applyFloorRow();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
floorTileSelect.addEventListener("change", () => {
|
||||
floorTileId = floorTileSelect.value || null;
|
||||
applyFloorRow();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
fillRowButton.addEventListener("click", () => {
|
||||
const rowIndex = clampNumber(Number(fillRowInput.value) - 1, 0, config.rows - 1);
|
||||
const tileId = fillRowTileSelect.value || activeTileId;
|
||||
if (!tileId) {
|
||||
return;
|
||||
}
|
||||
fillRowWithTile(rowIndex, tileId);
|
||||
});
|
||||
|
||||
const readFileAsDataUrl = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(new Error("Could not read file"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
tileForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const name = tileNameInput.value.trim();
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const color = tileColorInput.value;
|
||||
const width = clampNumber(Number(tileWidthInput.value), 1, 12);
|
||||
const height = clampNumber(Number(tileHeightInput.value), 1, 12);
|
||||
const file = tileImageInput.files?.[0] ?? null;
|
||||
const tile = {
|
||||
id: createId(),
|
||||
name,
|
||||
color,
|
||||
width,
|
||||
height,
|
||||
imageData: null,
|
||||
};
|
||||
|
||||
if (file) {
|
||||
tile.imageData = await readFileAsDataUrl(file);
|
||||
}
|
||||
|
||||
tiles.push(tile);
|
||||
tileNameInput.value = "";
|
||||
tileImageInput.value = "";
|
||||
tileWidthInput.value = "1";
|
||||
tileHeightInput.value = "1";
|
||||
if (!activeTileId) {
|
||||
setActiveTile(tile.id);
|
||||
}
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
const buildPattern = () => ({
|
||||
version: 1,
|
||||
config: {
|
||||
rows: config.rows,
|
||||
cols: config.cols,
|
||||
tileSize: config.tileSize,
|
||||
floorEnabled,
|
||||
floorTileId,
|
||||
},
|
||||
tiles: tiles.map((tile) => ({
|
||||
id: tile.id,
|
||||
name: tile.name,
|
||||
color: tile.color,
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
imageData: tile.imageData,
|
||||
})),
|
||||
placements: Array.from(placements.values()),
|
||||
grid: grid.map((row) => row.map((cell) => {
|
||||
if (!cell) {
|
||||
return null;
|
||||
}
|
||||
const placement = placements.get(cell);
|
||||
return placement ? placement.tileId : null;
|
||||
})),
|
||||
});
|
||||
|
||||
const syncPatternPreview = () => {
|
||||
const payload = buildPattern();
|
||||
patternJson.value = JSON.stringify(payload, null, 2);
|
||||
};
|
||||
|
||||
const downloadFile = (content, filename, type) => {
|
||||
const blob = new Blob([content], { type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
exportJsonButton.addEventListener("click", () => {
|
||||
applyFloorRow();
|
||||
const payload = buildPattern();
|
||||
downloadFile(JSON.stringify(payload, null, 2), "tile-pattern.json", "application/json");
|
||||
});
|
||||
|
||||
copyJsonButton.addEventListener("click", async () => {
|
||||
const payload = buildPattern();
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
|
||||
} catch (error) {
|
||||
patternJson.focus();
|
||||
patternJson.select();
|
||||
}
|
||||
});
|
||||
|
||||
exportPngButton.addEventListener("click", () => {
|
||||
const exportCanvas = document.createElement("canvas");
|
||||
exportCanvas.width = config.cols * config.tileSize;
|
||||
exportCanvas.height = config.rows * config.tileSize;
|
||||
const exportContext = exportCanvas.getContext("2d");
|
||||
exportContext.fillStyle = gridBackground;
|
||||
exportContext.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
||||
|
||||
placements.forEach((placement) => {
|
||||
const tile = getTileById(placement.tileId);
|
||||
if (!tile) {
|
||||
return;
|
||||
}
|
||||
const x = placement.col * config.tileSize;
|
||||
const y = placement.row * config.tileSize;
|
||||
const width = placement.width * config.tileSize;
|
||||
const height = placement.height * config.tileSize;
|
||||
if (tile.imageData) {
|
||||
const image = buildTileImage(tile);
|
||||
if (image.complete) {
|
||||
exportContext.drawImage(image, x, y, width, height);
|
||||
} else {
|
||||
image.onload = () => {
|
||||
exportPngButton.click();
|
||||
};
|
||||
}
|
||||
} else {
|
||||
exportContext.fillStyle = tile.color;
|
||||
exportContext.fillRect(x, y, width, height);
|
||||
}
|
||||
});
|
||||
|
||||
if (floorEnabled && floorTileId) {
|
||||
const tile = getTileById(floorTileId);
|
||||
if (tile) {
|
||||
const lastRow = config.rows - 1;
|
||||
for (let col = 0; col < config.cols; col += tile.width) {
|
||||
const width = clampNumber(tile.width, 1, config.cols - col);
|
||||
const x = col * config.tileSize;
|
||||
const y = lastRow * config.tileSize;
|
||||
const tileWidth = width * config.tileSize;
|
||||
if (tile.imageData) {
|
||||
const image = buildTileImage(tile);
|
||||
if (image.complete) {
|
||||
exportContext.drawImage(image, x, y, tileWidth, config.tileSize);
|
||||
} else {
|
||||
image.onload = () => {
|
||||
exportPngButton.click();
|
||||
};
|
||||
}
|
||||
} else {
|
||||
exportContext.fillStyle = tile.color;
|
||||
exportContext.fillRect(x, y, tileWidth, config.tileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exportCanvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "tile-pattern.png";
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
});
|
||||
|
||||
refreshJsonButton.addEventListener("click", () => {
|
||||
syncPatternPreview();
|
||||
});
|
||||
|
||||
importJsonButton.addEventListener("click", () => {
|
||||
try {
|
||||
const payload = JSON.parse(patternJson.value);
|
||||
if (!payload.config || !payload.grid || !payload.tiles) {
|
||||
throw new Error("Invalid pattern data");
|
||||
}
|
||||
config.rows = clampNumber(Number(payload.config.rows), 1, 100);
|
||||
config.cols = clampNumber(Number(payload.config.cols), 1, 100);
|
||||
config.tileSize = clampNumber(Number(payload.config.tileSize), 12, 120);
|
||||
floorEnabled = Boolean(payload.config.floorEnabled);
|
||||
floorTileId = payload.config.floorTileId ?? null;
|
||||
gridRowsInput.value = config.rows;
|
||||
gridColsInput.value = config.cols;
|
||||
gridSizeInput.value = config.tileSize;
|
||||
floorEnabledInput.checked = floorEnabled;
|
||||
fillRowInput.value = "1";
|
||||
|
||||
tiles = payload.tiles.map((tile) => ({
|
||||
id: tile.id || createId(),
|
||||
name: tile.name || "Untitled",
|
||||
color: tile.color || "#999999",
|
||||
width: clampNumber(Number(tile.width ?? 1), 1, 12),
|
||||
height: clampNumber(Number(tile.height ?? 1), 1, 12),
|
||||
imageData: tile.imageData || null,
|
||||
}));
|
||||
|
||||
imageCache.clear();
|
||||
tiles.forEach((tile) => buildTileImage(tile));
|
||||
initGrid();
|
||||
if (Array.isArray(payload.placements)) {
|
||||
payload.placements.forEach((placement) => {
|
||||
const tile = getTileById(placement.tileId);
|
||||
if (!tile) {
|
||||
return;
|
||||
}
|
||||
const row = clampNumber(Number(placement.row), 0, config.rows - 1);
|
||||
const col = clampNumber(Number(placement.col), 0, config.cols - 1);
|
||||
const width = clampNumber(Number(placement.width ?? tile.width), 1, config.cols - col);
|
||||
const height = clampNumber(Number(placement.height ?? tile.height), 1, config.rows - row);
|
||||
addPlacement(row, col, tile.id, width, height);
|
||||
});
|
||||
} else if (Array.isArray(payload.grid)) {
|
||||
payload.grid.forEach((row, rowIndex) => {
|
||||
row.forEach((cell, colIndex) => {
|
||||
if (cell) {
|
||||
const tile = getTileById(cell);
|
||||
if (tile) {
|
||||
addPlacement(rowIndex, colIndex, tile.id, 1, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setActiveTile(tiles[0]?.id ?? null);
|
||||
renderTiles();
|
||||
renderFloorOptions();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
} catch (error) {
|
||||
alert("Could not import JSON. Please check the format.");
|
||||
}
|
||||
});
|
||||
|
||||
initGrid();
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
Reference in New Issue
Block a user