SF映画っぽいエクスプローラ「Exp3D 1.3」
・ダウンロードされる方はこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Exp3D 1.3</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; }
/* --- Layout --- */
#app { display: flex; width: 100vw; height: 100vh; position: relative; }
/* 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%; position: absolute; top:0; left:0; }
/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud-canvas { width: 100%; height: 100%; display: block; }
#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 (Fixed) */
.ctx-menu {
position: fixed;
display: none; flex-direction: column;
background: var(--menu-bg); border: 1px solid var(--cyan);
box-shadow: 0 0 20px rgba(0,255,255,0.2); min-width: 200px;
z-index: 9999; 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; }
/* Shelf */
#shelf {
position: absolute; bottom: 0; left: 0; 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: fixed; 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; visibility: hidden;
perspective: 1000px; transition: opacity 0.4s;
}
#preview.active { opacity: 1; pointer-events: auto; visibility: visible; }
#p-box {
width: 90%; height: 90%; border: 1px solid var(--cyan); background: #000;
display: flex; flex-direction: column; box-shadow: 0 0 50px rgba(0,255,255,0.15);
transform: rotateY(90deg) scale(0.5);
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#preview.active #p-box { transform: rotateY(0deg) scale(1); }
#p-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; font-weight: bold; align-items: center;}
#p-body { flex: 1; padding: 0; overflow: hidden; position: relative; background: #050505; display: flex; justify-content: center; align-items: center;}
.p-btn {
background: transparent; border: 1px solid var(--cyan); color: var(--cyan);
padding: 5px 15px; margin-left: 10px; cursor: pointer; font-weight: bold; transition: 0.2s;
}
.p-btn:hover { background: var(--cyan); color: #000; }
.p-btn-save { border-color: #0f0; color: #0f0; }
.p-btn-save:hover { background: #0f0; color: #000; }
.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;
}
video, audio { max-width: 100%; max-height: 100%; outline: 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" class="p-btn" style="width:100%;">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="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 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>
</div>
<div id="preview">
<div id="p-box">
<div id="p-head">
<span id="p-title">FILENAME</span>
<div>
<button id="p-save" class="p-btn p-btn-save" style="display:none;">SAVE</button>
<button id="p-close" class="p-btn">CLOSE</button>
</div>
</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';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
// --- Config & Global State ---
const CONFIG = { colFolder: 0x00ffff, colFile: 0xff00ff, grid: 0x002233 };
const state = {
rootHandle: null, currentHandle: null, currentPath: [],
objects: [], shelfData: [], dragging: null, hovered: null, selected: null,
clipboard: null, isTransitioning: false
};
// --- Sound ---
const sfx = {
ctx: null,
init: () => { if (!sfx.ctx) sfx.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
play: (type) => {
if (!sfx.ctx) return;
const t = sfx.ctx.currentTime;
const osc = sfx.ctx.createOscillator();
const gain = sfx.ctx.createGain();
osc.connect(gain); gain.connect(sfx.ctx.destination);
if(type==='click'){ osc.frequency.setValueAtTime(600,t); osc.frequency.linearRampToValueAtTime(100,t+0.1); gain.gain.setValueAtTime(0.1,t); gain.gain.exponentialRampToValueAtTime(0.001,t+0.1); osc.start(t); osc.stop(t+0.1); }
else if(type==='hover'){ osc.frequency.setValueAtTime(400,t); osc.frequency.linearRampToValueAtTime(600,t+0.05); gain.gain.setValueAtTime(0.02,t); gain.gain.exponentialRampToValueAtTime(0.001,t+0.05); osc.start(t); osc.stop(t+0.05); }
else if(type==='open'){ osc.type='triangle'; osc.frequency.setValueAtTime(200,t); osc.frequency.linearRampToValueAtTime(600,t+0.2); gain.gain.setValueAtTime(0.1,t); gain.gain.exponentialRampToValueAtTime(0.001,t+0.2); osc.start(t); osc.stop(t+0.2); }
else if(type==='lock'){ osc.type='square'; osc.frequency.setValueAtTime(1000,t); gain.gain.setValueAtTime(0.05,t); gain.gain.exponentialRampToValueAtTime(0.001,t+0.1); osc.start(t); osc.stop(t+0.1); }
}
};
// --- Scene Init ---
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);
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);
// --- Reticle ---
const reticleGroup = new THREE.Group();
scene.add(reticleGroup);
reticleGroup.visible = false;
const rRing = new THREE.Mesh(new THREE.RingGeometry(1.2, 1.25, 32), new THREE.MeshBasicMaterial({color:0xffff00, side:THREE.DoubleSide}));
reticleGroup.add(rRing);
const bGeo = new THREE.BufferGeometry();
const bs=1.5, bl=0.4;
bGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array([
-bs,bs,0, -bs+bl,bs,0, -bs,bs,0, -bs,bs-bl,0,
bs,bs,0, bs-bl,bs,0, bs,bs,0, bs,bs-bl,0,
-bs,-bs,0, -bs+bl,-bs,0, -bs,-bs,0, -bs,-bs+bl,0,
bs,-bs,0, bs-bl,-bs,0, bs,-bs,0, bs,-bs+bl,0
]), 3));
reticleGroup.add(new THREE.LineSegments(bGeo, new THREE.LineBasicMaterial({color:0xffff00})));
// --- File System ---
document.getElementById('btn-mount').onclick = async () => {
sfx.init(); sfx.play('click');
try {
const h = await window.showDirectoryPicker();
state.rootHandle = h;
enterFolder(h, false);
updateTree(h);
} catch(e) { console.log(e); }
};
async function enterFolder(handle, animate=true) {
const load = async () => {
state.currentHandle = handle;
if(!state.currentPath.includes(handle)) state.currentPath.push(handle);
state.objects.forEach(o => filesGroup.remove(o));
state.objects = [];
deselect();
const entries = [];
for await (const e of handle.values()) entries.push(e);
layoutFiles(entries);
};
if(animate) animateTransition('in', load);
else await load();
}
async function goUp() {
if(state.currentPath.length>1) {
const p = state.currentPath[state.currentPath.length-2];
animateTransition('out', async () => {
state.currentPath.pop();
await enterFolder(p, false);
});
}
}
function animateTransition(dir, cb) {
if(state.isTransitioning) return;
state.isTransitioning = true;
sfx.play('open');
const startT = Date.now();
const startS = filesGroup.scale.x;
const targetS = dir==='in'?3.0:0.1;
function loop() {
const p = Math.min((Date.now()-startT)/400, 1);
const s = startS + (targetS - startS) * (p*p);
filesGroup.scale.set(s,s,s);
filesGroup.children.forEach(c => c.material.opacity = 0.9*(1-p));
if(p<1) requestAnimationFrame(loop);
else cb().then(() => {
filesGroup.scale.set(dir==='in'?0.1:3.0, dir==='in'?0.1:3.0, dir==='in'?0.1:3.0);
const startT2 = Date.now();
const startS2 = filesGroup.scale.x;
function loop2() {
const p2 = Math.min((Date.now()-startT2)/400, 1);
const s2 = startS2 + (1.0 - startS2) * (1-(1-p2)*(1-p2));
filesGroup.scale.set(s2,s2,s2);
filesGroup.children.forEach(c => c.material.opacity = 0.9*p2);
if(p2<1) requestAnimationFrame(loop2);
else state.isTransitioning=false;
}
loop2();
});
}
loop();
}
function layoutFiles(entries) {
const cols = Math.ceil(Math.sqrt(entries.length));
const spacing = 3, offset = (cols*3)/2;
entries.forEach((e,i) => {
createMonolith(e, (i%cols)*3-offset, Math.floor(i/cols)*3-offset);
});
}
function createMonolith(handle, x, z) {
const isF = handle.kind==='directory';
const col = isF ? CONFIG.colFolder : CONFIG.colFile;
const geo = new THREE.BoxGeometry(1.6, 2.2, 0.1);
const mat = new THREE.MeshPhysicalMaterial({
color:col, metalness:0.1, roughness:0.1, transmission:0.5, opacity:0.9, transparent:true, emissive:col, emissiveIntensity:0.2
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, 1.5, z);
mesh.userData = { handle, isFolder:isF, isRoot:true };
const edge = new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({color:0xffffff, transparent:true, opacity:0.6}));
mesh.add(edge);
const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=256;
const ctx = cvs.getContext('2d');
ctx.fillStyle=isF?'#00ffff':'#ff00ff'; ctx.font='80px Arial'; ctx.textAlign='center';
ctx.fillText(isF?'📂':'📄',128,100);
ctx.fillStyle='#fff'; ctx.font='bold 28px Courier New';
ctx.fillText(handle.name.substring(0,10),128,180);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(cvs)}));
sp.scale.set(1.5,1.5,1); sp.position.z=0.15;
mesh.add(sp);
filesGroup.add(mesh);
state.objects.push(mesh);
}
// --- Interaction ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function getRoot(o) { while(o){if(o.userData?.isRoot)return o; o=o.parent;} return null; }
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;
}
// IMPORTANT: Prevent clicks on the menu from triggering global pointer events
document.querySelectorAll('.ctx-menu').forEach(menu => {
menu.addEventListener('pointerdown', e => e.stopPropagation());
});
window.addEventListener('pointerdown', e => {
sfx.init();
if(e.button!==0) return;
// If click is not inside canvas (e.g. sidebar or shelf), don't deselect
if (!e.target.closest('canvas')) return;
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
hideMenus();
if(hits.length>0) {
const t = getRoot(hits[0].object);
if(t) {
selectObject(t);
state.dragging = { mesh:t, dist:camera.position.distanceTo(t.position) };
orbitControls.enabled = false;
t.material.opacity=0.5;
}
} else {
deselect();
}
});
window.addEventListener('pointermove', e => {
updateMouse(e);
if(state.dragging) {
raycaster.setFromCamera(mouse, camera);
const pl = new THREE.Plane(new THREE.Vector3(0,1,0), -1.5);
const pt = new THREE.Vector3();
raycaster.ray.intersectPlane(pl, pt);
if(pt) state.dragging.mesh.position.copy(pt);
updateReticle();
} else {
checkHover();
}
});
window.addEventListener('pointerup', () => {
if(state.dragging) {
const m = state.dragging.mesh;
m.material.opacity = 0.9;
if(m.position.clone().project(camera).y < -0.5) moveToShelf(m);
state.dragging=null;
orbitControls.enabled=true;
}
});
function selectObject(o) {
state.selected=o;
reticleGroup.visible=true;
sfx.play('lock');
updateReticle();
}
function deselect() {
state.selected=null;
reticleGroup.visible=false;
}
function updateReticle() {
if(state.selected) {
reticleGroup.position.copy(state.selected.position);
reticleGroup.lookAt(camera.position);
}
}
// --- Context Menu (Fixed Logic) ---
const ctxObj = document.getElementById('ctx-obj');
const ctxBg = document.getElementById('ctx-bg');
window.addEventListener('contextmenu', e => {
e.preventDefault();
// Don't show custom menu if clicking on sidebar/shelf
if(!e.target.closest('canvas')) return;
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
hideMenus();
sfx.play('click');
if(hits.length>0) {
const t = getRoot(hits[0].object);
if(t) {
selectObject(t);
showMenu(ctxObj, e.clientX, e.clientY);
}
} else {
deselect();
document.getElementById('cm-paste').classList.toggle('disabled', !state.clipboard);
showMenu(ctxBg, e.clientX, e.clientY);
}
});
function showMenu(el, x, y) {
el.style.display='flex';
// Adjust position to stay on screen
if(x+200 > window.innerWidth) x -= 200;
if(y+250 > window.innerHeight) y -= 250;
el.style.left=x+'px'; el.style.top=y+'px';
}
function hideMenus() { ctxObj.style.display='none'; ctxBg.style.display='none'; }
// --- Menu Actions (Fixed Handlers) ---
document.getElementById('cm-open').onclick = () => { if(state.selected) performOpen(state.selected); };
document.getElementById('cm-copy').onclick = () => {
if(state.selected) state.clipboard = state.selected.userData.handle;
hideMenus();
};
document.getElementById('cm-delete').onclick = async () => {
if(state.selected && confirm('Delete?')) {
try {
await state.currentHandle.removeEntry(state.selected.userData.handle.name, {recursive:true});
enterFolder(state.currentHandle, false);
} catch(err){ alert('Delete failed'); }
}
hideMenus();
};
document.getElementById('cm-rename').onclick = async () => {
if(state.selected){
const n = prompt("Rename:", state.selected.userData.handle.name);
if(n && state.selected.userData.handle.move) {
try {
await state.selected.userData.handle.move(n);
enterFolder(state.currentHandle, false);
} catch(err){ alert('Rename failed'); }
}
}
hideMenus();
};
document.getElementById('cm-prop').onclick = async () => {
if(state.selected){
const h = state.selected.userData.handle;
alert(`Name: ${h.name}\nKind: ${h.kind}`);
}
hideMenus();
};
// Bg Actions
document.getElementById('cm-new').onclick = async () => {
const n = prompt("Folder Name:");
if(n) {
try {
await state.currentHandle.getDirectoryHandle(n,{create:true});
enterFolder(state.currentHandle, false);
} catch(err){ alert('Create failed'); }
}
hideMenus();
};
document.getElementById('cm-paste').onclick = async () => {
if(state.clipboard && state.clipboard.kind === 'file'){
try {
const f = await state.clipboard.getFile();
const h = await state.currentHandle.getFileHandle('Copy_'+f.name, {create:true});
const w = await h.createWritable(); await w.write(f); await w.close();
enterFolder(state.currentHandle, false);
} catch(err) { alert('Paste failed'); }
} else if(state.clipboard) {
alert("Folder copy not supported by browser security.");
}
hideMenus();
};
document.getElementById('cm-up').onclick = () => { goUp(); hideMenus(); };
document.getElementById('cm-refresh').onclick = () => { enterFolder(state.currentHandle, false); hideMenus(); };
// --- Double Click ---
window.addEventListener('dblclick', e => {
updateMouse(e);
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
if(hits.length>0) {
const t = getRoot(hits[0].object);
if(t) performOpen(t);
}
});
function performOpen(mesh) {
hideMenus(); // Close any open menus
if(!mesh) return;
const h = mesh.userData.handle;
if(mesh.userData.isFolder) {
enterFolder(h);
updateTree(state.rootHandle);
} else {
sfx.play('open');
openPreview(h);
}
}
// --- Preview Logic ---
const pModal = document.getElementById('preview');
const pBody = document.getElementById('p-body');
const btnSave = document.getElementById('p-save');
let curFile = null;
let pRenderer, pScene, pFrame;
async function openPreview(handle) {
curFile=handle;
pModal.classList.add('active');
document.getElementById('p-title').innerText=handle.name;
pBody.innerHTML='<div style="color:#fff">Loading...</div>';
btnSave.style.display='none';
cleanupPreview();
try {
const f = await handle.getFile();
const n = f.name.toLowerCase();
const url = URL.createObjectURL(f);
if(n.match(/\.(fbx|glb|gltf|obj)$/)) init3D(url, n.split('.').pop());
else if(n.match(/\.(png|jpg|webp|gif)$/)) pBody.innerHTML=`<img src="${url}" style="max-width:100%; max-height:100%; object-fit:contain">`;
else if(n.match(/\.(mp4|webm|mkv)$/)) pBody.innerHTML=`<video src="${url}" controls autoplay></video>`;
else if(n.match(/\.(mp3|wav|ogg)$/)) pBody.innerHTML=`<div style="text-align:center"><div style="font-size:50px">🎵</div><audio src="${url}" controls></audio></div>`;
else {
// Text / Hex
try {
const t = await f.text();
if(t.includes('\0') && !n.endsWith('.txt')) throw new Error();
const ta = document.createElement('textarea');
ta.id='editor'; ta.className='hex-view'; ta.value=t;
pBody.innerHTML=''; pBody.appendChild(ta);
btnSave.style.display='inline-block';
} catch(e) {
const b = await f.arrayBuffer();
const v = new DataView(b);
let s = `BINARY (${f.size} B)\n\n`;
const l = Math.min(v.byteLength, 1024);
for(let i=0; i<l; i+=16) {
s+=i.toString(16).padStart(6,'0').toUpperCase()+' ';
for(let j=0; j<16; j++) {
if(i+j<l) s+=v.getUint8(i+j).toString(16).padStart(2,'0').toUpperCase()+' ';
} s+='\n';
}
const ta = document.createElement('textarea');
ta.className='hex-view'; ta.value=s; ta.readOnly=true;
pBody.innerHTML=''; pBody.appendChild(ta);
}
}
} catch(e){ pBody.innerHTML='Error'; }
}
function init3D(url, ext) {
pBody.innerHTML='';
const scn = new THREE.Scene(); scn.background=new THREE.Color(0x111111);
const cam = new THREE.PerspectiveCamera(50, pBody.clientWidth/pBody.clientHeight, 0.1, 1000);
cam.position.set(0,2,5);
const rnd = new THREE.WebGLRenderer({antialias:true});
rnd.setSize(pBody.clientWidth, pBody.clientHeight);
pBody.appendChild(rnd.domElement);
const ctr = new OrbitControls(cam, rnd.domElement);
scn.add(new THREE.AmbientLight(0xffffff, 0.5));
scn.add(new THREE.DirectionalLight(0xffffff, 2));
const load = (o) => {
if(ext==='gltf'||ext==='glb') o=o.scene;
const b = new THREE.Box3().setFromObject(o);
const s = 2 / b.getSize(new THREE.Vector3()).length();
o.scale.setScalar(s); o.position.y = -b.getSize(new THREE.Vector3()).y*s/2;
scn.add(o);
};
if(ext==='fbx') new FBXLoader().load(url, load);
else if(ext==='obj') new OBJLoader().load(url, load);
else new GLTFLoader().load(url, load);
function l(){ pFrame=requestAnimationFrame(l); rnd.render(scn,cam); }
l(); pRenderer=rnd; pScene=scn;
}
function cleanupPreview() {
if(pFrame) cancelAnimationFrame(pFrame);
if(pRenderer) { pRenderer.dispose(); pRenderer=null; }
}
document.getElementById('p-close').onclick=()=>{sfx.play('click'); pModal.classList.remove('active'); cleanupPreview();};
btnSave.onclick=async()=>{
sfx.play('lock');
const el=document.getElementById('editor');
if(curFile && el) {
try {
const w=await curFile.createWritable(); await w.write(el.value); await w.close();
el.style.background='#003300'; setTimeout(()=>el.style.background='#000',200);
} catch(e){alert("Error");}
}
};
// --- HUD (Fixed) ---
const hudC = document.getElementById('hud-canvas');
const hudX = hudC.getContext('2d');
const hudL = document.getElementById('hud-label');
function resizeHUD() { hudC.width=container.clientWidth; hudC.height=container.clientHeight; }
window.onresize=()=>{
camera.aspect=container.clientWidth/container.clientHeight; camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
composer.setSize(container.clientWidth, container.clientHeight);
resizeHUD();
}; resizeHUD();
function checkHover() {
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
if(hits.length>0) {
const t=getRoot(hits[0].object);
if(t) {
if(state.hovered!==t) sfx.play('hover');
state.hovered=t; document.body.style.cursor='pointer';
return;
}
}
state.hovered=null; document.body.style.cursor='default';
}
function drawHUD() {
hudX.clearRect(0,0, hudC.width, hudC.height);
if(state.selected && reticleGroup.visible) {
reticleGroup.children[0].rotation.z -= 0.05;
reticleGroup.children[1].scale.setScalar(1 + Math.sin(Date.now()*0.005)*0.1);
}
if(!state.hovered || state.dragging) { hudL.style.display='none'; return; }
const m = state.hovered;
const p = m.position.clone(); p.y+=1.2; p.project(camera);
const x = (p.x*0.5+0.5)*hudC.width, y = (p.y*-0.5+0.5)*hudC.height;
const c = m.userData.isFolder?'#00ffff':'#ff00ff';
hudX.beginPath(); hudX.strokeStyle=c; hudX.lineWidth=2;
hudX.moveTo(x,y); hudX.lineTo(x+20,y-20); hudX.lineTo(x+80,y-20); hudX.stroke();
hudX.beginPath(); hudX.fillStyle=c; hudX.arc(x,y,4,0,Math.PI*2); hudX.fill();
hudL.style.display='block'; hudL.style.left=(x+80)+'px'; hudL.style.top=(y-35)+'px';
hudL.innerText=m.userData.handle.name; hudL.style.color=c; hudL.style.borderColor=c;
}
// --- Shelf ---
const shelf=document.getElementById('shelf');
function moveToShelf(m) {
filesGroup.remove(m); state.objects=state.objects.filter(o=>o!==m);
state.shelfData.push(m.userData.handle); renderShelf(); deselect();
}
function renderShelf() {
shelf.innerHTML='';
state.shelfData.forEach((h,i)=>{
const d=document.createElement('div'); d.className='shelf-item'; d.draggable=true;
d.innerHTML=`<div class="shelf-img">${h.kind==='directory'?'📂':'📄'}</div><div class="shelf-text">${h.name}</div>`;
d.ondragstart=e=>e.dataTransfer.setData('i',i);
shelf.appendChild(d);
});
}
document.getElementById('shelf-tab').onclick=function(){shelf.classList.toggle('closed'); this.innerText=shelf.classList.contains('closed')?'▲ DOCK':'▼ DOCK';};
container.ondragover=e=>e.preventDefault();
container.ondrop=e=>{
e.preventDefault(); const i=e.dataTransfer.getData('i');
if(i!==''){
const h=state.shelfData[i]; state.shelfData.splice(i,1); renderShelf();
const v=new THREE.Vector3((e.clientX/container.clientWidth)*2-1,-(e.clientY/container.clientHeight)*2+1,0.5).unproject(camera).sub(camera.position).normalize();
const pos=camera.position.clone().add(v.multiplyScalar(-camera.position.z/v.z));
createMonolith(h, pos.x, pos.z);
}
};
// --- Tree ---
const treeC=document.getElementById('tree-container');
async function updateTree(r) { treeC.innerHTML=''; treeC.appendChild(mkNode(r,0)); for await(const e of r.values()) if(e.kind==='directory') treeC.appendChild(mkNode(e,1)); }
function mkNode(h,d) {
const e=document.createElement('div'); e.className='tree-node'; e.style.paddingLeft=(d*15)+'px';
e.innerHTML=`<div class="icon-3d icon-${h.kind}"></div><div>${h.name}</div>`;
e.onclick=()=>enterFolder(h);
return e;
}
// --- Loop ---
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
drawHUD();
if(state.selected) updateReticle();
composer.render();
}
animate();
</script>
</body>
</html>

コメント