SF映画っぽいエクスプローラ「Exp3D 1.2」
・ダウンロードされる方はこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Exp3D 1.2</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 */
#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-sep { height: 1px; background: #333; margin: 4px 0; }
.shortcut { font-size: 10px; opacity: 0.7; }
/* 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 with Rotation Animation */
#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; visibility: hidden;
perspective: 1000px; /* 3D perspective */
transition: opacity 0.4s;
}
#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);
/* Initial State: Rotated and Scaled down */
transform: rotateY(90deg) scale(0.5);
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#preview.active #p-box {
/* Active State: Flat and Full Size */
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; }
.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;
}
.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="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">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';
// --- Sound System (Web Audio API) ---
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 === 'hover') {
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.05);
gain.gain.setValueAtTime(0.05, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
osc.start(t); osc.stop(t + 0.05);
} else if (type === 'click') {
osc.type = 'square';
osc.frequency.setValueAtTime(600, t);
osc.frequency.exponentialRampToValueAtTime(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 === 'open') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, t);
osc.frequency.linearRampToValueAtTime(800, t + 0.3);
gain.gain.setValueAtTime(0.1, t);
gain.gain.linearRampToValueAtTime(0.001, t + 0.3);
osc.start(t); osc.stop(t + 0.3);
} else if (type === 'back') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(800, t);
osc.frequency.linearRampToValueAtTime(200, t + 0.3);
gain.gain.setValueAtTime(0.1, t);
gain.gain.linearRampToValueAtTime(0.001, t + 0.3);
osc.start(t); osc.stop(t + 0.3);
} else if (type === 'save') {
osc.type = 'sine';
osc.frequency.setValueAtTime(440, t);
osc.frequency.setValueAtTime(880, t + 0.1);
gain.gain.setValueAtTime(0.1, t);
gain.gain.setValueAtTime(0.1, t + 0.1);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.start(t); osc.stop(t + 0.4);
} else if (type === 'error') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, t);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.start(t); osc.stop(t + 0.2);
}
}
};
// --- Config ---
const CONFIG = {
colFolder: 0x00ffff,
colFile: 0xff00ff,
grid: 0x002233
};
const state = {
rootHandle: null,
currentHandle: null,
currentPath: [],
objects: [],
shelfData: [],
dragging: null,
hovered: null,
clipboard: null,
shelfOpen: true,
isTransitioning: false
};
// --- 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;
// Main group for scaling animations
const filesGroup = new THREE.Group();
scene.add(filesGroup);
// --- Transition Logic (Zoom Effect) ---
function animateTransition(direction, callback) {
if (state.isTransitioning) return;
state.isTransitioning = true;
// direction: 'in' (enter folder) or 'out' (go back)
const duration = 500;
const startScale = filesGroup.scale.x;
const targetScale = direction === 'in' ? 3.0 : 0.1;
const startOpacity = 1.0;
const startTime = Date.now();
// Sfx
sfx.play(direction === 'in' ? 'open' : 'back');
function loopOut() {
const elapsed = Date.now() - startTime;
const p = Math.min(elapsed / duration, 1);
// Ease In Quad
const ease = p * p;
// Scale up/down and Fade out
const s = startScale + (targetScale - startScale) * ease;
filesGroup.scale.set(s, s, s);
// Opacity needs to affect all children materials
state.objects.forEach(obj => {
obj.material.opacity = 0.9 * (1 - p);
obj.children.forEach(c => {
if(c.material) c.material.opacity = (c.userData.baseOpacity || 0.6) * (1-p);
});
});
if (p < 1) {
requestAnimationFrame(loopOut);
} else {
// Phase 1 Done: Swap content
callback().then(() => {
// Reset for Phase 2 (Fade In)
// If we went IN, we start small and grow to 1.
// If we went OUT, we start big and shrink to 1.
filesGroup.scale.set(direction==='in'?0.1:3.0, direction==='in'?0.1:3.0, direction==='in'?0.1:3.0);
animateIn();
});
}
}
function animateIn() {
const startTimeIn = Date.now();
const startS = filesGroup.scale.x;
function loopIn() {
const elapsed = Date.now() - startTimeIn;
const p = Math.min(elapsed / duration, 1);
const ease = 1 - (1 - p) * (1 - p); // Ease Out
const s = startS + (1.0 - startS) * ease;
filesGroup.scale.set(s, s, s);
state.objects.forEach(obj => {
obj.material.opacity = 0.9 * p;
obj.children.forEach(c => {
if(c.material) c.material.opacity = (c.userData.baseOpacity || 0.6) * p;
});
});
if (p < 1) {
requestAnimationFrame(loopIn);
} else {
state.isTransitioning = false;
// Normalize
state.objects.forEach(obj => obj.material.opacity = 0.9);
}
}
loopIn();
}
loopOut();
}
// --- File System ---
document.getElementById('btn-mount').onclick = async () => {
sfx.init(); // Init Audio
sfx.play('click');
try {
const h = await window.showDirectoryPicker();
state.rootHandle = h;
enterFolder(h, false); // No animation for initial load
updateTree(h);
} catch(e) { console.log(e); }
};
async function enterFolder(handle, animate = true) {
const loadLogic = async () => {
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);
};
if (animate) {
animateTransition('in', loadLogic);
} else {
await loadLogic();
}
}
async function goUp() {
if(state.currentPath.length > 1) {
const parent = state.currentPath[state.currentPath.length-2]; // Prev
animateTransition('out', async () => {
state.currentPath.pop();
state.currentHandle = parent;
state.objects.forEach(o => filesGroup.remove(o));
state.objects = [];
const entries = [];
for await (const e of parent.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 }));
line.userData = { baseOpacity: 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;
sprite.userData = { baseOpacity: 1.0 };
mesh.add(sprite);
filesGroup.add(mesh);
state.objects.push(mesh);
}
// --- Interaction Logic ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObj = null;
const onPointerDown = (e) => {
sfx.init();
if (e.button !== 0) return;
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) {
sfx.play('click');
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;
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -1.5);
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;
const screenPos = mesh.position.clone().project(camera);
if (screenPos.y < -0.5) {
sfx.play('save');
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 ---
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();
sfx.play('click');
if (intersects.length > 0) {
selectedObj = getRootObject(intersects[0].object);
showMenu(ctxObj, e.clientX, e.clientY);
} else {
selectedObj = null;
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';
}
document.getElementById('cm-open').onclick = () => performOpen(selectedObj);
document.getElementById('cm-copy').onclick = () => { if(selectedObj) state.clipboard = selectedObj.userData.handle; hideContextMenus(); };
document.getElementById('cm-up').onclick = () => { goUp(); 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 {
sfx.play('open');
openPreview(h);
}
hideContextMenus();
}
// --- HUD ---
const hudCanvas = document.getElementById('hud-canvas');
const hudCtx = hudCanvas.getContext('2d');
const hudLabel = document.getElementById('hud-label');
let lastHover = null;
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) {
if(state.hovered !== target) {
sfx.play('hover');
}
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';
hudCtx.beginPath();
hudCtx.strokeStyle = color;
hudCtx.lineWidth = 2;
hudCtx.moveTo(x, y);
hudCtx.lineTo(x + 20, y - 20);
hudCtx.lineTo(x + 80, y - 20);
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 = (x + 80) + 'px';
hudLabel.style.top = (y - 35) + 'px';
hudLabel.style.borderColor = color;
hudLabel.style.color = color;
hudLabel.innerText = mesh.userData.handle.name.toUpperCase();
}
// --- Preview & Save ---
const pModal = document.getElementById('preview');
const pBody = document.getElementById('p-body');
const btnSave = document.getElementById('p-save');
let currentFileHandle = null;
async function openPreview(handle) {
currentFileHandle = handle;
pModal.classList.add('active');
document.getElementById('p-title').innerText = handle.name;
pBody.innerHTML = '<div style="padding:20px; color:#fff;">Loading...</div>';
btnSave.style.display = 'none';
try {
const file = await handle.getFile();
const name = file.name.toLowerCase();
if(name.match(/\.(png|jpg|jpeg|gif|webp)$/)) {
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 {
// Text Editor
try {
const text = await file.text();
if (text.includes('\0') && !name.endsWith('.txt')) throw new Error("Binary");
const ta = document.createElement('textarea');
ta.id = 'editor-area';
ta.className = 'hex-view';
ta.value = text;
pBody.innerHTML = '';
pBody.appendChild(ta);
btnSave.style.display = 'inline-block';
} catch (err) {
// Binary view fallback (readonly)
pBody.innerHTML = '<div style="padding:20px;">Binary file. Read-only.</div>';
}
}
} catch(e) {
pBody.innerHTML = '<div style="padding:20px; color:red;">Error.</div>';
}
}
document.getElementById('p-close').onclick = () => {
sfx.play('click');
pModal.classList.remove('active');
};
btnSave.onclick = async () => {
sfx.play('save');
const ta = document.getElementById('editor-area');
if (currentFileHandle && ta) {
try {
const w = await currentFileHandle.createWritable();
await w.write(ta.value);
await w.close();
// Flash effect
ta.style.background = '#002200';
setTimeout(() => ta.style.background = '#000', 200);
} catch(e) { alert("Save failed: " + e); }
}
};
// --- Shelf ---
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);
});
}
const tab = document.getElementById('shelf-tab');
tab.onclick = () => {
sfx.play('click');
shelf.classList.toggle('closed');
tab.innerText = shelf.classList.contains('closed') ? '▲ DOCK' : '▼ DOCK';
};
container.addEventListener('dragover', e => e.preventDefault());
container.addEventListener('drop', e => {
e.preventDefault();
const idx = e.dataTransfer.getData('idx');
if(idx !== '') {
sfx.play('click');
const h = state.shelfData[idx];
state.shelfData.splice(idx, 1);
renderShelf();
// Restore logic simplified for brevity
enterFolder(state.currentHandle, false);
}
});
// --- Tree ---
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 = () => { sfx.play('click'); 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>

コメント