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)
|
- 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
|
||||||
|
- Auto-save tile library in the browser
|
||||||
|
- Import/export the tile library as JSON
|
||||||
- Export JSON for handing off to a tiler
|
- Export JSON for handing off to a tiler
|
||||||
- Export PNG snapshots
|
- Export PNG snapshots
|
||||||
- Import JSON to continue a saved pattern
|
- 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.
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|||||||
117
app.js
117
app.js
@@ -7,6 +7,9 @@ const tileImageInput = document.getElementById("tile-image");
|
|||||||
const tileList = document.getElementById("tile-list");
|
const tileList = document.getElementById("tile-list");
|
||||||
const activeTileName = document.getElementById("active-tile-name");
|
const activeTileName = document.getElementById("active-tile-name");
|
||||||
const activeTileSwatch = document.getElementById("active-tile-swatch");
|
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 gridRowsInput = document.getElementById("grid-rows");
|
||||||
const gridColsInput = document.getElementById("grid-cols");
|
const gridColsInput = document.getElementById("grid-cols");
|
||||||
@@ -36,6 +39,7 @@ let floorEnabled = false;
|
|||||||
let floorTileId = null;
|
let floorTileId = null;
|
||||||
const placements = new Map();
|
const placements = new Map();
|
||||||
const imageCache = new Map();
|
const imageCache = new Map();
|
||||||
|
const STORAGE_KEY = "tile-library-v1";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
rows: Number(gridRowsInput.value),
|
rows: Number(gridRowsInput.value),
|
||||||
@@ -59,6 +63,16 @@ const createPlacementId = () =>
|
|||||||
? crypto.randomUUID()
|
? crypto.randomUUID()
|
||||||
: `placement-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
: `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 = () => {
|
const applyFloorRow = () => {
|
||||||
if (!floorEnabled || !floorTileId) {
|
if (!floorEnabled || !floorTileId) {
|
||||||
return;
|
return;
|
||||||
@@ -194,7 +208,13 @@ const renderTiles = () => {
|
|||||||
deleteButton.addEventListener("click", () => {
|
deleteButton.addEventListener("click", () => {
|
||||||
tiles = tiles.filter((item) => item.id !== tile.id);
|
tiles = tiles.filter((item) => item.id !== tile.id);
|
||||||
imageCache.delete(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) {
|
if (floorTileId === tile.id) {
|
||||||
floorTileId = null;
|
floorTileId = null;
|
||||||
}
|
}
|
||||||
@@ -204,6 +224,7 @@ const renderTiles = () => {
|
|||||||
renderTiles();
|
renderTiles();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
|
saveLibraryToStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.append(selectButton, deleteButton);
|
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 = () => {
|
const renderCanvas = () => {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
drawGridBackground();
|
drawGridBackground();
|
||||||
@@ -522,6 +592,7 @@ tileForm.addEventListener("submit", async (event) => {
|
|||||||
renderTiles();
|
renderTiles();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
|
saveLibraryToStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildPattern = () => ({
|
const buildPattern = () => ({
|
||||||
@@ -674,14 +745,7 @@ importJsonButton.addEventListener("click", () => {
|
|||||||
floorEnabledInput.checked = floorEnabled;
|
floorEnabledInput.checked = floorEnabled;
|
||||||
fillRowInput.value = "1";
|
fillRowInput.value = "1";
|
||||||
|
|
||||||
tiles = payload.tiles.map((tile) => ({
|
tiles = normalizeTiles(payload.tiles);
|
||||||
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();
|
imageCache.clear();
|
||||||
tiles.forEach((tile) => buildTileImage(tile));
|
tiles.forEach((tile) => buildTileImage(tile));
|
||||||
@@ -716,12 +780,47 @@ importJsonButton.addEventListener("click", () => {
|
|||||||
renderFloorOptions();
|
renderFloorOptions();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
|
saveLibraryToStorage();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Could not import JSON. Please check the format.");
|
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();
|
initGrid();
|
||||||
|
loadLibraryFromStorage();
|
||||||
renderTiles();
|
renderTiles();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
syncPatternPreview();
|
syncPatternPreview();
|
||||||
|
|||||||
@@ -22,6 +22,11 @@
|
|||||||
<main class="app-layout">
|
<main class="app-layout">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Tile Library</h2>
|
<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">
|
<form id="tile-form" class="tile-form">
|
||||||
<label>
|
<label>
|
||||||
Tile name
|
Tile name
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ button:hover {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.tile-form {
|
.tile-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user