storage
This commit is contained in:
@@ -8,6 +8,8 @@ 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
|
||||
- Auto-save tile library in the browser
|
||||
- Import/export the tile library as JSON
|
||||
- Export JSON for handing off to a tiler
|
||||
- Export PNG snapshots
|
||||
- Import JSON to continue a saved pattern
|
||||
@@ -57,4 +59,5 @@ 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.
|
||||
- Tile library changes auto-save to your browser storage.
|
||||
- If you use image tiles, keep images square for best results.
|
||||
|
||||
117
app.js
117
app.js
@@ -7,6 +7,9 @@ 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 exportLibraryButton = document.getElementById("export-library");
|
||||
const importLibraryButton = document.getElementById("import-library");
|
||||
const importLibraryFile = document.getElementById("import-library-file");
|
||||
|
||||
const gridRowsInput = document.getElementById("grid-rows");
|
||||
const gridColsInput = document.getElementById("grid-cols");
|
||||
@@ -36,6 +39,7 @@ let floorEnabled = false;
|
||||
let floorTileId = null;
|
||||
const placements = new Map();
|
||||
const imageCache = new Map();
|
||||
const STORAGE_KEY = "tile-library-v1";
|
||||
|
||||
const config = {
|
||||
rows: Number(gridRowsInput.value),
|
||||
@@ -59,6 +63,16 @@ const createPlacementId = () =>
|
||||
? crypto.randomUUID()
|
||||
: `placement-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const normalizeTiles = (items) =>
|
||||
items.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,
|
||||
}));
|
||||
|
||||
const applyFloorRow = () => {
|
||||
if (!floorEnabled || !floorTileId) {
|
||||
return;
|
||||
@@ -194,7 +208,13 @@ const renderTiles = () => {
|
||||
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)));
|
||||
const placementsToRemove = [];
|
||||
placements.forEach((placement, placementId) => {
|
||||
if (placement.tileId === tile.id) {
|
||||
placementsToRemove.push(placementId);
|
||||
}
|
||||
});
|
||||
placementsToRemove.forEach((placementId) => clearPlacement(placementId));
|
||||
if (floorTileId === tile.id) {
|
||||
floorTileId = null;
|
||||
}
|
||||
@@ -204,6 +224,7 @@ const renderTiles = () => {
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
saveLibraryToStorage();
|
||||
});
|
||||
|
||||
actions.append(selectButton, deleteButton);
|
||||
@@ -268,6 +289,55 @@ const drawGridLines = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveLibraryToStorage = () => {
|
||||
const payload = {
|
||||
version: 1,
|
||||
tiles,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
|
||||
const loadLibraryFromPayload = (payload) => {
|
||||
if (!payload || !Array.isArray(payload.tiles)) {
|
||||
return false;
|
||||
}
|
||||
tiles = normalizeTiles(payload.tiles);
|
||||
imageCache.clear();
|
||||
tiles.forEach((tile) => buildTileImage(tile));
|
||||
const validIds = new Set(tiles.map((tile) => tile.id));
|
||||
const placementsToRemove = [];
|
||||
placements.forEach((placement, placementId) => {
|
||||
if (!validIds.has(placement.tileId)) {
|
||||
placementsToRemove.push(placementId);
|
||||
}
|
||||
});
|
||||
placementsToRemove.forEach((placementId) => clearPlacement(placementId));
|
||||
if (floorTileId && !validIds.has(floorTileId)) {
|
||||
floorTileId = null;
|
||||
}
|
||||
setActiveTile(tiles[0]?.id ?? null);
|
||||
renderTiles();
|
||||
renderFloorOptions();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
return true;
|
||||
};
|
||||
|
||||
const loadLibraryFromStorage = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(raw);
|
||||
loadLibraryFromPayload(payload);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
|
||||
const renderCanvas = () => {
|
||||
resizeCanvas();
|
||||
drawGridBackground();
|
||||
@@ -522,6 +592,7 @@ tileForm.addEventListener("submit", async (event) => {
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
saveLibraryToStorage();
|
||||
});
|
||||
|
||||
const buildPattern = () => ({
|
||||
@@ -674,14 +745,7 @@ importJsonButton.addEventListener("click", () => {
|
||||
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,
|
||||
}));
|
||||
tiles = normalizeTiles(payload.tiles);
|
||||
|
||||
imageCache.clear();
|
||||
tiles.forEach((tile) => buildTileImage(tile));
|
||||
@@ -716,12 +780,47 @@ importJsonButton.addEventListener("click", () => {
|
||||
renderFloorOptions();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
saveLibraryToStorage();
|
||||
} catch (error) {
|
||||
alert("Could not import JSON. Please check the format.");
|
||||
}
|
||||
});
|
||||
|
||||
exportLibraryButton.addEventListener("click", () => {
|
||||
const payload = {
|
||||
version: 1,
|
||||
tiles,
|
||||
};
|
||||
downloadFile(JSON.stringify(payload, null, 2), "tile-library.json", "application/json");
|
||||
});
|
||||
|
||||
importLibraryButton.addEventListener("click", () => {
|
||||
importLibraryFile.click();
|
||||
});
|
||||
|
||||
importLibraryFile.addEventListener("change", async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await file.text();
|
||||
const payload = JSON.parse(text);
|
||||
const loaded = loadLibraryFromPayload(payload);
|
||||
if (loaded) {
|
||||
saveLibraryToStorage();
|
||||
} else {
|
||||
alert("Invalid library JSON.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Could not import library JSON.");
|
||||
} finally {
|
||||
importLibraryFile.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
initGrid();
|
||||
loadLibraryFromStorage();
|
||||
renderTiles();
|
||||
renderCanvas();
|
||||
syncPatternPreview();
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
<main class="app-layout">
|
||||
<section class="panel">
|
||||
<h2>Tile Library</h2>
|
||||
<div class="library-actions">
|
||||
<button id="export-library" class="primary">Export library</button>
|
||||
<button id="import-library">Import library</button>
|
||||
<input id="import-library-file" type="file" accept="application/json" hidden />
|
||||
</div>
|
||||
<form id="tile-form" class="tile-form">
|
||||
<label>
|
||||
Tile name
|
||||
|
||||
@@ -86,6 +86,13 @@ button:hover {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.library-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tile-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
Reference in New Issue
Block a user