SF映画っぽいエクスプローラ「Exp3D 1.4」
・ダウンロードされる方はこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Exp3D 1.4.2</title>
<style>
:root {
--cyan: #00ffff;
--magenta: #ff00ff;
--yellow: #ffee00;
--bg: #010103;
--panel: rgba(5, 10, 15, 0.95);
}
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; position: relative; }
/* Sidebar */
#sidebar {
position: absolute; top: 0; left: 0; bottom: 0; width: 280px;
background: var(--panel); border-right: 1px solid var(--cyan);
display: flex; flex-direction: column; z-index: 50; backdrop-filter: blur(10px);
transition: transform 0.3s; transform: translateX(0);
}
#sidebar.closed { transform: translateX(-280px); }
#sb-tab {
position: absolute; top: 50%; right: -30px; width: 30px; height: 100px; margin-top: -50px;
background: var(--panel); border: 1px solid var(--cyan); border-left: none;
display: flex; align-items: center; justify-content: center; cursor: pointer;
writing-mode: vertical-rl; color: var(--cyan); font-weight:bold;
border-radius: 0 10px 10px 0;
}
#sb-header { padding: 15px; color: var(--cyan); border-bottom: 1px solid rgba(0,255,255,0.3); font-weight: bold; letter-spacing: 2px; }
#tree-container { flex: 1; overflow-y: auto; padding: 10px; }
.tree-node { display: flex; align-items: center; padding: 6px; cursor: pointer; color: #888; transition:0.2s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
.tree-node:hover { color: #fff; background: rgba(0,255,255,0.1); }
.icon-tree { margin-right: 8px; display: inline-block; }
/* Main View */
#main { flex: 1; position: relative; overflow: hidden; }
#canvas-wrap { width: 100%; height: 100%; position: absolute; }
#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; }
#mode-info {
position: absolute; top: 20px; right: 20px; text-align: right; pointer-events: none;
color: var(--yellow); text-shadow: 0 0 5px var(--yellow); font-weight: bold; font-size: 18px; z-index: 20;
}
/* Async Loader */
#loader {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
justify-content: center; align-items: center; z-index: 100;
display: none;
}
#loader.active { display: flex; }
.loader-text { color: var(--cyan); font-size: 24px; margin-bottom: 20px; animation: blink 1s infinite; letter-spacing: 2px; }
@keyframes blink { 50% { opacity: 0.5; } }
/* Shelf (Dock) */
#shelf {
position: absolute; bottom: 0; left: 0; right: 0; height: 160px;
background: linear-gradient(to top, rgba(0,20,30,0.98), transparent);
border-top: 2px solid var(--magenta); display: flex; align-items: center; padding: 0 50px; gap: 20px;
z-index: 40; transition: bottom 0.3s; overflow-x: auto;
}
#shelf.closed { bottom: -160px; }
#shelf-tab {
position: absolute; bottom: 160px; left: 50%; transform: translateX(-50%);
background: var(--magenta); color: #000; padding: 2px 30px; font-weight: bold;
cursor: pointer; clip-path: polygon(15% 0, 85% 0, 100% 100%, 0% 100%); z-index: 41; transition: bottom 0.3s;
}
#shelf.closed + #shelf-tab { bottom: 0; }
.shelf-item {
width: 100px; height: 120px; 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); flex-shrink: 0; position: relative;
}
/* Context Menu */
.ctx-menu {
position: fixed; display: none; flex-direction: column;
background: rgba(10, 15, 20, 0.98); border: 1px solid var(--cyan);
box-shadow: 0 0 20px rgba(0,255,255,0.3); min-width: 200px; z-index: 9999;
transform-origin: top; animation: menuSlide 0.15s ease-out; padding: 5px 0;
}
@keyframes menuSlide { from { transform: scaleY(0); opacity: 0; } to { transform: scaleY(1); opacity: 1; } }
.ctx-item { padding: 10px 15px; cursor: pointer; color: #ccc; display: flex; justify-content: space-between; border-bottom: 1px solid #333; }
.ctx-item:hover { background: var(--cyan); color: #000; }
.ctx-item:last-child { border-bottom: none; }
/* Preview (Backflip) */
#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; transition: opacity 0.3s;
perspective: 1500px; /* Perspective for 3D rotation */
}
#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.2);
/* Initial State: Tiny and Rotated 3 times (1080deg) */
transform: scale(0.0) rotateX(1080deg);
/* Slow down transition to see the flips */
transition: transform 1.0s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#preview.active #p-box {
/* Final State: Normal size, no rotation */
transform: scale(1) rotateX(0deg);
}
#p-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; 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; transition: 0.2s; }
.p-btn:hover { background: var(--cyan); color: #000; }
.p-btn-save { display: none; border-color: #0f0; color: #0f0; }
.hex-view { width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000; border: none; resize: none; font-family: 'Courier New', monospace; }
.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% 3px; z-index: 9999; opacity: 0.15;
}
</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 id="sb-tab">TREE</div>
</div>
<div id="main">
<div id="canvas-wrap"></div>
<div id="hud"><canvas id="hud-canvas"></canvas></div>
<div id="mode-info">MODE: F1 (VERTICAL)</div>
<div id="loader">
<div class="loader-text">READING DATA...</div>
<button id="btn-cancel-load" class="p-btn" style="border-color:#ff5555; color:#ff5555;">CLOSE</button>
</div>
<div id="shelf">
<div style="color:var(--magenta); opacity:0.6;">[ DRAG FILES HERE ]</div>
</div>
<div id="shelf-tab">DOCK</div>
<div id="ctx-obj" 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-rename">Rename</div>
<div class="ctx-item" id="cm-delete" style="color:#ff5555">Delete</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</div>
<div class="ctx-item" id="cm-up">Up Directory</div>
<div class="ctx-item" id="cm-refresh">Refresh</div>
</div>
</div>
</div>
<div id="preview">
<div id="p-box">
<div id="p-head">
<span id="p-title">FILE</span>
<div>
<button id="p-save" class="p-btn p-btn-save">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';
const CONFIG = {
colFolder: 0xffee00, colFile: 0x00ffff, colCursor: 0xffee00, grid: 0x002233
};
const state = {
rootHandle: null, currentHandle: null, currentPath: [],
objects: [], shelfData: [], dragging: null, hovered: null, selected: null,
clipboard: null, currentEntries: [],
viewMode: 'F1',
targetRotX: 0, currentRotX: 0,
targetRotY: 0, currentRotY: 0,
targetPosX: 0, currentPosX: 0,
targetPosY: 0, currentPosY: 0,
isRotating: false, lastMouse: new THREE.Vector2(),
isTransitioning: false, contextTarget: null,
dragStart: new THREE.Vector2(), isLoading: 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 o = sfx.ctx.createOscillator(); const g = sfx.ctx.createGain();
o.connect(g); g.connect(sfx.ctx.destination);
if(type==='click'){o.frequency.setValueAtTime(600,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
else if(type==='hover'){o.frequency.setValueAtTime(300,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.05);o.start(t);o.stop(t+0.05);}
else if(type==='open'){o.type='triangle';o.frequency.setValueAtTime(200,t);o.frequency.linearRampToValueAtTime(600,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
else if(type==='back'){o.type='triangle';o.frequency.setValueAtTime(600,t);o.frequency.linearRampToValueAtTime(200,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
else if(type==='lock'){o.type='square';o.frequency.setValueAtTime(1000,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
}
};
// --- Three.js ---
const container = document.getElementById('canvas-wrap');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020205, 0.01);
const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 0, 0.1);
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(0xffffff, 1.5));
const dl = new THREE.DirectionalLight(0xffffff, 2);
dl.position.set(0, 50, 0);
scene.add(dl);
// --- Celestial Background ---
const starGroup = new THREE.Group();
const starsGeo = new THREE.BufferGeometry();
const starCount = 5000;
const starPos = new Float32Array(starCount * 3);
for(let i=0; i<starCount*3; i++) starPos[i] = (Math.random()-0.5)*500;
starsGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({color:0xffffff, size:0.5, transparent:true, opacity:0.8});
starGroup.add(new THREE.Points(starsGeo, starMat));
const orbitGroup = new THREE.Group();
for(let i=0; i<8; i++) {
const r = 40 + i*15;
const geo = new THREE.TorusGeometry(r, 0.1, 16, 100);
const mat = new THREE.MeshBasicMaterial({color:0x00ffff, side:THREE.DoubleSide, transparent:true, opacity:0.15});
const ring = new THREE.Mesh(geo, mat);
ring.rotation.x = Math.random()*Math.PI; ring.rotation.y = Math.random()*Math.PI;
orbitGroup.add(ring);
}
starGroup.add(orbitGroup);
scene.add(starGroup);
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.enablePan = false; orbitControls.enableRotate = false; orbitControls.enableZoom = false;
orbitControls.target.set(0,0,-1);
const filesGroup = new THREE.Group();
scene.add(filesGroup);
// --- Reticle (Modified: Baumkuchen/Roulette Style) ---
const reticleGroup = new THREE.Group();
scene.add(reticleGroup);
reticleGroup.visible = false;
// Layer 1: Inner (Solid, fast spin)
const r1 = new THREE.Mesh(new THREE.RingGeometry(1.4, 1.6, 32), new THREE.MeshBasicMaterial({ color: CONFIG.colCursor, transparent: true, opacity: 0.8, side: THREE.DoubleSide }));
reticleGroup.add(r1);
// Layer 2: Middle (Wire/Dashed look, medium spin reverse)
const r2 = new THREE.Mesh(new THREE.RingGeometry(1.7, 1.9, 16), new THREE.MeshBasicMaterial({ color: CONFIG.colCursor, transparent: true, opacity: 0.5, side: THREE.DoubleSide, wireframe: true }));
reticleGroup.add(r2);
// Layer 3: Outer (Brackets/Segments, slow spin)
const bGeo = new THREE.BufferGeometry();
const bs=2.2, bl=0.8;
// Create broken ring segments
const segments = [];
for(let i=0; i<8; i++) {
const ang = (i/8)*Math.PI*2;
const x = Math.cos(ang)*2.2; const y = Math.sin(ang)*2.2;
segments.push(x, y, 0);
segments.push(x*1.1, y*1.1, 0);
}
// Using existing bracket logic but modified for rotation
const r3Geo = new THREE.RingGeometry(2.1, 2.2, 8, 1, 0, Math.PI*2);
const r3 = new THREE.Mesh(r3Geo, new THREE.MeshBasicMaterial({ color: CONFIG.colCursor, transparent: true, opacity: 0.3, side: THREE.DoubleSide, wireframe:false }));
// Create gaps in the ring geometry manually or just use a dashed texture.
// For simplicity and "tech" look, let's use a wireframe ring with low segments.
reticleGroup.add(r3);
// Store references for independent rotation
reticleGroup.userData = { r1, r2, r3 };
// --- HUD ---
const hudC = document.getElementById('hud-canvas');
const hudX = hudC.getContext('2d');
function resizeHUD() {
const dpr = window.devicePixelRatio || 1;
hudC.width = container.clientWidth * dpr; hudC.height = container.clientHeight * dpr;
hudX.scale(dpr, dpr);
}
window.onresize = () => {
camera.aspect=container.clientWidth/container.clientHeight; camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
composer.setSize(container.clientWidth, container.clientHeight);
resizeHUD();
};
resizeHUD();
// --- Logic ---
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) {}
};
window.addEventListener('keydown', e => {
if(e.key === 'F1') { e.preventDefault(); setMode('F1'); }
if(e.key === 'F2') { e.preventDefault(); setMode('F2'); }
});
function setMode(mode) {
state.viewMode = mode;
document.getElementById('mode-info').innerText = mode==='F1' ? 'MODE: F1 (VERTICAL)' : 'MODE: F2 (HORIZONTAL)';
sfx.play('click');
rebuildCylinder();
}
// --- Async Loader ---
document.getElementById('btn-cancel-load').onclick = () => {
state.isLoading = false;
document.getElementById('loader').classList.remove('active');
};
async function enterFolder(handle, animate=true) {
state.currentHandle = handle;
if(!state.currentPath.includes(handle)) state.currentPath.push(handle);
state.isLoading = true;
document.getElementById('loader').classList.add('active');
// Clean up old
state.objects.forEach(o => filesGroup.remove(o));
state.objects = [];
reticleGroup.visible = false; state.selected = null; updatePopup(null);
const entries = [];
try {
for await (const e of handle.values()) {
if (!state.isLoading) break;
entries.push(e);
if (entries.length % 20 === 0) await new Promise(r => setTimeout(r, 0));
}
} catch(e) {}
document.getElementById('loader').classList.remove('active');
state.isLoading = false;
state.currentEntries = entries;
if(animate) animateTransition('in', rebuildCylinder);
else rebuildCylinder();
}
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);
});
}
}
// --- Transitions (Modified to Match 1.3 Logic) ---
function animateTransition(dir, cb) {
if (state.isTransitioning) return;
state.isTransitioning = true;
sfx.play(dir==='in'?'open':'back');
const startT = Date.now();
const startS = filesGroup.scale.x;
// 1.3 Style: "Zoom In" scales UP to 3.0+, "Zoom Out" scales DOWN to 0.1
const targetS = dir==='in' ? 4.0 : 0.01;
function loop1() {
const p = Math.min((Date.now()-startT)/400, 1);
// Quadratic Ease In
const s = startS + (targetS - startS) * (p*p);
// Apply scale to the whole group (Simulates flying through/away)
filesGroup.scale.set(s,s,s);
// Fade out content
state.objects.forEach(o => { if(o.material) o.material.opacity = (1-p)*0.5; });
if(p < 1) requestAnimationFrame(loop1);
else {
// Phase 2: Switch Content
cb();
// Reset transforms
filesGroup.rotation.set(0,0,0);
state.targetRotX=0; state.currentRotX=0; state.targetRotY=0; state.currentRotY=0;
state.targetPosX=0; state.currentPosX=0; state.targetPosY=0; state.currentPosY=0;
// Setup for arrival
// If we came IN, new content starts small (0.1) and grows to 1
// If we went OUT, new content starts huge (4.0) and shrinks to 1
const startS2 = dir==='in' ? 0.1 : 4.0;
filesGroup.scale.set(startS2, startS2, startS2);
const startT2 = Date.now();
function loop2() {
const p2 = Math.min((Date.now()-startT2)/500, 1);
// Ease Out Back for a nice "pop" effect
const ease = 1 - Math.pow(1-p2, 3);
const s2 = startS2 + (1.0 - startS2) * ease;
filesGroup.scale.set(s2,s2,s2);
// Fade in new content
state.objects.forEach(o => {
if(o.material) o.material.opacity = p2 * 0.5; // Final opacity is roughly 0.5 for transparent boxes
});
if(p2<1) requestAnimationFrame(loop2);
else {
state.isTransitioning = false;
filesGroup.scale.set(1,1,1);
}
}
loop2();
}
}
loop1();
}
// --- Layout Rebuild ---
function rebuildCylinder() {
state.objects.forEach(o => filesGroup.remove(o));
state.objects = [];
const entries = state.currentEntries || [];
const count = entries.length;
const radius = 22;
if(state.viewMode === 'F1') {
// Vertical Cylinder (F1)
const spacing = 5.0;
const circumference = 2 * Math.PI * radius;
const maxCols = Math.floor(circumference / spacing);
const cols = Math.max(1, Math.min(count, maxCols));
const rowHeight = 7.0;
entries.forEach((e, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const angleStep = (Math.PI * 2) / Math.max(cols, 10);
const angle = col * angleStep + Math.PI;
const x = radius * Math.sin(angle);
const z = radius * Math.cos(angle);
let y = -(row * rowHeight) + (Math.floor(count/cols)*rowHeight)/2;
if (col % 2 !== 0) y -= rowHeight * 0.5;
createMonolith(e, x, y, z, angle, i, 'F1');
});
} else {
// Horizontal Cylinder (F2) - Wide Row
const spacing = 16.0;
const circumference = 2 * Math.PI * radius;
const maxRows = Math.floor(circumference / spacing);
const rows = Math.max(1, Math.min(count, maxRows));
const colWidth = 16.0;
entries.forEach((e, i) => {
const ringIndex = i % rows;
const colIndex = Math.floor(i / rows);
const angleStep = (Math.PI * 2) / Math.max(rows, 10);
const angle = ringIndex * angleStep;
const y = radius * Math.sin(angle);
const z = radius * Math.cos(angle);
const x = (colIndex * colWidth) - (Math.floor(count/rows)*colWidth)/2;
createMonolith(e, x, y, z, angle, i, 'F2');
});
}
filesGroup.scale.set(1,1,1); filesGroup.rotation.set(0,0,0);
}
// Draw Helper
function drawStrokeText(ctx, txt, x, y, size, align='left') {
ctx.font = `bold ${size}px "Segoe UI"`;
ctx.textAlign = align; ctx.textBaseline = 'middle';
ctx.lineWidth = 6; ctx.strokeStyle = '#000000'; ctx.strokeText(txt, x, y);
ctx.fillStyle = '#ffffff'; ctx.fillText(txt, x, y);
}
function createMonolith(handle, x, y, z, angle, index, mode) {
const isF = handle.kind === 'directory';
const col = isF ? CONFIG.colFolder : CONFIG.colFile;
const geo = new THREE.BoxGeometry(2.4, 3.2, 0.1);
const mat = new THREE.MeshPhysicalMaterial({ color: col, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
if(mode === 'F1') {
mesh.lookAt(0, y, 0);
} else {
mesh.lookAt(x, 0, 0);
mesh.rotateZ(-Math.PI/2); // Fixed F2 orientation
}
mesh.userData = { handle, isFolder:isF, isRoot:true, thumbTex:null };
const edges = new THREE.EdgesGeometry(geo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }));
mesh.add(line);
// 1. Thumbnail
const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=256;
const ctx = cvs.getContext('2d');
const ext = handle.name.split('.').pop().toUpperCase().slice(0,4);
drawStrokeText(ctx, isF ? 'DIR' : ext, 128, 128, 80, 'center');
const defTex = new THREE.CanvasTexture(cvs);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: defTex }));
sprite.scale.set(2, 2, 1);
sprite.position.z = 0.2;
mesh.add(sprite);
// Async Thumb
if (!isF) {
const n = handle.name.toLowerCase();
const loadT = (t) => {
// Fix F1 Orientation if needed
if(mode==='F1') { t.center.set(0.5, 0.5); t.rotation = 0; t.flipY = true; }
mesh.material = new THREE.MeshBasicMaterial({map:t, color:0xffffff});
sprite.visible=false; mesh.userData.thumbTex=t;
};
if(n.match(/\.(jpg|png|webp|gif)$/)) {
handle.getFile().then(f => new THREE.TextureLoader().load(URL.createObjectURL(f), loadT));
} else if(n.match(/\.bmp$/)) {
handle.getFile().then(f => createImageBitmap(f)).then(bmp => {
loadT(new THREE.CanvasTexture(bmp));
});
} else if(n.match(/\.(mp4|webm)$/)) {
handle.getFile().then(f => {
const v = document.createElement('video'); v.src=URL.createObjectURL(f);
v.muted=true; v.loop=true; v.play().then(()=>{ loadT(new THREE.VideoTexture(v)); }).catch(()=>{});
});
}
}
// 2. Info Panel
const iCvs = document.createElement('canvas'); iCvs.width=512; iCvs.height=256;
const iCtx = iCvs.getContext('2d');
const ix = mode==='F2' ? 200 : 10;
const name = handle.name.length > 15 ? handle.name.substring(0,12)+"..." : handle.name;
const date = new Date().toLocaleDateString();
if(mode === 'F2') {
// F2: Left Number, Right Info
drawStrokeText(iCtx, `#${index+1}`, 80, 128, 60, 'center');
drawStrokeText(iCtx, name, 250, 50, 40);
drawStrokeText(iCtx, "SIZE: --", 250, 100, 30);
drawStrokeText(iCtx, "DATE: "+date, 250, 150, 30);
} else {
// F1: Info Only
drawStrokeText(iCtx, name, 10, 50, 40);
drawStrokeText(iCtx, "SIZE: --", 10, 100, 30);
drawStrokeText(iCtx, "DATE: "+date, 10, 150, 30);
}
const iTex = new THREE.CanvasTexture(iCvs);
const iSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: iTex }));
if(mode === 'F1') {
iSprite.position.set(4.5, 0, 0);
iSprite.scale.set(5, 2.5, 1);
} else {
iSprite.position.set(0, 4.0, 0); // Above/Right in local
iSprite.scale.set(5, 2.5, 1);
}
// Push text slightly forward
iSprite.position.z += 0.2;
mesh.add(iSprite);
filesGroup.add(mesh);
state.objects.push(mesh);
}
// --- Interaction ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let popupMesh = null;
function getRoot(o) { while(o){if(o.userData?.isRoot)return o; o=o.parent;} return null; }
function updateMouse(e) {
const r = renderer.domElement.getBoundingClientRect();
mouse.x = ((e.clientX-r.left)/r.width)*2-1;
mouse.y = -((e.clientY-r.top)/r.height)*2+1;
}
function updatePopup(target) {
if(popupMesh) { filesGroup.remove(popupMesh); popupMesh=null; }
if(!target) return;
const geo = new THREE.PlaneGeometry(6, 8);
const mat = new THREE.MeshBasicMaterial({
map: target.userData.thumbTex || null,
color: target.userData.thumbTex ? 0xffffff : (target.userData.isFolder?CONFIG.colFolder:CONFIG.colFile),
transparent: true, opacity: 0.95, side: THREE.DoubleSide
});
popupMesh = new THREE.Mesh(geo, mat);
popupMesh.position.copy(target.position);
if(state.viewMode === 'F1') {
const dir = new THREE.Vector3().subVectors(new THREE.Vector3(0,target.position.y,0), target.position).normalize();
popupMesh.position.add(dir.multiplyScalar(5));
popupMesh.lookAt(0, target.position.y, 0);
} else {
const center = new THREE.Vector3(target.position.x, 0, 0);
const dir = new THREE.Vector3().subVectors(center, target.position).normalize();
popupMesh.position.add(dir.multiplyScalar(5));
popupMesh.lookAt(center);
popupMesh.rotateZ(-Math.PI/2);
}
popupMesh.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({color:0xffffff})));
filesGroup.add(popupMesh);
reticleGroup.scale.set(3,3,1);
}
window.addEventListener('pointerdown', e => {
sfx.init();
if(e.button!==0 || !e.target.closest('canvas')) return;
updateMouse(e);
state.dragStart.set(e.clientX, e.clientY);
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
if(hits.length>0) {
const t = getRoot(hits[0].object);
state.selected = t;
state.dragging = { mesh:t, isMove: false };
orbitControls.enabled = false;
} else {
state.selected = null;
reticleGroup.visible = false;
updatePopup(null);
state.isRotating = true;
state.lastMouse.set(e.clientX, e.clientY);
document.body.style.cursor = 'grabbing';
}
});
window.addEventListener('pointermove', e => {
updateMouse(e);
if(state.isRotating) {
const deltaX = e.clientX - state.lastMouse.x;
const deltaY = e.clientY - state.lastMouse.y;
if(state.viewMode === 'F1') {
state.targetRotY -= deltaX * 0.005;
state.targetPosY += deltaY * 0.05;
} else {
state.targetPosX += deltaX * 0.05;
state.targetRotX -= deltaY * 0.005;
}
state.lastMouse.set(e.clientX, e.clientY);
} else if(state.dragging) {
if(state.dragStart.distanceTo(new THREE.Vector2(e.clientX, e.clientY)) > 5) state.dragging.isMove = true;
} else {
checkHover();
}
});
window.addEventListener('pointerup', (e) => {
if(state.dragging) {
if(!state.dragging.isMove) {
activateReticle(state.dragging.mesh);
updatePopup(state.dragging.mesh);
sfx.play('lock');
} else {
const m = state.dragging.mesh;
if(e.clientY > window.innerHeight - 160) moveToShelf(m);
}
state.dragging = null;
orbitControls.enabled = true;
}
state.isRotating = false;
document.body.style.cursor = 'default';
});
window.addEventListener('wheel', e => {
if(e.target.closest('canvas')) {
if(state.viewMode==='F1') state.targetRotY += e.deltaY * 0.001;
else state.targetRotX += e.deltaY * 0.001;
}
});
function activateReticle(mesh) { reticleGroup.visible = true; }
function checkHover() {
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
if(hits.length>0) {
const t = getRoot(hits[0].object);
if(state.hovered !== t) sfx.play('hover');
state.hovered = t;
document.body.style.cursor = 'pointer';
} else {
state.hovered = null;
document.body.style.cursor = 'default';
}
}
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
state.currentRotY += (state.targetRotY - state.currentRotY) * 0.1;
state.currentPosY += (state.targetPosY - state.currentPosY) * 0.1;
state.currentRotX += (state.targetRotX - state.currentRotX) * 0.1;
state.currentPosX += (state.targetPosX - state.currentPosX) * 0.1;
if(state.viewMode === 'F1') {
filesGroup.rotation.y = state.currentRotY;
filesGroup.position.y = state.currentPosY;
filesGroup.rotation.x = 0; filesGroup.position.x = 0;
} else {
filesGroup.rotation.x = state.currentRotX;
filesGroup.position.x = state.currentPosX;
filesGroup.rotation.y = 0; filesGroup.position.y = 0;
}
starGroup.rotation.y += 0.0005; orbitGroup.rotation.x += 0.001; orbitGroup.rotation.y += 0.001;
// Reticle Animation (Modified: 3 Layer Roulette)
if(reticleGroup.visible && (state.selected || state.hovered)) {
const t = state.selected || state.hovered;
const wp = new THREE.Vector3(); t.getWorldPosition(wp);
const wq = new THREE.Quaternion(); t.getWorldQuaternion(wq);
reticleGroup.position.copy(wp);
reticleGroup.quaternion.copy(wq);
reticleGroup.translateZ(0.2);
// Independent Rotation per layer
if(reticleGroup.userData.r1) {
reticleGroup.userData.r1.rotation.z += 0.08; // Inner fast
reticleGroup.userData.r2.rotation.z -= 0.04; // Middle reverse medium
reticleGroup.userData.r3.rotation.z += 0.01; // Outer slow
}
const s = (state.selected && popupMesh) ? 3 : 1;
reticleGroup.scale.set(s,s,1);
} else {
reticleGroup.visible = false;
}
drawHUD();
composer.render();
}
animate();
// --- HUD ---
function drawHUD() {
const dpr = window.devicePixelRatio||1;
hudX.clearRect(0,0,hudC.width/dpr, hudC.height/dpr);
const t = state.hovered || state.selected;
if(!t || state.dragging) return;
const p = new THREE.Vector3(); t.getWorldPosition(p);
p.y += 2.0; p.project(camera);
if(p.z > 1) return;
const x = (p.x*0.5+0.5)*(hudC.width/dpr);
const y = (p.y*-0.5+0.5)*(hudC.height/dpr);
hudX.shadowBlur = 10; hudX.shadowColor = CONFIG.colCursor;
hudX.strokeStyle = '#ffee00'; hudX.lineWidth = 2;
hudX.beginPath(); hudX.moveTo(x,y); hudX.lineTo(x+40,y-40); hudX.lineTo(x+160,y-40); hudX.stroke();
drawStrokeText(hudX, t.userData.handle.name, x+45, y-45, 20);
hudX.shadowBlur = 0;
}
// --- Context Menu ---
const ctxObj = document.getElementById('ctx-obj');
const ctxBg = document.getElementById('ctx-bg');
document.querySelectorAll('.ctx-item').forEach(el => {
el.addEventListener('pointerdown', e => e.stopPropagation());
el.addEventListener('click', e => e.stopPropagation());
});
document.querySelectorAll('.ctx-menu').forEach(el => el.addEventListener('pointerdown', e => e.stopPropagation()));
window.addEventListener('contextmenu', e => {
e.preventDefault();
if(e.target.closest('.tree-node')) {
showMenu(ctxObj, e.clientX, e.clientY);
return;
}
if(!e.target.closest('canvas')) return;
updateMouse(e); raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(state.objects, true);
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
sfx.play('click');
if(hits.length>0) {
const t = getRoot(hits[0].object);
state.selected = t; state.contextTarget = t.userData.handle;
activateReticle(t); updatePopup(t);
showMenu(ctxObj, e.clientX, e.clientY);
} else {
state.selected = null; state.contextTarget = null;
updatePopup(null);
showMenu(ctxBg, e.clientX, e.clientY);
}
});
function showMenu(el, x, y) {
el.style.display='flex';
if(x+200>window.innerWidth) x-=200; if(y+200>window.innerHeight) y-=200;
el.style.left=x+'px'; el.style.top=y+'px';
}
function getActionTarget() { return state.contextTarget || (state.selected ? state.selected.userData.handle : null); }
document.getElementById('cm-open').onclick = () => {
const t = getActionTarget();
if(t) { if(t.kind==='directory') { enterFolder(t); updateTree(state.rootHandle); } else openPreview(t); }
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-copy').onclick = () => { state.clipboard = getActionTarget(); document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); };
document.getElementById('cm-delete').onclick = async () => {
const t = getActionTarget();
if(t && confirm('Delete?')) { await state.currentHandle.removeEntry(t.name, {recursive:true}); enterFolder(state.currentHandle, false); }
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-rename').onclick = async () => {
const t = getActionTarget();
if(t) { const n = prompt("Rename:", t.name); if(n && t.move) { await t.move(n); enterFolder(state.currentHandle, false); } }
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-prop').onclick = async () => {
const t = getActionTarget(); if(t) alert(`Name: ${t.name}\nKind: ${t.kind}`);
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-new').onclick = async () => {
const n = prompt("New Folder Name:"); if(n) { await state.currentHandle.getDirectoryHandle(n, {create:true}); enterFolder(state.currentHandle, false); }
document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-paste').onclick = async () => {
if(state.clipboard && state.clipboard.kind === 'file') {
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);
} document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
};
document.getElementById('cm-up').onclick = () => { goUp(); document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); };
document.getElementById('cm-refresh').onclick = () => { enterFolder(state.currentHandle, false); document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); };
window.addEventListener('dblclick', e => {
if(!e.target.closest('canvas')) return;
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.userData.isFolder) { enterFolder(t.userData.handle); updateTree(state.rootHandle); }
else openPreview(t.userData.handle);
}
});
// --- Sidebar/Shelf ---
document.getElementById('sb-tab').onclick=()=>document.getElementById('sidebar').classList.toggle('closed');
document.getElementById('shelf-tab').onclick=()=>document.getElementById('shelf').classList.toggle('closed');
function moveToShelf(m) {
filesGroup.remove(m); state.objects=state.objects.filter(o=>o!==m);
if(popupMesh) { filesGroup.remove(popupMesh); popupMesh=null; }
state.shelfData.push(m.userData.handle); renderShelf();
}
function renderShelf() {
const s = document.getElementById('shelf'); s.innerHTML='';
state.shelfData.forEach((h,i)=>{
const d=document.createElement('div'); d.className='shelf-item'; d.draggable=true;
d.innerHTML=`<div style="font-size:30px">📦</div><div style="font-size:10px">${h.name}</div>`;
d.ondragstart=e=>e.dataTransfer.setData('i',i);
s.appendChild(d);
});
}
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();
createMonolith(h, 0, 0, 10, 0, 0, state.viewMode);
}
};
// --- Preview ---
const pModal=document.getElementById('preview'), pBody=document.getElementById('p-body');
const btnSave = document.getElementById('p-save');
let currentFileHandle = null;
document.getElementById('p-close').onclick=()=>pModal.classList.remove('active');
btnSave.onclick = async () => {
const ta = document.getElementById('editor-area');
if(currentFileHandle && ta) {
try {
const w = await currentFileHandle.createWritable();
await w.write(ta.value); await w.close();
alert("Saved!");
} catch(e) { alert("Save failed."); }
}
};
async function openPreview(handle) {
currentFileHandle = handle;
pModal.classList.add('active'); document.getElementById('p-title').innerText=handle.name;
pBody.innerHTML='Loading...'; btnSave.style.display='none';
try {
const f = await handle.getFile();
const u = URL.createObjectURL(f);
if(f.name.match(/\.(png|jpg|webp|gif|bmp)$/i)) pBody.innerHTML=`<img src="${u}" style="max-width:100%;max-height:100%">`;
else if(f.name.match(/\.(mp4|webm)$/i)) pBody.innerHTML=`<video src="${u}" controls autoplay style="max-width:100%"></video>`;
else if(f.name.match(/\.(fbx|obj|glb)$/i)) pBody.innerHTML='[3D Model View]';
else {
const t = await f.text();
pBody.innerHTML = `<textarea id="editor-area" class="hex-view">${t}</textarea>`;
btnSave.style.display = 'block';
}
} catch(e) { pBody.innerText="Error"; }
}
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=`<span class="icon-tree">${h.kind==='directory'?'📂':'📄'}</span>${h.name}`;
e.onclick=()=>enterFolder(h);
e.oncontextmenu=(ev)=>{ ev.preventDefault(); ev.stopPropagation(); state.contextTarget=h; showMenu(ctxObj, ev.clientX, ev.clientY); };
return e;
}
</script>
</body>
</html>

コメント