SF映画っぽいエクスプローラ「Exp3D 1.1」
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Exp3D 1.1</title>
<style>
:root {
--cyan: #00ffff;
--magenta: #ff00ff;
--bg: #050508;
--panel: rgba(10, 15, 20, 0.95);
--menu-bg: rgba(15, 20, 25, 0.98);
}
body { margin: 0; overflow: hidden; background-color: var(--bg); font-family: 'Segoe UI', monospace; user-select: none; color: #fff; }
/* --- UI Layout --- */
#app { display: flex; width: 100vw; height: 100vh; }
/* Sidebar */
#sidebar {
width: 280px; background: var(--panel); border-right: 1px solid var(--cyan);
display: flex; flex-direction: column; z-index: 20; backdrop-filter: blur(10px);
}
#sb-header {
padding: 15px; color: var(--cyan); font-weight: bold; letter-spacing: 2px;
border-bottom: 1px solid rgba(0,255,255,0.3); background: linear-gradient(90deg, rgba(0,255,255,0.1), transparent);
}
#tree-container { flex: 1; overflow-y: auto; padding: 10px; }
.tree-node {
display: flex; align-items: center; padding: 4px 8px; cursor: pointer; color: #888;
font-size: 13px; transition: 0.2s; border-left: 2px solid transparent;
}
.tree-node:hover { background: rgba(0,255,255,0.1); color: #fff; }
.icon-3d {
width: 12px; height: 14px; margin-right: 8px; position: relative;
transform-style: preserve-3d; transform: rotateX(-10deg) rotateY(25deg);
background: rgba(255,255,255,0.1); border: 1px solid currentColor; flex-shrink: 0;
}
.icon-folder { color: var(--cyan); }
.icon-file { color: var(--magenta); border-radius: 2px; }
/* Main View */
#main { flex: 1; position: relative; overflow: hidden; }
#canvas-wrap { width: 100%; height: 100%; }
/* HUD & Callout */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud-label {
position: absolute; display: none; color: var(--cyan); font-family: 'Courier New'; font-weight: bold;
font-size: 18px; text-shadow: 0 0 5px var(--cyan); white-space: nowrap; pointer-events: none;
background: rgba(0,0,0,0.8); padding: 4px 8px; border: 1px solid var(--cyan);
}
/* Context Menu */
.ctx-menu {
position: absolute; display: none; flex-direction: column;
background: var(--menu-bg); border: 1px solid var(--cyan);
box-shadow: 0 0 15px rgba(0,255,255,0.2); min-width: 180px;
z-index: 200; pointer-events: auto; padding: 5px 0;
}
.ctx-item {
padding: 8px 15px; cursor: pointer; color: #ccc; font-size: 13px;
display: flex; justify-content: space-between; transition: 0.2s;
}
.ctx-item:hover { background: var(--cyan); color: #000; }
.ctx-item.disabled { color: #555; pointer-events: none; }
.ctx-sep { height: 1px; background: #333; margin: 4px 0; }
.shortcut { font-size: 10px; opacity: 0.7; }
/* Bottom Shelf */
#shelf {
position: absolute; bottom: 0; left: 280px; right: 0; height: 150px;
background: linear-gradient(to top, rgba(0,20,30,0.98), transparent);
border-top: 2px solid var(--magenta); display: flex; align-items: center; justify-content: flex-start;
padding: 0 20px; gap: 15px; z-index: 30; transition: bottom 0.3s;
}
#shelf.closed { bottom: -150px; }
#shelf-tab {
position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%);
background: var(--magenta); color: #000; padding: 2px 20px; font-weight: bold;
cursor: pointer; clip-path: polygon(10% 0, 90% 0, 100% 100%, 0% 100%); z-index: 31; transition: bottom 0.3s;
}
#shelf.closed + #shelf-tab { bottom: 0; }
.shelf-item {
width: 80px; height: 100px; background: rgba(0,0,0,0.6); border: 1px solid var(--magenta);
display: flex; flex-direction: column; align-items: center; justify-content: center;
cursor: grab; color: var(--magenta); position: relative; flex-shrink: 0;
}
.shelf-img { font-size: 30px; margin-bottom: 5px; }
.shelf-text { font-size: 10px; text-align: center; width: 90%; overflow: hidden; text-overflow: ellipsis; }
/* Preview Modal */
#preview {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9);
display: flex; justify-content: center; align-items: center; z-index: 300;
opacity: 0; pointer-events: none; transition: 0.3s; visibility: hidden;
}
#preview.active { opacity: 1; pointer-events: auto; visibility: visible; }
#p-box { width: 85%; height: 85%; border: 1px solid var(--cyan); background: #000; display: flex; flex-direction: column; box-shadow: 0 0 30px rgba(0,255,255,0.1); }
#p-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; font-weight: bold;}
#p-body { flex: 1; padding: 0; overflow: hidden; position: relative; background: #050505; }
.hex-view {
width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000;
font-family: 'Courier New', monospace; white-space: pre; overflow: auto; border: none; outline: none; resize: none;
}
.scanlines {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;
background: linear-gradient(to bottom, rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%);
background-size: 100% 4px; z-index: 9999; opacity: 0.3;
}
</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="app">
<div id="sidebar">
<div id="sb-header">SYSTEM // NAV</div>
<div style="padding:10px;">
<button id="btn-mount" style="width:100%; background:transparent; border:1px solid var(--cyan); color:var(--cyan); padding:8px; cursor:pointer;">MOUNT DRIVE</button>
</div>
<div id="tree-container"></div>
</div>
<div id="main">
<div id="canvas-wrap"></div>
<div id="hud">
<canvas id="hud-canvas"></canvas>
<div id="hud-label">FILENAME</div>
</div>
<div id="shelf">
<div style="color:var(--magenta); font-size:12px; opacity:0.6;">[ DRAG FILES & FOLDERS HERE ]</div>
</div>
<div id="shelf-tab">▼ DOCK</div>
<div id="ctx-obj" class="ctx-menu">
<div class="ctx-item" id="cm-open">Open <span class="shortcut">DblClk</span></div>
<div class="ctx-sep"></div>
<div class="ctx-item" id="cm-copy">Copy <span class="shortcut">Ctrl+C</span></div>
<div class="ctx-item" id="cm-rename">Rename</div>
<div class="ctx-item" id="cm-delete" style="color:#ff5555">Delete</div>
<div class="ctx-sep"></div>
<div class="ctx-item" id="cm-prop">Properties</div>
</div>
<div id="ctx-bg" class="ctx-menu">
<div class="ctx-item" id="cm-new">New Folder</div>
<div class="ctx-item" id="cm-paste">Paste <span class="shortcut">Ctrl+V</span></div>
<div class="ctx-sep"></div>
<div class="ctx-item" id="cm-up">Up Directory</div>
<div class="ctx-item" id="cm-refresh">Refresh Scan</div>
</div>
</div>
</div>
<div id="preview">
<div id="p-box">
<div id="p-head"><span id="p-title"></span><span id="p-close" style="cursor:pointer;">[CLOSE]</span></div>
<div id="p-body"></div>
</div>
</div>
<div class="scanlines"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
// --- Config ---
const CONFIG = {
colFolder: 0x00ffff,
colFile: 0xff00ff,
grid: 0x002233
};
const state = {
rootHandle: null,
currentHandle: null,
currentPath: [],
objects: [],
shelfData: [],
dragging: null,
hovered: null,
clipboard: null, // { handle, type: 'copy' }
shelfOpen: true
};
// --- Scene Setup ---
const container = document.getElementById('canvas-wrap');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050508, 0.02);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 8, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0x404040, 5));
const dl = new THREE.DirectionalLight(0xffffff, 3);
dl.position.set(5, 10, 5);
scene.add(dl);
scene.add(new THREE.GridHelper(100, 100, CONFIG.grid, 0x001122));
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(container.clientWidth, container.clientHeight), 1.5, 0.4, 0.85);
bloom.threshold = 0.1; bloom.strength = 1.2; bloom.radius = 0.5;
composer.addPass(bloom);
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.maxPolarAngle = Math.PI / 2.1;
const filesGroup = new THREE.Group();
scene.add(filesGroup);
// --- File System ---
document.getElementById('btn-mount').onclick = async () => {
try {
const h = await window.showDirectoryPicker();
state.rootHandle = h;
enterFolder(h);
updateTree(h);
} catch(e) { console.log(e); }
};
async function enterFolder(handle) {
state.currentHandle = handle;
if(!state.currentPath.includes(handle)) state.currentPath.push(handle);
state.objects.forEach(o => filesGroup.remove(o));
state.objects = [];
const entries = [];
for await (const e of handle.values()) entries.push(e);
layoutFiles(entries);
}
function layoutFiles(entries) {
const cols = Math.ceil(Math.sqrt(entries.length));
const spacing = 3;
const offset = (cols * spacing) / 2;
entries.forEach((e, i) => {
const x = (i % cols) * spacing - offset;
const z = Math.floor(i / cols) * spacing - offset;
createMonolith(e, x, z);
});
}
function createMonolith(handle, x, z) {
const isFolder = handle.kind === 'directory';
const color = isFolder ? CONFIG.colFolder : CONFIG.colFile;
const geo = new THREE.BoxGeometry(1.6, 2.2, 0.1);
const mat = new THREE.MeshPhysicalMaterial({
color: color, metalness: 0.1, roughness: 0.1, transmission: 0.5, opacity: 0.9, transparent: true,
emissive: color, emissiveIntensity: 0.2
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, 1.5, z);
mesh.userData = { handle, isFolder, isRoot: true };
const edges = new THREE.EdgesGeometry(geo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.6 }));
mesh.add(line);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 256; canvas.height = 256;
ctx.fillStyle = isFolder ? '#00ffff' : '#ff00ff';
ctx.font = '80px Arial';
ctx.textAlign = 'center';
ctx.fillText(isFolder ? '📂' : '📄', 128, 100);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 28px Courier New';
ctx.fillText(handle.name.substring(0,10), 128, 180);
if(handle.name.length > 10) ctx.fillText(handle.name.substring(10,20)+'...', 128, 220);
const tex = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex }));
sprite.scale.set(1.5, 1.5, 1);
sprite.position.z = 0.15;
mesh.add(sprite);
filesGroup.add(mesh);
state.objects.push(mesh);
}
// --- Interaction Logic (Drag & Click) ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObj = null;
const onPointerDown = (e) => {
if (e.button !== 0) return; // Left click only for drag
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(state.objects, true);
if (intersects.length > 0) {
let target = getRootObject(intersects[0].object);
if (target) {
state.dragging = {
mesh: target,
dist: camera.position.distanceTo(target.position),
};
orbitControls.enabled = false;
target.material.opacity = 0.5;
}
}
hideContextMenus();
};
const onPointerMove = (e) => {
updateMouse(e);
if (state.dragging) {
raycaster.setFromCamera(mouse, camera);
const ray = raycaster.ray;
// Move on a virtual plane
const targetY = 1.5;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -targetY);
const target = new THREE.Vector3();
ray.intersectPlane(plane, target);
if(target) state.dragging.mesh.position.copy(target);
} else {
checkHover();
}
};
const onPointerUp = () => {
if (state.dragging) {
const mesh = state.dragging.mesh;
mesh.material.opacity = 0.9;
// Drop to Shelf Check
const screenPos = mesh.position.clone().project(camera);
if (screenPos.y < -0.5) {
moveToShelf(mesh);
}
state.dragging = null;
orbitControls.enabled = true;
}
};
function getRootObject(obj) {
let target = obj;
while (target) {
if (target.userData && target.userData.isRoot) return target;
target = target.parent;
}
return null;
}
window.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
function updateMouse(e) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
}
// --- Context Menu & Logic ---
const ctxObj = document.getElementById('ctx-obj');
const ctxBg = document.getElementById('ctx-bg');
window.addEventListener('contextmenu', (e) => {
e.preventDefault();
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(state.objects, true);
hideContextMenus();
if (intersects.length > 0) {
selectedObj = getRootObject(intersects[0].object);
showMenu(ctxObj, e.clientX, e.clientY);
} else {
selectedObj = null;
// Update Paste state
document.getElementById('cm-paste').classList.toggle('disabled', !state.clipboard);
showMenu(ctxBg, e.clientX, e.clientY);
}
});
function showMenu(el, x, y) {
el.style.display = 'flex';
if(x + 180 > window.innerWidth) x -= 180;
if(y + 200 > window.innerHeight) y -= 200;
el.style.left = x + 'px';
el.style.top = y + 'px';
}
function hideContextMenus() {
ctxObj.style.display = 'none';
ctxBg.style.display = 'none';
}
// Context Menu Actions
document.getElementById('cm-open').onclick = () => performOpen(selectedObj);
document.getElementById('cm-copy').onclick = () => {
if(selectedObj) state.clipboard = selectedObj.userData.handle;
hideContextMenus();
};
document.getElementById('cm-delete').onclick = async () => {
if(selectedObj && confirm('Delete ' + selectedObj.userData.handle.name + '?')) {
await state.currentHandle.removeEntry(selectedObj.userData.handle.name, { recursive: true });
enterFolder(state.currentHandle);
}
hideContextMenus();
};
document.getElementById('cm-rename').onclick = async () => {
if(selectedObj) {
const newName = prompt("Rename:", selectedObj.userData.handle.name);
if(newName && selectedObj.userData.handle.move) {
await selectedObj.userData.handle.move(newName);
enterFolder(state.currentHandle);
}
}
hideContextMenus();
};
document.getElementById('cm-prop').onclick = async () => {
if(selectedObj) {
const h = selectedObj.userData.handle;
let info = `Name: ${h.name}\nKind: ${h.kind}`;
if(h.kind === 'file') {
const f = await h.getFile();
info += `\nSize: ${f.size} bytes\nType: ${f.type}\nLast Mod: ${new Date(f.lastModified).toLocaleString()}`;
}
alert(info);
}
hideContextMenus();
};
document.getElementById('cm-new').onclick = async () => {
const name = prompt("Folder Name:", "New Folder");
if(name) {
await state.currentHandle.getDirectoryHandle(name, { create: true });
enterFolder(state.currentHandle);
}
hideContextMenus();
};
document.getElementById('cm-paste').onclick = async () => {
if(state.clipboard) {
const src = state.clipboard;
if(src.kind === 'file') {
const f = await src.getFile();
const newHandle = await state.currentHandle.getFileHandle('Copy_' + src.name, { create: true });
const w = await newHandle.createWritable();
await w.write(f);
await w.close();
enterFolder(state.currentHandle);
} else {
alert("Folder copy not implemented in browser sandbox.");
}
}
hideContextMenus();
};
document.getElementById('cm-up').onclick = () => {
if(state.currentPath.length > 1) {
state.currentPath.pop();
enterFolder(state.currentPath[state.currentPath.length-1]);
}
hideContextMenus();
};
document.getElementById('cm-refresh').onclick = () => {
if(state.currentHandle) enterFolder(state.currentHandle);
hideContextMenus();
};
// --- Double Click ---
window.addEventListener('dblclick', (e) => {
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(state.objects, true);
if(intersects.length > 0) {
const root = getRootObject(intersects[0].object);
if(root) performOpen(root);
}
});
function performOpen(mesh) {
if(!mesh) return;
const h = mesh.userData.handle;
if(mesh.userData.isFolder) {
enterFolder(h);
updateTree(state.rootHandle);
} else {
openPreview(h);
}
hideContextMenus();
}
// --- HUD Logic ---
const hudCanvas = document.getElementById('hud-canvas');
const hudCtx = hudCanvas.getContext('2d');
const hudLabel = document.getElementById('hud-label');
function resizeHUD() {
hudCanvas.width = container.clientWidth;
hudCanvas.height = container.clientHeight;
}
window.addEventListener('resize', resizeHUD);
resizeHUD();
function checkHover() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(state.objects, true);
if (intersects.length > 0) {
let target = getRootObject(intersects[0].object);
if(target) {
state.hovered = target;
document.body.style.cursor = 'pointer';
return;
}
}
state.hovered = null;
document.body.style.cursor = 'default';
}
function drawHUD() {
hudCtx.clearRect(0,0, hudCanvas.width, hudCanvas.height);
if (!state.hovered || state.dragging) {
hudLabel.style.display = 'none';
return;
}
const mesh = state.hovered;
const pos = mesh.position.clone();
pos.y += 1.2;
pos.project(camera);
const x = (pos.x * 0.5 + 0.5) * hudCanvas.width;
const y = (pos.y * -0.5 + 0.5) * hudCanvas.height;
const color = mesh.userData.isFolder ? '#00ffff' : '#ff00ff';
const tx = x + 60;
const ty = y - 60;
hudCtx.beginPath();
hudCtx.strokeStyle = color;
hudCtx.lineWidth = 2;
hudCtx.moveTo(x, y);
hudCtx.lineTo(x + 20, y - 20);
hudCtx.lineTo(tx, ty);
hudCtx.stroke();
hudCtx.beginPath(); hudCtx.arc(x,y, 4, 0, Math.PI*2); hudCtx.fillStyle = color; hudCtx.fill();
hudLabel.style.display = 'block';
hudLabel.style.left = tx + 'px';
hudLabel.style.top = (ty - 15) + 'px';
hudLabel.style.borderColor = color;
hudLabel.style.color = color;
hudLabel.innerText = mesh.userData.handle.name.toUpperCase();
}
// --- File Preview Logic (Text/Image/Binary) ---
const pModal = document.getElementById('preview');
const pBody = document.getElementById('p-body');
async function openPreview(handle) {
pModal.classList.add('active');
document.getElementById('p-title').innerText = handle.name;
pBody.innerHTML = '<div style="padding:20px; color:#fff;">Loading...</div>';
try {
const file = await handle.getFile();
const name = file.name.toLowerCase();
if(name.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/)) {
// Image
pBody.innerHTML = '';
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.objectFit = 'contain';
pBody.appendChild(img);
} else {
// Try Text
try {
const text = await file.text();
// Simple binary check: look for null bytes
if (text.includes('\0') && !name.endsWith('.txt')) {
throw new Error("Binary content detected");
}
const ta = document.createElement('textarea');
ta.className = 'hex-view';
ta.value = text;
pBody.innerHTML = '';
pBody.appendChild(ta);
} catch (err) {
// Fallback to Binary (Hex)
const buf = await file.arrayBuffer();
const view = new DataView(buf);
let hexStr = `BINARY VIEW (${file.size} bytes)\n\n`;
const limit = Math.min(view.byteLength, 2048); // Show first 2KB
for(let i=0; i<limit; i+=16) {
hexStr += i.toString(16).padStart(6,'0').toUpperCase() + ' ';
let chars = '';
for(let j=0; j<16; j++) {
if(i+j < limit) {
const b = view.getUint8(i+j);
hexStr += b.toString(16).padStart(2,'0').toUpperCase() + ' ';
chars += (b >= 32 && b <= 126) ? String.fromCharCode(b) : '.';
} else {
hexStr += ' ';
}
}
hexStr += ' ' + chars + '\n';
}
if(file.size > limit) hexStr += '\n... (Truncated)';
const ta = document.createElement('textarea');
ta.className = 'hex-view';
ta.value = hexStr;
pBody.innerHTML = '';
pBody.appendChild(ta);
}
}
} catch(e) {
pBody.innerHTML = '<div style="padding:20px; color:red;">Error reading file: ' + e.message + '</div>';
}
}
document.getElementById('p-close').onclick = () => pModal.classList.remove('active');
// --- Shelf Logic ---
const shelf = document.getElementById('shelf');
function moveToShelf(mesh) {
filesGroup.remove(mesh);
state.objects = state.objects.filter(o => o !== mesh);
state.shelfData.push(mesh.userData.handle);
renderShelf();
}
function renderShelf() {
shelf.innerHTML = '';
if (state.shelfData.length === 0) shelf.innerHTML = '<div style="color:var(--magenta); font-size:12px; opacity:0.6;">[ DRAG FILES & FOLDERS HERE ]</div>';
state.shelfData.forEach((h, i) => {
const el = document.createElement('div');
el.className = 'shelf-item';
el.draggable = true;
el.innerHTML = `<div class="shelf-img">${h.kind==='directory'?'📂':'📄'}</div><div class="shelf-text">${h.name}</div>`;
el.addEventListener('dragstart', (e) => e.dataTransfer.setData('idx', i));
shelf.appendChild(el);
});
}
container.addEventListener('dragover', e => e.preventDefault());
container.addEventListener('drop', e => {
e.preventDefault();
const idx = e.dataTransfer.getData('idx');
if(idx !== '') restoreFromShelf(parseInt(idx), e.clientX, e.clientY);
});
function restoreFromShelf(idx, cx, cy) {
const h = state.shelfData[idx];
state.shelfData.splice(idx, 1);
renderShelf();
const vec = new THREE.Vector3((cx/container.clientWidth)*2-1, -(cy/container.clientHeight)*2+1, 0.5);
vec.unproject(camera);
vec.sub(camera.position).normalize();
const dist = (0 - camera.position.z) / vec.z;
const pos = camera.position.clone().add(vec.multiplyScalar(dist));
createMonolith(h, pos.x, pos.z);
}
const tab = document.getElementById('shelf-tab');
tab.onclick = () => {
shelf.classList.toggle('closed');
tab.innerText = shelf.classList.contains('closed') ? '▲ DOCK' : '▼ DOCK';
};
// --- Tree View ---
const treeCont = document.getElementById('tree-container');
async function updateTree(root) {
treeCont.innerHTML = '';
treeCont.appendChild(createNode(root, 0));
for await (const e of root.values()) {
if(e.kind === 'directory') treeCont.appendChild(createNode(e, 1));
}
}
function createNode(h, depth) {
const div = document.createElement('div');
div.className = 'tree-node';
div.onclick = () => { if(h.kind==='directory') enterFolder(h); };
for(let i=0; i<depth; i++) div.appendChild(document.createElement('div')).className='indent';
div.innerHTML += `<div class="icon-3d icon-${h.kind}"></div><div>${h.name}</div>`;
return div;
}
// --- Loop ---
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
drawHUD();
composer.render();
}
animate();
window.onresize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
composer.setSize(container.clientWidth, container.clientHeight);
resizeHUD();
};
</script>
</body>
</html>

コメント