円錐形のフォルダビューワ「Folder Viewer 3D」
[ 操作説明 ]
・上キーで下の階層に進む。
・下キーで上の階層に戻る。
・左右のキーで移動先を選ぶ。
・homeキーでルートフォルダに戻る。
・Enterキーで選択中のファイルを開く。
・escキーで閉じる。
・ダウンロードされる方はこちら。↓
・バージョン1.3のソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Cyber Stream Explorer 1.3</title>
<style>
:root {
--main: #ffcc00; /* Amber */
--accent: #00ffff; /* Cyan */
--bg: #010101; /* Black */
}
body { margin: 0; overflow: hidden; background-color: var(--bg); font-family: 'Segoe UI', sans-serif; user-select: none; color: #fff; }
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#error-log { position: absolute; bottom: 10px; left: 10px; color: #ff0000; font-family: monospace; background: rgba(0,0,0,0.8); padding: 5px; display: none; pointer-events: auto; }
#header { position: absolute; top: 20px; left: 20px; pointer-events: auto; }
button.sys-btn { background: rgba(50, 40, 0, 0.6); border: 1px solid var(--main); color: var(--main); padding: 10px 24px; font-weight: bold; cursor: pointer; text-transform: uppercase; letter-spacing: 2px; }
button.sys-btn:hover { background: rgba(255, 200, 0, 0.3); color: #fff; }
#path-display { color: var(--accent); font-family: monospace; font-size: 18px; font-weight: bold; text-shadow: 0 0 5px var(--accent); margin-top: 10px; }
.instructions {
position: absolute; top: 20px; right: 20px; text-align: right;
color: rgba(200, 220, 255, 0.9); font-size: 13px; line-height: 1.8;
background: rgba(0, 0, 0, 0.7); padding: 20px; border-right: 4px solid var(--main);
pointer-events: auto; box-shadow: 0 0 20px rgba(0,0,0,0.5);
}
.key {
border: 1px solid #fff; padding: 2px 6px; border-radius: 4px;
background: #fff; color: #000; font-weight: bold; margin: 0 3px; font-family: monospace;
}
#loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 9000; display: none; justify-content: center; align-items: center; color: var(--main); font-size: 24px; font-weight: bold; letter-spacing: 4px; }
#stock { position: absolute; bottom: 0; left: 0; width: 100%; height: 160px; background: linear-gradient(to top, rgba(10,5,0,0.95), transparent); border-top: 2px solid var(--main); display: flex; align-items: center; padding: 0 40px; gap: 20px; z-index: 40; transition: bottom 0.3s; overflow-x: auto; pointer-events: auto; }
#stock.closed { bottom: -160px; }
#stock-tabs { position: absolute; bottom: 160px; left: 50%; transform: translateX(-50%); display: flex; gap: 4px; z-index: 41; transition: bottom 0.3s; pointer-events: auto; }
#stock.closed + #stock-tabs { bottom: 0; }
.tab-btn { background: rgba(20, 10, 0, 0.9); color: #888; padding: 6px 20px; cursor: pointer; border-top: 2px solid #553300; min-width: 100px; text-align: center; font-weight: bold; clip-path: polygon(10% 0, 90% 0, 100% 100%, 0% 100%); }
.tab-btn.active { background: var(--main); color: #000; border-top-color: #fff; }
.stock-item { width: 100px; height: 120px; background: rgba(0,0,0,0.5); border: 1px solid var(--main); display: flex; flex-direction: column; align-items: center; cursor: grab; color: var(--main); flex-shrink: 0; position: relative; }
.stock-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }
#viewer-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 100; justify-content: center; align-items: center; pointer-events: auto; perspective: 1500px; }
#viewer-win { width: 90%; height: 85%; border: 2px solid var(--main); background: #050505; display: flex; flex-direction: column; transform-style: preserve-3d; opacity: 0; box-shadow: 0 0 50px rgba(255, 200, 0, 0.2); }
#viewer-overlay.active #viewer-win { animation: animFlipOpen 0.6s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
#viewer-overlay.closing #viewer-win { animation: animFlipClose 0.5s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
@keyframes animFlipOpen { 0% { transform: translateZ(-500px) rotateX(180deg) scale(0.5); opacity: 0; } 100% { transform: translateZ(0) rotateX(0deg) scale(1); opacity: 1; } }
@keyframes animFlipClose { 0% { transform: translateZ(0) rotateX(0deg) scale(1); opacity: 1; } 100% { transform: translateZ(-500px) rotateX(-180deg) scale(0.5); opacity: 0; } }
#viewer-head { padding: 10px 20px; background: rgba(255, 200, 0, 0.1); border-bottom: 1px solid var(--main); color: var(--main); font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
#viewer-body { flex: 1; overflow: hidden; position: relative; display:flex; justify-content:center; align-items:center; }
#viewer-body img, #viewer-body video { max-width: 100%; max-height: 100%; object-fit: contain; }
.win-btn { background: transparent; border: 1px solid var(--main); color: var(--main); padding: 5px 20px; cursor: pointer; margin-left: 10px; font-weight: bold; transition:0.2s;}
.win-btn:hover { background: var(--main); color: #000; }
#dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 500; justify-content: center; align-items: center; pointer-events: auto; }
#dialog-box { background: #080808; border: 1px solid var(--accent); padding: 30px; width: 450px; text-align: center; box-shadow: 0 0 30px rgba(0,255,255,0.2); }
#dialog-title { color: var(--accent); font-weight: bold; margin-bottom: 20px; display: block; font-size: 18px; }
#dialog-input { width: 100%; background: #111; border: 1px solid #333; color: #fff; padding: 12px; font-size: 18px; outline: none; margin-bottom: 25px; text-align: center; }
#dialog-input:focus { border-color: var(--accent); }
.ctx-menu { display: none; position: absolute; z-index: 200; background: rgba(5, 10, 15, 0.98); border: 1px solid var(--main); min-width: 200px; pointer-events: auto; }
.ctx-item { padding: 12px 20px; color: #ddd; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.1); }
.ctx-item:hover { background: var(--main); color: #000; }
#drag-ghost { position: absolute; pointer-events: none; z-index: 9999; padding: 10px 20px; background: rgba(0,0,0,0.8); border: 1px solid var(--main); color: var(--main); font-weight: bold; border-radius: 5px; display: none; }
</style>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
</script>
</head>
<body>
<div id="ui-layer">
<div id="error-log"></div>
<div id="header">
<button id="btn-init" class="sys-btn">INITIALIZE SYSTEM</button>
<div id="path-display">SYSTEM STANDBY</div>
</div>
<div class="instructions">
<b>CONTROLS:</b><br>
<span class="key">←</span> <span class="key">→</span> : Rotate & Select<br>
<span class="key">↑</span> / <span class="key">Ent</span> : Open<br>
<span class="key">↓</span> : Back<br>
<span class="key">Drag</span> : Move Camera<br>
<span class="key">R-Click</span> : Menu
</div>
<div id="stock" class="closed"></div>
<div id="stock-tabs"></div>
</div>
<div id="viewer-overlay">
<div id="viewer-win">
<div id="viewer-head">
<span id="viewer-title">FILE</span>
<div><button id="viewer-close" class="win-btn">CLOSE</button></div>
</div>
<div id="viewer-body"></div>
</div>
</div>
<div id="ctx-menu" class="ctx-menu">
<div class="ctx-item" id="cm-open">Open</div>
<div class="ctx-item" id="cm-copy">Copy</div>
<div class="ctx-item" id="cm-paste">Paste</div>
<div class="ctx-item" id="cm-rename">Rename</div>
<div class="ctx-item" id="cm-new">New Folder</div>
<div class="ctx-item" id="cm-stock">Add to Stock</div>
<div class="ctx-item" id="cm-del" style="color:#ff5555;">Delete</div>
</div>
<div id="ctx-tab" class="ctx-menu">
<div class="ctx-item" id="cm-tab-hide">Hide Stock</div>
<div class="ctx-item" id="cm-tab-cancel" style="color:#ff5555;">Cancel Tab</div>
</div>
<div id="dialog-overlay">
<div id="dialog-box">
<span id="dialog-title">INPUT</span>
<input type="text" id="dialog-input">
<div>
<button id="dialog-ok" class="sys-btn" style="border-color:var(--accent);color:var(--accent);width:120px;">OK</button>
<button id="dialog-cancel" class="sys-btn" style="border-color:#ff5555;color:#ff5555;width:120px;">CANCEL</button>
</div>
</div>
</div>
<div id="drag-ghost">MOVING...</div>
<div id="loading"><div class="blink">SCANNING...</div></div>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import TWEEN from 'three/addons/libs/tween.module.js';
const CONFIG = {
layerDepth: 800,
coneSlope: 0.7,
baseRadius: 180,
camHeight: 280,
camDist: 650,
camLookAhead: 750,
nodeWidth: 180,
spacingFactor: 2.5,
colMain: 0xffcc00,
colFile: 0x00aaff,
colLine: 0xffaa00,
bloomStr: 2.5, bloomRad: 0.8, bloomThres: 0.7,
flowSpeedNormal: 20,
flowSpeedFast: 400
};
const state = {
rootNode: null, activeNode: null, targetChildIdx: 0,
visibleNodes: [], stockList: [], activeStockIdx: -1, clipboard: null,
focusPointZ: 0, targetFocusZ: 0, currentRotation: 0, targetRotation: 0,
mouse: new THREE.Vector2(), raycaster: new THREE.Raycaster(),
ctxTarget: null, tabCtxIdx: -1,
dragging: null, isCamDragging: false, lastMouse: new THREE.Vector2(),
isMoving: false, centerMesh: null,
currentFlowSpeed: CONFIG.flowSpeedNormal
};
const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x020202, 0.0005);
const camera = new THREE.PerspectiveCamera(55, window.innerWidth/window.innerHeight, 1, 60000);
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), CONFIG.bloomStr, CONFIG.bloomRad, CONFIG.bloomThres);
composer.addPass(bloom);
const world = new THREE.Group(); scene.add(world);
const grid = new THREE.GridHelper(80000, 400, 0x332200, 0x080500); grid.position.y = -2000; scene.add(grid);
// Stream Lines (Background)
const streamGroup = new THREE.Group(); scene.add(streamGroup);
const streamGeo = new THREE.BufferGeometry();
const streamPos = [];
for(let i=0; i<1500; i++) {
const x=(Math.random()-0.5)*15000, y=(Math.random()-0.5)*15000, z=(Math.random()-0.5)*30000;
const len = 3000 + Math.random() * 4000;
streamPos.push(x,y,z, x,y,z-len);
}
streamGeo.setAttribute('position', new THREE.Float32BufferAttribute(streamPos, 3));
const streamMat = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.6 });
const streamLines = new THREE.LineSegments(streamGeo, streamMat);
streamGroup.add(streamLines);
// --- OVERLAYS ---
// 1. Info Mesh
const infoMesh = new THREE.Mesh(new THREE.PlaneGeometry(600, 300), new THREE.MeshBasicMaterial({ transparent:true, opacity:0, depthTest:false, side: THREE.DoubleSide }));
infoMesh.renderOrder = 9999; scene.add(infoMesh); infoMesh.visible = false;
function updateInfoTexture(node) {
if(!node) return;
const w=1024; const h=512;
const c = document.createElement('canvas'); c.width=w; c.height=h;
const ctx = c.getContext('2d');
ctx.shadowColor = "#00ffff"; ctx.shadowBlur = 10;
ctx.font = "bold 50px 'Segoe UI', monospace";
let y = 100; const x = 50;
ctx.fillStyle = "#00ffff"; ctx.textAlign = 'left'; ctx.fillText("NAME: ", x, y);
ctx.fillStyle = "#ffffff"; ctx.fillText(node.name, x + 180, y);
y += 80;
ctx.fillStyle = "#00ffff"; ctx.fillText("KIND: ", x, y);
ctx.fillStyle = "#cccccc"; ctx.fillText(node.kind.toUpperCase(), x + 180, y);
y += 80;
ctx.fillStyle = "#00ffff"; ctx.fillText("INFO: ", x, y);
ctx.fillStyle = "#cccccc"; ctx.fillText(`${node.sizeStr || '-'} / ${node.dateStr || '-'}`, x + 180, y);
infoMesh.material.map = new THREE.CanvasTexture(c);
infoMesh.material.map.colorSpace = THREE.SRGBColorSpace;
infoMesh.material.needsUpdate = true;
infoMesh.material.opacity = 1;
}
// 2. Magnified Mesh
const magMesh = new THREE.Mesh(new THREE.PlaneGeometry(320, 420), new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0, depthTest: false }));
magMesh.renderOrder = 9998; scene.add(magMesh); magMesh.visible = false;
// 3. Indicator Line
const lineGeo = new THREE.BufferGeometry();
const lineMesh = new THREE.Line(lineGeo, new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8, depthTest: false }));
lineMesh.renderOrder = 9999; scene.add(lineMesh); lineMesh.visible = false;
// 4. Center Cursor
const centerCursor = new THREE.Mesh(new THREE.RingGeometry(160, 170, 64), new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent:true, opacity:0.8 }));
scene.add(centerCursor); centerCursor.visible = false;
// 5. Exp3D Style Target Cursor (4 Layers)
const reticleGroup = new THREE.Group();
scene.add(reticleGroup); reticleGroup.visible = false;
function createReticle() {
// Layer 1: Inner Solid Ring
const g1 = new THREE.RingGeometry(130, 133, 64);
const m1 = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent:true, opacity:0.9 });
const r1 = new THREE.Mesh(g1, m1);
r1.userData = { speed: 0.02 };
reticleGroup.add(r1);
// Layer 2: Segmented Ring
const g2 = new THREE.RingGeometry(138, 145, 64, 1, 0, Math.PI * 1.5);
const m2 = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent:true, opacity:0.6 });
const r2 = new THREE.Mesh(g2, m2);
r2.userData = { speed: -0.03 };
reticleGroup.add(r2);
// Layer 3: Dashed Ring
const g3 = new THREE.RingGeometry(150, 152, 32);
const m3 = new THREE.MeshBasicMaterial({ color: 0xffaa00, side: THREE.DoubleSide, transparent:true, opacity:0.8, wireframe:false });
// Create dashes by texture or multiple segments. Using segments here.
const r3 = new THREE.Group();
for(let i=0; i<8; i++){
const s = new THREE.Mesh(new THREE.RingGeometry(150,152,8,1, i*(Math.PI/4), Math.PI/8), m3);
r3.add(s);
}
r3.userData = { speed: 0.015 };
reticleGroup.add(r3);
// Layer 4: Outer Brackets
const r4 = new THREE.Group();
const m4 = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent:true, opacity:1.0 });
const s4a = new THREE.Mesh(new THREE.RingGeometry(160, 165, 32, 1, -0.5, 1.0), m4);
const s4b = new THREE.Mesh(new THREE.RingGeometry(160, 165, 32, 1, Math.PI-0.5, 1.0), m4);
r4.add(s4a); r4.add(s4b);
r4.userData = { speed: -0.05 };
reticleGroup.add(r4);
}
createReticle();
// Particles
let arrowTexture;
function createArrowTexture() {
const c=document.createElement('canvas'); c.width=64; c.height=64; const x=c.getContext('2d');
x.fillStyle='#ffaa00'; x.beginPath(); x.moveTo(32,10); x.lineTo(54,54); x.lineTo(10,54); x.closePath(); x.fill();
arrowTexture = new THREE.CanvasTexture(c);
}
createArrowTexture();
const particles = [];
function spawnArrow(startPos, endPos) {
const m = new THREE.SpriteMaterial({ map: arrowTexture, color: 0xffffff, transparent: true, opacity: 1.0, depthTest: false });
const s = new THREE.Sprite(m); s.scale.set(30, 30, 1); s.position.copy(startPos); scene.add(s);
particles.push({ mesh: s, start: startPos.clone(), end: endPos.clone(), progress: 0, speed: 0.02+Math.random()*0.015 });
}
function init() {
window.addEventListener('resize', onResize);
document.getElementById('btn-init').onclick = selectRoot;
document.addEventListener('keydown', e => {
const v = document.getElementById('viewer-overlay');
if(v.classList.contains('active')) {
const isEditor = document.getElementById('editor-area');
if(isEditor && document.activeElement === isEditor) { if(e.key === 'Escape') closeViewer(); return; }
if(['ArrowDown', 'Enter', 'Escape'].includes(e.key)) { e.preventDefault(); closeViewer(); }
return;
}
if(document.getElementById('dialog-overlay').style.display==='flex') return;
onKey(e);
});
document.addEventListener('contextmenu', onContextMenu);
window.addEventListener('click', e => { if(!e.target.closest('.ctx-menu')) document.querySelectorAll('.ctx-menu').forEach(m => m.style.display='none'); });
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
document.addEventListener('dblclick', onMouseDoubleClick);
const container = document.body;
container.ondragover = e => e.preventDefault();
container.ondrop = async e => { e.preventDefault(); const idx = e.dataTransfer.getData('stockIndex'); if(idx) moveFromStock(parseInt(idx)); };
bindMenu(); bindDialog(); bindViewer();
animate();
}
async function selectRoot() {
try {
const h = await window.showDirectoryPicker();
showLoad(true);
state.rootNode = await scanNode(h, null, 0);
state.activeNode = state.rootNode;
state.targetChildIdx = 0;
state.focusPointZ = state.rootNode.z;
rebuildWorld();
showLoad(false);
updatePath();
} catch(e) { console.error(e); showLoad(false); }
}
async function scanNode(handle, parent, depth) {
const node = {
handle: handle, name: handle.name, kind: handle.kind,
parent: parent, depth: depth, children: [],
angle: 0, z: -depth * CONFIG.layerDepth,
obj: null, visualLine: null, thumbTex: null, sizeStr: '', dateStr: '',
currentScale: 1.0, currentLift: 0.0
};
if(handle.kind === 'file') {
handle.getFile().then(f => {
node.sizeStr = (f.size/1024).toFixed(1) + ' KB';
node.dateStr = new Date(f.lastModified).toLocaleDateString();
const fType = f.type.toLowerCase();
const loadT = (t) => {
node.thumbTex = t; node.thumbTex.colorSpace = THREE.SRGBColorSpace;
if(node.obj) updateNodeTexture(node);
};
if(fType.startsWith('image/')) {
if(fType === 'image/bmp' && window.createImageBitmap) createImageBitmap(f).then(bmp => loadT(new THREE.CanvasTexture(bmp)));
else { const url = URL.createObjectURL(f); new THREE.TextureLoader().load(url, (t) => { loadT(t); URL.revokeObjectURL(url); }); }
} else if(fType.startsWith('video/')) {
const v = document.createElement('video'); v.src = URL.createObjectURL(f); v.muted = true; v.preload = 'metadata'; v.playsInline = true; v.loop = true;
v.play().then(() => { const vTex = new THREE.VideoTexture(v); vTex.colorSpace = THREE.SRGBColorSpace; node.thumbTex = vTex; if(node.obj) updateNodeTexture(node); })
.catch(e => { v.onloadeddata = () => { v.currentTime = 0.5; }; v.onseeked = () => { const c = document.createElement('canvas'); c.width=320; c.height=180; c.getContext('2d').drawImage(v,0,0,320,180); loadT(new THREE.CanvasTexture(c)); }; });
}
}).catch(()=>{});
} else node.sizeStr = '<DIR>';
if (handle.kind === 'directory') {
try {
const entries = []; for await (const e of handle.values()) entries.push(e);
entries.sort((a,b) => (a.kind===b.kind ? a.name.localeCompare(b.name) : (a.kind==='directory'?-1:1)));
for (const e of entries) node.children.push(await scanNode(e, node, depth+1));
} catch(e){}
}
return node;
}
function updateNodeTexture(node) {
if(node.obj && node.kind === 'file' && node.thumbTex) {
const thumbMesh = node.obj.getObjectByName('thumbMesh');
if(thumbMesh) {
thumbMesh.material.map = node.thumbTex;
thumbMesh.material.needsUpdate = true;
thumbMesh.material.color.setHex(0xffffff);
}
}
}
function calculateRadius(count) {
const itemArc = CONFIG.nodeWidth * CONFIG.spacingFactor;
const circumference = count * itemArc;
const minRadius = circumference / (2 * Math.PI);
return Math.max(CONFIG.baseRadius, minRadius);
}
function createCenterVisual() {
if(state.centerMesh) { scene.remove(state.centerMesh); state.centerMesh = null; }
if(!state.activeNode) return;
const grp = new THREE.Group();
grp.position.set(0, 0, state.activeNode.z);
const geo = new THREE.BoxGeometry(100, 100, 100);
const mat = new THREE.MeshBasicMaterial({ color: CONFIG.colMain, wireframe: true, transparent:true, opacity:0.3 });
const cube = new THREE.Mesh(geo, mat);
grp.add(cube);
const inner = new THREE.Mesh(new THREE.BoxGeometry(80,80,80), new THREE.MeshBasicMaterial({ color: CONFIG.colMain, transparent:true, opacity:0.8 }));
grp.add(inner);
const cvs = document.createElement('canvas'); cvs.width=512; cvs.height=128;
const ctx = cvs.getContext('2d');
ctx.font = "bold 80px 'Segoe UI'"; ctx.fillStyle = "#ffffff"; ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.shadowBlur=10; ctx.shadowColor="#ffcc00";
ctx.fillText(state.activeNode.name, 256, 64);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(cvs), transparent: true, depthWrite: false }));
sprite.scale.set(300, 75, 1);
sprite.position.set(0, -100, 0);
grp.add(sprite);
scene.add(grp);
state.centerMesh = grp;
centerCursor.visible = true;
centerCursor.position.set(0, 0, state.activeNode.z);
centerCursor.rotation.set(0,0,0);
}
function rebuildWorld() {
while(world.children.length) { const c = world.children[0]; world.remove(c); if(c.geometry) c.geometry.dispose(); }
state.visibleNodes = [];
createCenterVisual();
if(!state.activeNode) return;
let p = state.activeNode; while(p) { createNodeVisual(p, true); p = p.parent; }
const children = state.activeNode.children;
if (children.length > 0) {
const childZ = state.activeNode.z - CONFIG.layerDepth;
const radius = calculateRadius(children.length);
const step = (Math.PI * 2) / Math.max(children.length, 1);
children.forEach((child, i) => {
child.angle = i * step;
child.z = childZ;
createNodeVisual(child, false, radius);
});
}
const target = state.activeNode.children[state.targetChildIdx];
if(state.activeNode.parent && state.activeNode.parent.obj && state.activeNode.obj) createConnection(state.activeNode.parent, state.activeNode, true);
children.forEach(c => { if(c.obj && state.activeNode.obj) createConnection(state.activeNode, c, (c === target)); });
rotateToTarget();
}
function createNodeVisual(node, isCenter, overrideRadius) {
if (node.obj && node.obj.parent === world) return;
const grp = new THREE.Group();
let x=0, y=0;
if (!isCenter) {
const r = overrideRadius || calculateRadius(1);
x = Math.cos(node.angle) * r;
y = Math.sin(node.angle) * r;
}
grp.position.set(x, y, node.z);
if (!isCenter) {
grp.rotation.z = node.angle + Math.PI/2;
}
const w = CONFIG.nodeWidth; const h = w * 1.3;
// Increased Font Size and Scale
const nCvs = document.createElement('canvas'); nCvs.width=1024; nCvs.height=256;
const nCtx = nCvs.getContext('2d');
nCtx.font = `bold 120px "Segoe UI", Arial`; nCtx.textAlign='center'; nCtx.textBaseline='middle';
const tm = nCtx.measureText(node.name);
const maxW = nCvs.width * 0.95;
nCtx.save(); nCtx.translate(nCvs.width/2, nCvs.height/2);
if(tm.width > maxW) nCtx.scale(maxW/tm.width, 1);
nCtx.shadowBlur=8; nCtx.shadowColor="#000";
nCtx.fillStyle = '#ffffff'; nCtx.fillText(node.name, 0, 0); nCtx.restore();
const nameSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(nCvs), transparent: true, depthWrite: false }));
nameSprite.scale.set(w * 1.5, w * 0.4, 1);
nameSprite.position.set(0, 0, 0);
grp.add(nameSprite);
if (node.kind === 'directory') {
const isEmpty = node.children.length === 0;
const depth = isEmpty ? 20 : w;
const geo = new THREE.BoxGeometry(w, w, depth);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({ color: CONFIG.colMain }));
grp.add(edges); grp.userData.rotMesh = edges;
const hit = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ visible: false }));
grp.add(hit); hit.userData.node = node; state.visibleNodes.push(hit);
nameSprite.position.set(0, -w/2 - 80, 0);
} else {
const bgGeo = new THREE.PlaneGeometry(w, h);
let mat;
if(node.thumbTex) {
mat = new THREE.MeshBasicMaterial({ map: node.thumbTex, side: THREE.DoubleSide, color: 0xffffff });
} else {
const pCvs = document.createElement('canvas'); pCvs.width=256; pCvs.height=320;
const pCtx = pCvs.getContext('2d');
pCtx.fillStyle='rgba(0, 20, 40, 0.95)'; pCtx.fillRect(0,0,256,320);
pCtx.fillStyle='#008800'; pCtx.font='14px monospace';
pCtx.fillText("> FILE", 10, 30);
const defaultTex = new THREE.CanvasTexture(pCvs);
mat = new THREE.MeshBasicMaterial({ map: defaultTex, side: THREE.DoubleSide, color: 0x999999 });
}
const thumb = new THREE.Mesh(bgGeo, mat);
thumb.name = 'thumbMesh';
thumb.position.set(0, h/2 + 20, 0);
thumb.rotation.z = Math.PI;
grp.add(thumb);
const ext = node.name.split('.').pop().toUpperCase().slice(0,4);
const eCvs = document.createElement('canvas'); eCvs.width=128; eCvs.height=64;
const eCtx = eCvs.getContext('2d');
eCtx.font = "bold 40px 'Segoe UI'";
eCtx.textAlign='center'; eCtx.textBaseline='middle';
eCtx.strokeStyle='#000'; eCtx.lineWidth=4; eCtx.strokeText(ext, 64,32);
eCtx.fillStyle='#999999'; eCtx.fillText(ext, 64,32);
const eTex = new THREE.CanvasTexture(eCvs);
const extSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: eTex, transparent:true }));
extSprite.scale.set(w/2, w/4, 1);
extSprite.position.set(0, h/2 + 20, 10);
extSprite.name = 'extSprite';
grp.add(extSprite);
const hit = new THREE.Mesh(new THREE.BoxGeometry(w,h,20), new THREE.MeshBasicMaterial({ visible: false }));
hit.position.set(0, h/2 + 20, 0);
grp.add(hit); hit.userData.node = node; state.visibleNodes.push(hit);
}
world.add(grp); node.obj = grp;
}
function createConnection(pNode, cNode, isBold) {
const path = new THREE.LineCurve3(pNode.obj.position, cNode.obj.position);
const tubGeo = new THREE.TubeGeometry(path, 1, isBold?6:2, 8, false);
const tubMat = new THREE.MeshBasicMaterial({ color: CONFIG.colLine, transparent: true, opacity: isBold?0.8:0.1, blending: THREE.AdditiveBlending });
const mesh = new THREE.Mesh(tubGeo, tubMat);
world.add(mesh); cNode.visualLine = mesh; cNode.isTube = true;
}
function rotateToTarget() {
if (!state.activeNode.children.length) return;
const target = state.activeNode.children[state.targetChildIdx];
const dest = (Math.PI / 2) - target.angle;
let delta = dest - state.currentRotation;
while (delta <= -Math.PI) delta += Math.PI*2; while (delta > Math.PI) delta -= Math.PI*2;
new TWEEN.Tween({r:state.currentRotation}).to({r:state.currentRotation+delta}, 500).easing(TWEEN.Easing.Cubic.Out).onUpdate(o=>state.currentRotation=o.r).start();
updateConnectionStyles();
updateInfoTexture(target);
}
function updateConnectionStyles() {
const children = state.activeNode.children;
const target = children[state.targetChildIdx];
children.forEach(c => {
if(c.visualLine) { world.remove(c.visualLine); if(c.visualLine.geometry) c.visualLine.geometry.dispose(); }
if(c.obj && state.activeNode.obj) createConnection(state.activeNode, c, (c === target));
});
}
async function diveInto(node) {
if (node.kind === 'file') { openViewer(node.handle); return; }
if (node.children.length === 0) {
const temp = await scanNode(node.handle, node.parent, node.depth);
node.children = temp.children;
}
triggerTransitionEffect();
new TWEEN.Tween(state).to({ focusPointZ: node.z }, 800).easing(TWEEN.Easing.Quartic.InOut).onComplete(() => {
state.isMoving = false; state.activeNode = node; state.targetChildIdx = 0; rebuildWorld(); updatePath();
}).start();
}
function moveUp() {
if (!state.activeNode.parent) return;
const parent = state.activeNode.parent;
const idx = parent.children.findIndex(c => c.name === state.activeNode.name);
triggerTransitionEffect();
new TWEEN.Tween(state).to({ focusPointZ: parent.z }, 800).easing(TWEEN.Easing.Quartic.InOut).onComplete(() => {
state.isMoving = false; state.activeNode = parent; state.targetChildIdx = idx >= 0 ? idx : 0; rebuildWorld(); updatePath();
}).start();
}
function triggerTransitionEffect() {
state.isMoving = true;
new TWEEN.Tween(streamGroup.children[0].material).to({ opacity: 0.8 }, 400).yoyo(true).repeat(1).start();
}
function updateCamera() {
const currentZ = state.focusPointZ;
const lookAtZ = currentZ - CONFIG.camLookAhead;
const children = state.activeNode && state.activeNode.children ? state.activeNode.children : [];
const r = children.length > 0 ? calculateRadius(children.length) : CONFIG.baseRadius;
let camY = r + CONFIG.camHeight;
let camZ = currentZ + CONFIG.camDist;
if (state.isMoving) { camY += 50; camZ -= 100; }
streamGroup.position.z = currentZ;
if (Math.abs(state.targetFocusZ) > 0.1) {
state.focusPointZ += state.targetFocusZ * 0.1;
state.targetFocusZ *= 0.9;
}
camera.position.set(0, camY, camZ);
camera.lookAt(0, r, lookAtZ);
}
function updateVisuals() {
world.rotation.z = state.currentRotation;
// Stream Lines Logic
const targetSpeed = state.isMoving ? CONFIG.flowSpeedFast : CONFIG.flowSpeedNormal;
state.currentFlowSpeed += (targetSpeed - state.currentFlowSpeed) * 0.1;
streamGroup.position.z -= state.currentFlowSpeed;
if(streamGroup.position.z < -20000) streamGroup.position.z += 20000;
if (state.centerMesh) {
state.centerMesh.position.z = state.activeNode ? state.activeNode.z : 0;
centerCursor.position.z = state.centerMesh.position.z;
state.centerMesh.rotation.z += 0.01;
}
let targetNode = null;
if (state.activeNode && state.activeNode.children.length > 0) {
targetNode = state.activeNode.children[state.targetChildIdx];
}
// --- Overlays Update ---
if (targetNode && targetNode.obj) {
const p = new THREE.Vector3();
targetNode.obj.getWorldPosition(p);
const dirToCam = new THREE.Vector3().subVectors(camera.position, p).normalize();
const popDist = 150;
const magPos = p.clone().add(dirToCam.multiplyScalar(popDist));
// Show MagMesh ONLY for Media (Image/Video)
const isMedia = targetNode.kind === 'file' && targetNode.name.match(/\.(jpg|png|webp|gif|bmp|mp4|webm)$/i);
if (isMedia) {
magMesh.visible = true;
magMesh.position.copy(magPos);
magMesh.lookAt(camera.position);
if (targetNode.thumbTex) {
magMesh.material.map = targetNode.thumbTex;
magMesh.material.color.setHex(0xffffff);
magMesh.material.opacity = 1;
magMesh.material.needsUpdate = true;
} else {
magMesh.material.map = null;
magMesh.material.color.setHex(0x333333);
magMesh.material.opacity = 0.5;
magMesh.material.needsUpdate = true;
}
} else {
magMesh.visible = false;
}
// Info Mesh (Visible only for Files)
if (targetNode.kind === 'file') {
infoMesh.visible = true;
lineMesh.visible = true;
const basePos = magMesh.visible ? magPos : p;
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
const infoPos = basePos.clone().add(right.multiplyScalar(450));
infoMesh.position.copy(infoPos);
infoMesh.lookAt(camera.position);
const down = new THREE.Vector3(0, -1, 0).applyQuaternion(camera.quaternion);
const infoBottom = infoPos.clone().add(down.multiplyScalar(100));
const pts = [infoBottom, p];
lineMesh.geometry.setFromPoints(pts);
} else {
infoMesh.visible = false;
lineMesh.visible = false;
}
// Yellow Cursor (Reticle) - Visible for ALL targets
reticleGroup.visible = true;
reticleGroup.position.copy(p);
// Billboard to camera
reticleGroup.lookAt(camera.position);
// Rotate Layers
reticleGroup.children.forEach(child => {
if(child.userData && child.userData.speed) {
child.rotation.z += child.userData.speed;
}
});
} else {
magMesh.visible = false;
infoMesh.visible = false;
lineMesh.visible = false;
reticleGroup.visible = false;
}
state.visibleNodes.forEach(obj => {
const node = obj.userData.node;
let tScale = 1.0; let tLift = 0.0;
const isTarget = (node === targetNode);
const isActive = (node === state.activeNode);
if (isActive) { tScale = 2.5; tLift = 120; }
else if (isTarget) {
if (node.kind === 'directory') { tScale = 2.0; tLift = 80; }
else { tScale = 1.0; tLift = 0; }
}
node.currentScale += (tScale - node.currentScale) * 0.1;
node.currentLift += (tLift - node.currentLift) * 0.1;
node.obj.scale.set(node.currentScale, node.currentScale, node.currentScale);
const childrenCount = (state.activeNode && state.activeNode.children) ? state.activeNode.children.length : 1;
const baseR = calculateRadius(childrenCount);
const r = baseR + node.currentLift;
node.obj.position.x = Math.cos(node.angle) * r;
node.obj.position.y = Math.sin(node.angle) * r;
node.obj.position.z = node.z;
node.obj.rotation.set(0,0,0);
node.obj.rotation.z = node.angle + Math.PI/2;
if (node.kind === 'file') {
const extSprite = node.obj.getObjectByName('extSprite');
if(extSprite) extSprite.visible = !isTarget;
} else {
if(node.obj.userData.rotMesh) {
node.obj.userData.rotMesh.rotation.x += 0.01;
node.obj.userData.rotMesh.rotation.y += 0.02;
}
}
if (node.visualLine && node.parent && node.parent.obj) {
const pPos = node.parent.obj.position;
const cPos = node.obj.position;
if(node.isTube) {
node.visualLine.geometry.dispose();
const path = new THREE.LineCurve3(pPos, cPos);
node.visualLine.geometry = new THREE.TubeGeometry(path, 1, 6, 8, false);
} else {
const pos = node.visualLine.geometry.attributes.position.array;
pos[0]=pPos.x; pos[1]=pPos.y; pos[2]=pPos.z;
pos[3]=cPos.x; pos[4]=cPos.y; pos[5]=cPos.z;
node.visualLine.geometry.attributes.position.needsUpdate = true;
}
}
});
}
function updateParticles() {
if (state.activeNode && state.activeNode.children.length > 0) {
const target = state.activeNode.children[state.targetChildIdx];
if (target && target.obj && state.activeNode.obj) {
const s = new THREE.Vector3(); state.activeNode.obj.getWorldPosition(s);
const e = new THREE.Vector3(); target.obj.getWorldPosition(e);
if (Math.random() < 0.4) spawnArrow(s, e);
}
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; p.progress += p.speed;
if (p.progress >= 1.0) { scene.remove(p.mesh); particles.splice(i, 1); }
else {
p.mesh.position.lerpVectors(p.start, p.end, p.progress);
if (p.progress > 0.8) p.mesh.material.opacity = (1.0 - p.progress) * 5;
}
}
}
function onKey(e) {
if(!state.activeNode) return;
switch(e.key) {
case 'ArrowLeft': e.preventDefault(); changeTarget(1); break;
case 'ArrowRight': e.preventDefault(); changeTarget(-1); break;
case 'ArrowUp': case 'Enter': e.preventDefault(); if(state.activeNode.children[state.targetChildIdx]) diveInto(state.activeNode.children[state.targetChildIdx]); break;
case 'ArrowDown': e.preventDefault(); moveUp(); break;
case 'Home': e.preventDefault(); if(state.rootNode) { state.activeNode=state.rootNode; state.targetChildIdx=0; state.focusPointZ=state.rootNode.z; rebuildWorld(); updatePath(); } break;
}
}
function changeTarget(dir) {
const len = state.activeNode.children.length; if(len === 0) return;
state.targetChildIdx = (state.targetChildIdx + dir + len) % len;
rotateToTarget();
}
function onMouseMove(e) {
state.mouse.x = (e.clientX/window.innerWidth)*2-1; state.mouse.y = -(e.clientY/window.innerHeight)*2+1;
if(state.isCamDragging) {
const dx = e.clientX - state.lastMouse.x; const dy = e.clientY - state.lastMouse.y;
state.targetFocusZ += dy * 8.0;
state.currentRotation += dx * 0.005;
state.targetRotation = state.currentRotation;
state.lastMouse.set(e.clientX, e.clientY); return;
}
if(state.dragging) {
const ghost = document.getElementById('drag-ghost'); ghost.style.display = 'block';
ghost.style.left = (e.clientX + 10) + 'px'; ghost.style.top = (e.clientY + 10) + 'px'; return;
}
state.raycaster.setFromCamera(state.mouse, camera);
const hits = state.raycaster.intersectObjects(state.visibleNodes);
document.body.style.cursor = hits.length > 0 ? 'pointer' : 'default';
}
function onMouseDown(e) {
if(e.button !== 0) return;
if(e.target.closest('#stock') || e.target.closest('#stock-tabs') || e.target.closest('.ctx-menu')) return;
state.raycaster.setFromCamera(state.mouse, camera);
const hits = state.raycaster.intersectObjects(state.visibleNodes);
if(hits.length > 0) {
const node = hits[0].object.userData.node;
const idx = state.activeNode.children.indexOf(node);
if(idx >= 0) { state.targetChildIdx = idx; rotateToTarget(); state.dragging = { node: node, startX: e.clientX, startY: e.clientY }; }
else if (node === state.activeNode.parent) moveUp();
} else {
state.isCamDragging = true;
state.lastMouse.set(e.clientX, e.clientY);
}
}
async function onMouseUp(e) {
state.isCamDragging = false;
if(state.dragging) {
const ghost = document.getElementById('drag-ghost'); ghost.style.display = 'none';
if(e.clientY > window.innerHeight - 160) await moveToStock(state.dragging.node.handle);
state.dragging = null;
}
}
function onMouseDoubleClick(e) {
state.raycaster.setFromCamera(state.mouse, camera);
const hits = state.raycaster.intersectObjects(state.visibleNodes);
if(hits.length > 0) diveInto(hits[0].object.userData.node);
}
function onContextMenu(e) {
e.preventDefault();
if(e.target.closest('.tab-btn')) { state.tabCtxIdx = parseInt(e.target.closest('.tab-btn').dataset.idx); const m = document.getElementById('ctx-tab'); m.style.display='block'; m.style.left=e.clientX+'px'; m.style.top=e.clientY+'px'; return; }
state.raycaster.setFromCamera(state.mouse, camera);
const hits = state.raycaster.intersectObjects(state.visibleNodes);
if(hits.length > 0) {
state.ctxTarget = hits[0].object.userData.node;
const idx = state.activeNode.children.indexOf(state.ctxTarget);
if(idx>=0) { state.targetChildIdx=idx; rotateToTarget(); }
const m = document.getElementById('ctx-menu'); m.style.display='block'; m.style.left=e.clientX+'px'; m.style.top=e.clientY+'px';
document.getElementById('cm-stock').style.display = state.ctxTarget.kind==='directory'?'block':'none';
} else document.querySelectorAll('.ctx-menu').forEach(m => m.style.display='none');
}
async function moveToStock(handle) {
if(state.activeStockIdx < 0) { alert("No active Stock Tab!"); return; }
const dest = state.stockList[state.activeStockIdx].handle;
if(handle.move) { try { await handle.move(dest); const t = await scanNode(state.activeNode.handle, state.activeNode.parent, state.activeNode.depth); state.activeNode.children = t.children; rebuildWorld(); renderStock(); } catch(e) { alert("Move failed: " + e.message); } }
}
async function moveFromStock(stockItemIdx) {
if(state.activeStockIdx < 0) return;
const currentStock = state.stockList[state.activeStockIdx];
let entries = []; for await (const e of currentStock.handle.values()) entries.push(e);
if(entries[stockItemIdx] && entries[stockItemIdx].move) { try { await entries[stockItemIdx].move(state.activeNode.handle); const t = await scanNode(state.activeNode.handle, state.activeNode.parent, state.activeNode.depth); state.activeNode.children = t.children; rebuildWorld(); renderStock(); } catch(e) { alert("Retrieve failed: " + e.message); } }
}
function addToStock(node){
if(state.stockList.find(s=>s.name===node.name)) return;
state.stockList.push({name:node.name, handle:node.handle}); state.activeStockIdx = state.stockList.length-1; renderStock(); document.getElementById('stock').classList.remove('closed');
}
async function renderStock(){
const t=document.getElementById('stock-tabs'); const d=document.getElementById('stock'); t.innerHTML=''; d.innerHTML='';
if(state.stockList.length===0){ d.classList.add('closed'); return; }
state.stockList.forEach((s,i)=>{ const b=document.createElement('div'); b.className='tab-btn'+(i===state.activeStockIdx?' active':''); b.innerText=s.name; b.dataset.idx=i; b.onclick=()=>{ if(state.activeStockIdx === i) { if(d.classList.contains('closed')) d.classList.remove('closed'); else d.classList.add('closed'); } else { state.activeStockIdx=i; renderStock(); document.getElementById('stock').classList.remove('closed'); } }; t.appendChild(b); });
if(state.activeStockIdx >= 0) { const cur=state.stockList[state.activeStockIdx]; try{ let idx = 0; for await(const e of cur.handle.values()){ const el=document.createElement('div'); el.className='stock-item'; el.draggable = true; const myIdx = idx; el.ondragstart = ev => { ev.dataTransfer.setData('stockIndex', myIdx); }; let iconHtml = `<div class="stock-thumb" style="font-size:30px; color:#555;">${e.kind==='directory'?'📁':'📄'}</div>`; if(e.kind === 'file' && e.name.match(/\.(jpg|png|webp|gif)$/i)) { e.getFile().then(f=>{ const u=URL.createObjectURL(f); el.querySelector('.stock-thumb').innerHTML=`<img src="${u}">`; }); } el.innerHTML=`${iconHtml}<div class="stock-name">${e.name}</div>`; d.appendChild(el); idx++; } } catch(e){ d.innerHTML='ERR'; } }
}
function bindMenu() {
const hide=()=>document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
document.getElementById('cm-open').onclick=()=>{if(state.ctxTarget)diveInto(state.ctxTarget);hide();};
document.getElementById('cm-copy').onclick=()=>{if(state.ctxTarget)state.clipboard=state.ctxTarget.handle;hide();};
document.getElementById('cm-paste').onclick=async()=>{if(state.clipboard&&state.activeNode){try{if(state.clipboard.kind==='file'){const f=await state.clipboard.getFile();const h=await state.activeNode.handle.getFileHandle(state.clipboard.name,{create:true});const w=await h.createWritable();await w.write(f);await w.close(); const t=await scanNode(state.activeNode.handle,state.activeNode.parent,state.activeNode.depth); state.activeNode.children=t.children; rebuildWorld();}}catch(e){alert(e);}}hide();};
document.getElementById('cm-rename').onclick=async()=>{if(state.ctxTarget&&state.ctxTarget.handle.move){const n=await showDialog("RENAME",state.ctxTarget.name);if(n){await state.ctxTarget.handle.move(n);state.ctxTarget.name=n;rebuildWorld();}}hide();};
document.getElementById('cm-new').onclick=async()=>{const n=await showDialog("NEW FOLDER","New Folder");if(n){await state.activeNode.handle.getDirectoryHandle(n,{create:true});const t=await scanNode(state.activeNode.handle,state.activeNode.parent,state.activeNode.depth);state.activeNode.children=t.children;rebuildWorld();}hide();};
document.getElementById('cm-del').onclick=async()=>{if(state.ctxTarget&&confirm("Delete?")){await state.activeNode.handle.removeEntry(state.ctxTarget.name,{recursive:true});const t=await scanNode(state.activeNode.handle,state.activeNode.parent,state.activeNode.depth);state.activeNode.children=t.children;rebuildWorld();}hide();};
document.getElementById('cm-stock').onclick=()=>{if(state.ctxTarget.kind==='directory')addToStock(state.ctxTarget);hide();};
document.getElementById('cm-tab-hide').onclick = () => { document.getElementById('stock').classList.add('closed'); hide(); };
document.getElementById('cm-tab-cancel').onclick = () => { if(state.tabCtxIdx >= 0) { state.stockList.splice(state.tabCtxIdx, 1); if(state.activeStockIdx >= state.stockList.length) state.activeStockIdx = state.stockList.length - 1; renderStock(); } hide(); };
}
let dRes=null; function showDialog(t,v){return new Promise(r=>{document.getElementById('dialog-overlay').style.display='flex';document.getElementById('dialog-title').innerText=t;document.getElementById('dialog-input').value=v;document.getElementById('dialog-input').focus();dRes=r;});}
function bindDialog(){document.getElementById('dialog-ok').onclick=()=>{document.getElementById('dialog-overlay').style.display='none';if(dRes)dRes(document.getElementById('dialog-input').value);};document.getElementById('dialog-cancel').onclick=()=>{document.getElementById('dialog-overlay').style.display='none';if(dRes)dRes(null);};}
async function openViewer(h){
const v=document.getElementById('viewer-overlay'),b=document.getElementById('viewer-body');
v.style.display='flex'; v.classList.remove('closing'); setTimeout(()=>v.classList.add('active'),10);
document.getElementById('viewer-title').innerText=h.name; b.innerHTML='LOADING...';
try{
const f=await h.getFile(),u=URL.createObjectURL(f); b.innerHTML='';
if(f.type.startsWith('image'))b.innerHTML=`<img src="${u}">`;
else if(f.type.startsWith('video'))b.innerHTML=`<video src="${u}" controls autoplay></video>`;
else if(f.type.startsWith('text')||h.name.match(/\.(txt|js|json|html|css|md)$/)){
const t=await f.text(); const ta=document.createElement('textarea');
ta.className='editor'; ta.id='editor-area'; ta.value=t; b.appendChild(ta);
// No save for now as simplified viewer
} else b.innerText="BINARY FILE";
}catch(e){console.error(e); b.innerText="ERROR: " + e.message;}
}
function closeViewer(){const v=document.getElementById('viewer-overlay'); v.classList.remove('active'); v.classList.add('closing'); setTimeout(()=>v.style.display='none',500);}
function bindViewer(){document.getElementById('viewer-close').onclick=closeViewer;}
function showLoad(b){document.getElementById('loading').style.display=b?'flex':'none';}
function updatePath(){let p=state.activeNode,s=p.name;while(p.parent){p=p.parent;s=p.name+' / '+s;}document.getElementById('path-display').innerText=s.toUpperCase();}
function onResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);composer.setSize(window.innerWidth,window.innerHeight);}
function animate() {
requestAnimationFrame(animate); TWEEN.update();
updateCamera(); updateVisuals(); updateParticles();
composer.render();
}
init();
</script>
</body>
</html>

コメント