<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<title>Tiles Grid + PHP Gallery + Objects + Lines</title>
<style>
:root{ --bg:#121212; --bg2:#1e1e1e; --card:#2a2a2a; --ink:#eee; --ink2:#bbb; --accent:#4fc3f7; }
*{ box-sizing:border-box }
body{ margin:0; font-family:system-ui,ui-sans-serif; background:var(--bg); color:var(--ink); height:100vh; display:flex; flex-direction:column; }
.topbar{ display:flex; gap:0; background:#1e1e1e; box-shadow:0 2px 8px rgba(0,0,0,.5); z-index:10; }
.topbar button{ flex:1; background:none; border:none; color:var(--ink); padding:.9rem 0; font-size:1.2rem; cursor:pointer; }
.topbar button:hover{ color:var(--accent); background:#262626; }
.panel{ max-height:0; overflow:hidden; background:#1b1b1b; transition:max-height .35s ease; border-bottom:1px solid #242424; }
.panel.open{ max-height:360px; }
/* Settings (steppers) */
.settings-track{ display:flex; gap:1rem; overflow-x:auto; padding:.6rem 1rem; }
.stepper{ flex:0 0 auto; background:var(--card); border:1px solid #333; border-radius:.6rem; padding:.5rem; min-width:122px; text-align:center; }
.stepper label{ display:block; font-size:.8rem; color:var(--ink2); margin-bottom:.35rem; }
.stepper-controls{ display:flex; align-items:center; background:#1e1e1e; border-radius:.5rem; overflow:hidden; }
.stepper button{ background:#333; border:none; color:var(--ink); font-size:1.1rem; padding:.35rem 0; width:34%; cursor:pointer; }
.stepper button:hover{ background:var(--accent); color:#000; }
.stepper input{ flex:1; background:#1e1e1e; border:none; color:var(--ink); text-align:center; padding:.25rem 0; font-size:1rem; width:56px; }
/* Gallery from PHP */
.folder-bar{ display:flex; align-items:center; gap:.5rem; padding:.6rem 1rem; border-bottom:1px solid #242424; }
.crumbs{ display:flex; flex-wrap:wrap; gap:.4rem; font-size:.9rem; }
.crumbs button{ background:none; border:none; color:var(--accent); cursor:pointer; padding:.2rem .3rem; }
.crumbs .sep{ color:#666; }
.folders-track, .gallery-track{ display:flex; gap:.8rem; overflow-x:auto; padding:.8rem 1rem; }
.folder-card{ flex:0 0 auto; display:flex; align-items:center; gap:.6rem; background:#222; border:1px solid #333; border-radius:.7rem; padding:.55rem .75rem; cursor:pointer; }
.folder-card:hover{ border-color:var(--accent); background:#2f2f2f; }
.gallery-track img{ width:220px; height:150px; object-fit:cover; border-radius:.8rem; background:#333; flex-shrink:0; border:1px solid #333; cursor:pointer; }
.gallery-track img:hover{ border-color:var(--accent); }
.empty-hint{ color:#aaa; font-size:.9rem; padding:0 1rem .8rem 1rem; }
/* Main content */
.content{ flex:1; display:flex; flex-direction:column; gap:.6rem; padding:1rem; }
.meta{ font-size:.9rem; color:#cfcfcf; opacity:.9; }
/* Objects bar */
.objects { display:flex; align-items:center; gap:.5rem; background:#161616; border:1px solid #2a2a2a; border-radius:.6rem; padding:.5rem .6rem; }
.obj-pill{ background:#222; color:#ddd; border:1px solid #333; border-radius:999px; padding:.35rem .7rem; cursor:pointer; }
.obj-pill.active{ background:var(--accent); color:#000; border-color:transparent; }
.objects .controls{ display:flex; gap:.4rem; margin-left:auto; }
.objects .controls button{ background:#222; color:#ddd; border:1px solid #333; border-radius:.5rem; padding:.35rem .6rem; cursor:pointer; }
.objects .controls button:hover{ background:var(--accent); color:#000; border-color:transparent; }
/* Lines (within active object) */
.lines { display:flex; gap:.5rem; align-items:center; flex-wrap:wrap; background:#161616; border:1px solid #2a2a2a; border-radius:.6rem; padding:.5rem .6rem; }
.line-pill{ background:#222; color:#ddd; border:1px solid #333; border-radius:999px; padding:.35rem .7rem; cursor:pointer; }
.line-pill.active{ background:var(--accent); color:#000; border-color:transparent; }
.lines .controls{ display:flex; gap:.4rem; margin-left:auto; }
.lines .controls button{ background:#222; color:#ddd; border:1px solid #333; border-radius:.5rem; padding:.35rem .6rem; cursor:pointer; }
.lines .controls button:hover{ background:var(--accent); color:#000; border-color:transparent; }
/* Tank (for active line) */
.tank { display:flex; align-items:center; gap:.6rem; background:#161616; border:1px solid #2a2a2a; border-radius:.6rem; padding:.5rem .6rem; }
.tank .count{ color:#bbb; font-size:.9rem; }
.tank-strip{ display:flex; gap:.5rem; overflow-x:auto; padding:.3rem; background:#0f0f0f; border-radius:.4rem; border:1px dashed #333; flex:1; }
.tank-item{ position:relative; flex:0 0 auto; width:72px; height:72px; border:1px solid #2a2a2a; border-radius:.35rem; overflow:hidden; background:#111; }
.tank-item img{ width:100%; height:100%; object-fit:contain; image-rendering:pixelated; display:block; }
.badge{ position:absolute; left:4px; top:4px; background:#000c; color:#fff; border-radius:.3rem; padding:.05rem .3rem; font-weight:700; font-size:.85rem; }
.remove{ position:absolute; right:4px; top:4px; background:#000c; color:#fff; border:none; border-radius:.3rem; padding:.05rem .35rem; cursor:pointer; font-size:.85rem; }
.remove:hover{ background:#f44336; }
/* Stage */
.stage{ position:relative; flex:1; background:#0f0f0f; border:1px solid #2a2a2a; border-radius:.8rem; overflow:hidden; touch-action:none; }
.viewport{ position:absolute; left:0; top:0; transform-origin:0 0; will-change: transform; }
.img-layer{ position:absolute; left:0; top:0; }
.img-layer img{ display:block; image-rendering:pixelated; }
.grid{ position:absolute; left:0; top:0; pointer-events:auto; }
.tile{ position:absolute; border:1px solid red; box-sizing:border-box; pointer-events:auto; }
/* Full-screen Object View */
.overlay{ position:fixed; inset:0; background:rgba(0,0,0,.85); display:none; z-index:50; }
.overlay.open{ display:block; }
.ov-wrap{ position:absolute; inset:3%; background:#111; border:1px solid #333; border-radius:.8rem; display:flex; flex-direction:column; overflow:hidden; }
.ov-head{ display:flex; align-items:center; gap:.6rem; padding:.6rem .8rem; background:#1b1b1b; border-bottom:1px solid #222; }
.ov-head h2{ margin:0; font-size:1.05rem; color:var(--accent); }
.ov-head button{ margin-left:auto; background:#222; color:#ddd; border:1px solid #333; border-radius:.5rem; padding:.35rem .6rem; cursor:pointer; }
.ov-head button:hover{ background:var(--accent); color:#000; border-color:transparent; }
.ov-body{ flex:1; overflow:auto; padding:.8rem; display:flex; flex-direction:column; gap:.8rem; }
.ov-line{ background:#151515; border:1px solid #2a2a2a; border-radius:.6rem; padding:.6rem; }
.ov-line h3{ margin:.1rem 0 .5rem; font-size:.95rem; color:#ddd; }
.ov-strip{ display:flex; gap:.5rem; flex-wrap:wrap; }
.ov-item{ position:relative; width:80px; height:80px; border:1px solid #333; border-radius:.35rem; overflow:hidden; background:#111; }
.ov-item img{ width:100%; height:100%; object-fit:contain; image-rendering:pixelated; }
.ov-badge{ position:absolute; left:4px; top:4px; background:#000c; color:#fff; border-radius:.3rem; padding:.05rem .35rem; font-weight:700; font-size:.85rem; }
</style>
</head>
<body>
<div class="topbar">
<button id="settingsBtn" title="Settings">โ๏ธ</button>
<button id="imagesBtn" title="Images">๐ผ๏ธ</button>
<button id="objectViewBtn" title="Object View">๐๏ธ</button>
<button id="resetBtn" title="Reset view">๐</button>
</div>
<!-- Settings panel -->
<div class="panel" id="settingsPanel">
<div class="settings-track">
<div class="stepper" data-key="tileWidth"><label>tileWidth</label>
<div class="stepper-controls"><button>-</button><input type="number" value="16"><button>+</button></div>
</div>
<div class="stepper" data-key="tileHeight"><label>tileHeight</label>
<div class="stepper-controls"><button>-</button><input type="number" value="16"><button>+</button></div>
</div>
<div class="stepper" data-key="xOffset"><label>xOffset</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="yOffset"><label>yOffset</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="Hspacing"><label>Hspacing</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="Vspacing"><label>Vspacing</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
</div>
</div>
<!-- Images panel (PHP-driven) -->
<div class="panel" id="imagesPanel">
<div class="folder-bar">
<strong style="margin-right:.5rem">๐</strong>
<div class="crumbs" id="crumbs"></div>
<div style="flex:1"></div>
<button id="goUpBtn" title="Up one level" style="background:none;border:1px solid #2a2a2a;border-radius:.4rem;padding:.35rem .6rem;color:var(--ink)">โฌ๏ธ Up</button>
<button id="refreshBtn" title="Refresh" style="margin-left:.4rem;background:none;border:1px solid #2a2a2a;border-radius:.4rem;padding:.35rem .6rem;color:var(--ink)">๐</button>
</div>
<div class="folders-track" id="foldersTrack"></div>
<div class="empty-hint" id="emptyFolders" hidden>No folders here.</div>
<div class="gallery-track" id="galleryTrack"></div>
<div class="empty-hint" id="emptyImages" hidden>No images in this folder.</div>
</div>
<div class="content">
<!-- Objects bar -->
<div class="objects" id="objectsBar">
<div id="objectPills"></div>
<div class="controls">
<button id="addObjectBtn">โ New Object</button>
<button id="renameObjectBtn">โ๏ธ Rename</button>
<button id="removeObjectBtn">๐๏ธ Delete</button>
</div>
</div>
<!-- Lines (for active object) -->
<div class="lines" id="linesBar">
<div id="linePills"></div>
<div class="controls">
<button id="addLineBtn">โ Add Line</button>
<button id="renameLineBtn">โ๏ธ Rename</button>
<button id="removeLineBtn">๐๏ธ Remove</button>
<button id="clearLineBtn">๐งน Clear Line</button>
</div>
</div>
<!-- Tank (active line) -->
<div class="tank" id="tankBar">
<div class="count" id="tankCount">0 items</div>
<div class="tank-strip" id="tankStrip" title="Captured tiles for the selected line appear here"></div>
</div>
<div class="meta" id="meta">Open an image, then tap grid tiles to add thumbnails to the selected line.</div>
<div class="stage" id="stage">
<div class="viewport" id="viewport">
<div class="img-layer" id="imgLayer"></div>
<div class="grid" id="grid"></div>
</div>
</div>
</div>
<!-- Full-screen Object View -->
<div class="overlay" id="objectOverlay" aria-hidden="true">
<div class="ov-wrap">
<div class="ov-head">
<h2 id="ovTitle">Object</h2>
<button id="closeOverlayBtn">โ Close</button>
</div>
<div class="ov-body" id="ovBody"></div>
</div>
</div>
<script>
/* ---------- Constants ---------- */
const ONE_BASED = false; // set true for r1c1 style labels
/* ---------- Panels ---------- */
const settingsBtn = document.getElementById('settingsBtn');
const imagesBtn = document.getElementById('imagesBtn');
const objectViewBtn = document.getElementById('objectViewBtn');
const resetBtn = document.getElementById('resetBtn');
const settingsPanel = document.getElementById('settingsPanel');
const imagesPanel = document.getElementById('imagesPanel');
function closeAllPanels(){ settingsPanel.classList.remove('open'); imagesPanel.classList.remove('open'); }
function togglePanel(p){ const isOpen = p.classList.contains('open'); closeAllPanels(); if(!isOpen) p.classList.add('open'); }
settingsBtn.addEventListener('click', ()=> togglePanel(settingsPanel));
imagesBtn .addEventListener('click', ()=> { togglePanel(imagesPanel); if (!imagesPanel.dataset.inited){ imagesPanel.dataset.inited='1'; openSub(''); } });
/* ---------- Settings ---------- */
const defaults = { tileWidth:16, tileHeight:16, xOffset:0, yOffset:0, Hspacing:0, Vspacing:0 };
let settings = {...defaults};
const stepperInputs = {};
document.querySelectorAll('.stepper').forEach(st=>{
const key = st.dataset.key;
const input = st.querySelector('input');
const [minus, plus] = st.querySelectorAll('button');
stepperInputs[key] = input;
input.value = settings[key];
const stepSize = (key === 'tileWidth' || key === 'tileHeight') ? 8 : 1;
const commit = ()=>{
let v = parseInt(input.value)||0;
if (key === 'tileWidth' || key === 'tileHeight') v = Math.max(1, Math.round(v/8)*8);
else v = Math.max(0, v);
settings[key] = v; input.value = v; drawGrid();
};
minus.addEventListener('click', ()=>{ input.value = (parseInt(input.value)||0) - stepSize; commit(); });
plus .addEventListener('click', ()=>{ input.value = (parseInt(input.value)||0) + stepSize; commit(); });
input.addEventListener('change', commit);
});
function setTileSize(n){
if (!Number.isFinite(n) || n <= 0) return;
settings.tileWidth = settings.tileHeight = n;
if (stepperInputs.tileWidth) stepperInputs.tileWidth.value = n;
if (stepperInputs.tileHeight) stepperInputs.tileHeight.value = n;
drawGrid();
}
/* ---------- Image & Grid ---------- */
const imgLayer = document.getElementById('imgLayer');
const gridEl = document.getElementById('grid');
const meta = document.getElementById('meta');
let imgEl = null, imgW = 0, imgH = 0;
function setImage(src){
const img = new Image();
img.decoding = 'async';
img.onload = ()=>{
imgW = img.naturalWidth; imgH = img.naturalHeight;
imgLayer.innerHTML = '';
img.style.position='absolute'; img.style.left='0px'; img.style.top='0px';
img.style.width = imgW+'px'; img.style.height = imgH+'px';
imgLayer.appendChild(img);
gridEl.style.width = imgW+'px'; gridEl.style.height = imgH+'px';
meta.textContent = `Image: ${imgW}ร${imgH} โ tap tiles to add to the selected line`;
drawGrid(); centerView();
};
img.src = src;
imgEl = img;
}
function drawGrid(){
if (!imgW || !imgH) { gridEl.innerHTML=''; return; }
const tw = settings.tileWidth|0, th = settings.tileHeight|0;
const ox = settings.xOffset|0, oy = settings.yOffset|0;
const hs = settings.Hspacing|0, vs = settings.Vspacing|0;
gridEl.innerHTML = '';
let row = 0;
for (let y = oy; y + th <= imgH; y += th + vs, row++){
let col = 0;
for (let x = ox; x + tw <= imgW; x += tw + hs, col++){
const d = document.createElement('div');
d.className = 'tile';
d.style.left = x + 'px';
d.style.top = y + 'px';
d.style.width = tw + 'px';
d.style.height = th + 'px';
d.dataset.x = x; d.dataset.y = y; d.dataset.w = tw; d.dataset.h = th;
d.dataset.row = row; d.dataset.col = col;
gridEl.appendChild(d);
}
}
}
/* ---------- Pinch-zoom + pan ---------- */
const stage = document.getElementById('stage');
const viewport = document.getElementById('viewport');
let scale = 1, tx = 0, ty = 0;
const MIN_SCALE = 0.25, MAX_SCALE = 16;
const pts = new Map();
function applyTransform(){ viewport.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; }
function centerView(){
if(!imgW || !imgH) return;
const r = stage.getBoundingClientRect();
scale = Math.min( Math.max(1, Math.min(r.width/imgW, r.height/imgH)), 2 );
tx = (r.width - imgW*scale)/2;
ty = (r.height - imgH*scale)/2;
applyTransform();
}
resetBtn.addEventListener('click', centerView);
function dist(a,b){ const dx=a.x-b.x, dy=a.y-b.y; return Math.hypot(dx,dy); }
function screenToWorld(x,y){ const r = stage.getBoundingClientRect(); const sx = x - r.left, sy = y - r.top; return { wx:(sx - tx)/scale, wy:(sy - ty)/scale }; }
let lastPan=null, pinchRef=null;
stage.addEventListener('pointerdown', e=>{
stage.setPointerCapture(e.pointerId); pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if (pts.size===1) lastPan={x:e.clientX,y:e.clientY};
else if (pts.size===2){ const [a,b]=Array.from(pts.values()); pinchRef={ d0:dist(a,b), m0:{x:(a.x+b.x)/2,y:(a.y+b.y)/2}, s0:scale }; }
});
stage.addEventListener('pointermove', e=>{
if(!pts.has(e.pointerId)) return; pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if (pts.size===1 && lastPan){ const p=pts.get(e.pointerId); tx += p.x-lastPan.x; ty += p.y-lastPan.y; lastPan={x:p.x,y:p.y}; applyTransform(); }
else if (pts.size===2 && pinchRef){ const [a,b]=Array.from(pts.values()); const d1=dist(a,b); if(pinchRef.d0>0){
const world = screenToWorld(pinchRef.m0.x, pinchRef.m0.y);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchRef.s0 * (d1/pinchRef.d0)));
const r = stage.getBoundingClientRect(); const sx=pinchRef.m0.x - r.left; const sy=pinchRef.m0.y - r.top;
tx = sx - world.wx * newScale; ty = sy - world.wy * newScale; scale = newScale; applyTransform();
}}
});
function endPointer(e){ try{stage.releasePointerCapture(e.pointerId);}catch{} pts.delete(e.pointerId); if(pts.size<2) pinchRef=null; if(pts.size===0) lastPan=null; }
stage.addEventListener('pointerup', endPointer);
stage.addEventListener('pointercancel', endPointer);
stage.addEventListener('pointerleave', e=>{ if(pts.has(e.pointerId)) endPointer(e); });
let lastTap=0; stage.addEventListener('pointerdown', e=>{ const now=performance.now(); if(now-lastTap<300) centerView(); lastTap=now; }, {capture:true});
stage.addEventListener('wheel', e=>{
if(!imgW||!imgH) return; e.preventDefault();
const k = Math.exp(-e.deltaY * 0.0015);
const r = stage.getBoundingClientRect(); const sx=e.clientX - r.left; const sy=e.clientY - r.top;
const world = screenToWorld(e.clientX, e.clientY);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale * k));
tx = sx - world.wx * newScale; ty = sy - world.wy * newScale; scale = newScale; applyTransform();
},{passive:false});
/* ---------- Workspace: Objects + Lines ---------- */
const objectsBar = document.getElementById('objectsBar');
const objectPills = document.getElementById('objectPills');
const addObjectBtn = document.getElementById('addObjectBtn');
const renameObjectBtn = document.getElementById('renameObjectBtn');
const removeObjectBtn = document.getElementById('removeObjectBtn');
const linesBar = document.getElementById('linesBar');
const linePills = document.getElementById('linePills');
const addLineBtn = document.getElementById('addLineBtn');
const renameLineBtn = document.getElementById('renameLineBtn');
const removeLineBtn = document.getElementById('removeLineBtn');
const clearLineBtn = document.getElementById('clearLineBtn');
const tankStrip = document.getElementById('tankStrip');
const tankCount = document.getElementById('tankCount');
/* workspace schema:
workspace = {
objects: [
{ id, name, lines:[ {id, name, items:[{x,y,w,h,row,col,badge,thumbDataURL}]} ], activeLineId }
],
activeObjectId
}
*/
const workspace = {
objects: [
{ id: rid(), name: 'Object 1', lines: [ {id: rid(), name:'Line 1', items:[]} ], activeLineId: null }
],
activeObjectId: null
};
workspace.objects[0].activeLineId = workspace.objects[0].lines[0].id;
workspace.activeObjectId = workspace.objects[0].id;
function rid(){ return Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,10); }
function activeObject(){ return workspace.objects.find(o=>o.id===workspace.activeObjectId) || null; }
function activeLine(){
const o = activeObject(); if(!o) return null;
return o.lines.find(l=> l.id === o.activeLineId) || null;
}
/* Objects controls */
function renderObjects(){
objectPills.innerHTML='';
for (const o of workspace.objects){
const b = document.createElement('button');
b.className = 'obj-pill' + (o.id===workspace.activeObjectId ? ' active' : '');
b.textContent = o.name;
b.onclick = ()=>{ workspace.activeObjectId = o.id; if(!o.activeLineId && o.lines[0]) o.activeLineId=o.lines[0].id; renderObjects(); renderLines(); renderActiveTank(); };
objectPills.appendChild(b);
}
}
function addObject(){
const idx = workspace.objects.length + 1;
const obj = { id: rid(), name: `Object ${idx}`, lines: [ {id: rid(), name:'Line 1', items:[]} ], activeLineId: null };
obj.activeLineId = obj.lines[0].id;
workspace.objects.push(obj);
workspace.activeObjectId = obj.id;
renderObjects(); renderLines(); renderActiveTank();
}
function renameObject(){
const o = activeObject(); if(!o) return;
const name = prompt('Rename object:', o.name);
if (name && name.trim()){ o.name = name.trim(); renderObjects(); }
}
function removeObject(){
if (workspace.objects.length === 1){ alert('At least one object is required.'); return; }
const idx = workspace.objects.findIndex(o=> o.id===workspace.activeObjectId);
if (idx>=0){ workspace.objects.splice(idx,1); const next = workspace.objects[Math.max(0, idx-1)]; workspace.activeObjectId = next.id; renderObjects(); renderLines(); renderActiveTank(); }
}
addObjectBtn.addEventListener('click', addObject);
renameObjectBtn.addEventListener('click', renameObject);
removeObjectBtn.addEventListener('click', removeObject);
/* Lines controls (within active object) */
function renderLines(){
const o = activeObject(); linePills.innerHTML='';
if (!o) return;
for (const l of o.lines){
const b = document.createElement('button');
b.className = 'line-pill' + (l.id===o.activeLineId ? ' active' : '');
b.textContent = l.name;
b.onclick = ()=>{ o.activeLineId = l.id; renderLines(); renderActiveTank(); };
linePills.appendChild(b);
}
}
function addLine(){
const o = activeObject(); if(!o) return;
const idx = o.lines.length + 1;
const line = { id: rid(), name: `Line ${idx}`, items: [] };
o.lines.push(line); o.activeLineId = line.id; renderLines(); renderActiveTank();
}
function renameLine(){
const o = activeObject(); if(!o) return;
const l = activeLine(); if(!l) return;
const name = prompt('Rename line:', l.name);
if (name && name.trim()){ l.name = name.trim(); renderLines(); }
}
function removeLine(){
const o = activeObject(); if(!o) return;
if (o.lines.length===1){ alert('At least one line is required.'); return; }
const idx = o.lines.findIndex(l=> l.id===o.activeLineId);
if (idx>=0){ o.lines.splice(idx,1); const next=o.lines[Math.max(0, idx-1)]; o.activeLineId=next.id; renderLines(); renderActiveTank(); }
}
function clearLine(){
const l = activeLine(); if(!l) return;
l.items = []; renderActiveTank();
}
addLineBtn.addEventListener('click', addLine);
renameLineBtn.addEventListener('click', renameLine);
removeLineBtn.addEventListener('click', removeLine);
clearLineBtn.addEventListener('click', clearLine);
function renderActiveTank(){
const l = activeLine(); tankStrip.innerHTML='';
if (!l){ tankCount.textContent='0 items'; return; }
l.items.forEach((t, i)=>{
const item = document.createElement('div'); item.className='tank-item'; item.title = `${t.badge} (${t.w}ร${t.h}) @ (${t.x},${t.y})`;
const img = document.createElement('img'); img.src = t.thumbDataURL;
const badge = document.createElement('div'); badge.className='badge'; badge.textContent = t.badge;
const remove = document.createElement('button'); remove.className='remove'; remove.textContent='ร'; remove.title='Remove';
remove.onclick = ()=>{ l.items.splice(i,1); renderActiveTank(); };
item.appendChild(img); item.appendChild(badge); item.appendChild(remove);
tankStrip.appendChild(item);
});
tankCount.textContent = `${l.items.length} item${l.items.length===1?'':'s'}`;
}
renderObjects(); renderLines(); renderActiveTank();
/* ---------- Capture tiles into ACTIVE line of ACTIVE object ---------- */
gridEl.addEventListener('click', (e)=>{
const tile = e.target.closest('.tile');
if (!tile || !imgEl) return;
e.stopPropagation(); // avoid panning
const x = parseInt(tile.dataset.x, 10);
const y = parseInt(tile.dataset.y, 10);
const w = parseInt(tile.dataset.w, 10);
const h = parseInt(tile.dataset.h, 10);
let row = parseInt(tile.dataset.row, 10);
let col = parseInt(tile.dataset.col, 10);
if (ONE_BASED){ row += 1; col += 1; }
const badgeText = `r${row}c${col}`;
// make thumbnail
const cv = document.createElement('canvas'); cv.width = w; cv.height = h;
const cctx = cv.getContext('2d'); cctx.imageSmoothingEnabled = false;
cctx.drawImage(imgEl, x, y, w, h, 0, 0, w, h);
const dataURL = cv.toDataURL('image/png');
const l = activeLine(); if(!l){ alert('No active line selected.'); return; }
l.items.push({ x,y,w,h,row,col,badge:badgeText, thumbDataURL:dataURL });
renderActiveTank();
});
/* ---------- Full-screen Object View ---------- */
const objectOverlay = document.getElementById('objectOverlay');
const ovTitle = document.getElementById('ovTitle');
const ovBody = document.getElementById('ovBody');
const closeOverlayBtn = document.getElementById('closeOverlayBtn');
function openObjectView(){
const o = activeObject(); if(!o) return;
ovTitle.textContent = o.name;
ovBody.innerHTML = '';
for (const line of o.lines){
const block = document.createElement('div'); block.className='ov-line';
const h = document.createElement('h3'); h.textContent = line.name;
const strip = document.createElement('div'); strip.className='ov-strip';
for (const t of line.items){
const item = document.createElement('div'); item.className='ov-item';
const img = document.createElement('img'); img.src = t.thumbDataURL;
const badge = document.createElement('div'); badge.className='ov-badge'; badge.textContent = t.badge;
item.appendChild(img); item.appendChild(badge);
strip.appendChild(item);
}
block.appendChild(h); block.appendChild(strip);
ovBody.appendChild(block);
}
objectOverlay.classList.add('open');
objectOverlay.setAttribute('aria-hidden','false');
}
function closeObjectView(){
objectOverlay.classList.remove('open');
objectOverlay.setAttribute('aria-hidden','true');
}
objectViewBtn.addEventListener('click', openObjectView);
closeOverlayBtn.addEventListener('click', closeObjectView);
objectOverlay.addEventListener('click', (e)=>{ if(e.target===objectOverlay) closeObjectView(); });
/* ---------- PHP gallery wiring ---------- */
const crumbsEl = document.getElementById('crumbs');
const foldersTrack = document.getElementById('foldersTrack');
const emptyFolders = document.getElementById('emptyFolders');
const galleryTrack = document.getElementById('galleryTrack');
const emptyImages = document.getElementById('emptyImages');
const goUpBtn = document.getElementById('goUpBtn');
const refreshBtn = document.getElementById('refreshBtn');
let currentSub = '';
async function fetchDir(sub=''){
const res = await fetch(`media.php?sub=${encodeURIComponent(sub)}`, {cache:'no-store'});
if(!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json();
}
function renderBreadcrumb(bc){
crumbsEl.innerHTML=''; bc.forEach((c,i)=>{ const b=document.createElement('button'); b.textContent=c.label; b.onclick=()=>openSub(c.sub); crumbsEl.appendChild(b);
if(i<bc.length-1){ const s=document.createElement('span'); s.className='sep'; s.textContent='โบ'; crumbsEl.appendChild(s);} });
}
function renderFolders(items){
foldersTrack.innerHTML=''; emptyFolders.hidden = !!items.length;
for(const f of items){ const card=document.createElement('div'); card.className='folder-card';
card.innerHTML=`<span>๐</span><span>${f.name}</span>`; card.onclick=()=>openSub(f.sub); foldersTrack.appendChild(card); }
}
function renderImages(items){
galleryTrack.innerHTML=''; emptyImages.hidden = !!items.length;
for(const im of items){ const img=document.createElement('img'); img.alt=im.name; img.loading='lazy'; img.decoding='async'; img.src=im.url;
img.onclick=()=> setImage(im.url); galleryTrack.appendChild(img); }
}
/* Numeric folder -> auto tile size */
function inferTileFromPath(sub){
if (!sub) return null;
const parts = sub.split('/').filter(Boolean).reverse();
for (const p of parts){
const n = parseInt(p, 10);
if (String(n) === p && n > 0) return n;
}
return null;
}
async function openSub(sub){
try{
const data = await fetchDir(sub);
currentSub = data.cwd||'';
renderBreadcrumb(data.breadcrumb||[]);
renderFolders(data.folders||[]);
renderImages(data.images||[]);
const inferred = inferTileFromPath(currentSub);
if (inferred) setTileSize(inferred);
} catch(err){
console.error(err);
alert('Failed to load folder.');
}
}
function parentOf(sub){ if(!sub) return ''; const p=sub.split('/').filter(Boolean); p.pop(); return p.join('/'); }
goUpBtn .addEventListener('click', ()=> openSub(parentOf(currentSub)));
refreshBtn .addEventListener('click', ()=> openSub(currentSub));
/* ---------- Boot ---------- */
document.addEventListener('DOMContentLoaded', ()=>{
renderObjects(); renderLines(); renderActiveTank();
openSub(''); // PHP gallery root
});
</script>
</body>
</html>