๐ŸŒ
index.html
โ† Back
๐Ÿ“ Html โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
<!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>