SF映画っぽいエクスプローラ「Exp3D 1.4」


画像
画像

・ダウンロードされる方はこちら。↓


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Exp3D 1.4.3 (Refresh & Cyber Cursor)</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 (ACROBATIC 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;
        }
        #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); 
            transform: translateZ(-1000px) scale(0.1) rotateX(1080deg);
            transition: transform 1.2s cubic-bezier(0.19, 1, 0.22, 1); 
        }
        
        #preview.active #p-box { 
            transform: translateZ(0) 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));
        // Bloom強化 (Strength 1.5 -> 2.0)
        const bloom = new UnrealBloomPass(new THREE.Vector2(container.clientWidth, container.clientHeight), 2.0, 0.3, 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 (3 Layer Segmented Cyber Cursor) ---
        const reticleGroup = new THREE.Group();
        scene.add(reticleGroup);
        reticleGroup.visible = false;

        // Helper for segmented rings
        function createSegmentedRing(radiusInner, radiusOuter, count, color, opacity, speed) {
            const g = new THREE.Group();
            const angleSize = (Math.PI * 2) / count;
            const gap = 0.1; // Gap between segments
            for(let i=0; i<count; i++) {
                const start = i * angleSize + gap;
                const len = angleSize - gap*2;
                const geo = new THREE.RingGeometry(radiusInner, radiusOuter, 16, 1, start, len);
                const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: opacity, side: THREE.DoubleSide });
                g.add(new THREE.Mesh(geo, mat));
            }
            g.userData = { speed: speed };
            return g;
        }

        // Layer 1: Inner (Small segments, fast)
        const r1 = createSegmentedRing(1.4, 1.55, 3, CONFIG.colCursor, 1.0, 0.08);
        reticleGroup.add(r1);

        // Layer 2: Middle (Medium segments, reverse, medium speed) - Changed from Wire to Band
        const r2 = createSegmentedRing(1.65, 1.8, 5, CONFIG.colCursor, 0.7, -0.05);
        reticleGroup.add(r2);

        // Layer 3: Outer (Large segments, slow)
        const r3 = createSegmentedRing(1.9, 2.1, 8, CONFIG.colCursor, 0.4, 0.02);
        reticleGroup.add(r3);

        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');
            
            if(!animate) {
                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 (SCATTER / GATHER) ---
        function animateTransition(dir, cb) {
            if (state.isTransitioning) return;
            state.isTransitioning = true;
            sfx.play(dir==='in'?'open':'back');
            
            const startT = Date.now();
            const duration = 800; // ms

            const oldObjects = [...state.objects];
            oldObjects.forEach(o => {
                o.userData.startPos = o.position.clone();
                const v = new THREE.Vector3(o.position.x, o.position.y, 0).normalize();
                if(v.lengthSq()===0) v.set(1,0,0);
                o.userData.scatterDir = v;
            });

            function loop1() {
                const p = Math.min((Date.now() - startT) / duration, 1);
                const ease = p * p; // Ease In
                
                oldObjects.forEach(o => {
                    const sp = o.userData.startPos;
                    const sd = o.userData.scatterDir;
                    
                    if (dir === 'in') {
                        o.position.z = sp.z + (200 * ease);
                        o.position.x = sp.x + (sd.x * 50 * ease);
                        o.position.y = sp.y + (sd.y * 50 * ease);
                    } else {
                        o.position.z = sp.z - (300 * ease);
                        o.position.x = sp.x * (1 - ease);
                        o.position.y = sp.y * (1 - ease);
                    }
                    
                    if(o.material) o.material.opacity = 0.3 * (1 - p);
                    o.children.forEach(c => { if(c.material) c.material.opacity = (1 - p); });
                });

                if (p < 1) {
                    requestAnimationFrame(loop1);
                } else {
                    oldObjects.forEach(o => filesGroup.remove(o));
                    state.objects = [];
                    cb(); 
                    const newObjects = state.objects;
                    newObjects.forEach(o => {
                        o.userData.targetPos = o.position.clone();
                        const v = new THREE.Vector3(o.position.x, o.position.y, 0).normalize();
                        if(v.lengthSq()===0) v.set(1,0,0);
                        
                        if (dir === 'in') {
                            o.position.z = -300;
                            o.position.x = 0; o.position.y = 0;
                        } else {
                            o.position.z = 200;
                            o.position.x = o.userData.targetPos.x + (v.x * 50);
                            o.position.y = o.userData.targetPos.y + (v.y * 50);
                        }
                        if(o.material) o.material.opacity = 0;
                        o.children.forEach(c => { if(c.material) c.material.opacity = 0; });
                    });

                    const startT2 = Date.now();
                    function loop2() {
                        const p2 = Math.min((Date.now() - startT2) / duration, 1);
                        const ease2 = 1 - Math.pow(1 - p2, 3); // Ease Out
                        
                        newObjects.forEach(o => {
                            const tp = o.userData.targetPos;
                            if (dir === 'in') {
                                o.position.z = -300 + (tp.z - (-300)) * ease2;
                                o.position.x = tp.x * ease2;
                                o.position.y = tp.y * ease2;
                            } else {
                                const v = new THREE.Vector3(tp.x, tp.y, 0).normalize();
                                if(v.lengthSq()===0) v.set(1,0,0);
                                o.position.z = 200 + (tp.z - 200) * ease2;
                                o.position.x = (tp.x + v.x * 50) + (tp.x - (tp.x + v.x * 50)) * ease2;
                                o.position.y = (tp.y + v.y * 50) + (tp.y - (tp.y + v.y * 50)) * ease2;
                            }
                            if(o.material) o.material.opacity = 0.3 * p2;
                            o.children.forEach(c => { if(c.material) c.material.opacity = p2; });
                        });

                        if(p2 < 1) requestAnimationFrame(loop2);
                        else {
                            state.isTransitioning = false;
                            newObjects.forEach(o => {
                                o.position.copy(o.userData.targetPos);
                                if(o.material) o.material.opacity = 0.3;
                                o.children.forEach(c => { if(c.material) c.material.opacity = 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') {
                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 {
                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');
                });
            }
            
            if(!state.isTransitioning) {
                filesGroup.scale.set(1,1,1);
                filesGroup.rotation.set(0,0,0);
                filesGroup.position.set(0,0,0);
            }
        }

        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); 
            }

            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);

            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');
            
            // Thumbnail Material: MeshBasicMaterial for Glow
            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);

            if (!isF) {
                const n = handle.name.toLowerCase();
                const loadT = (t) => {
                    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}); // Changed to Basic for GLOW
                    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)$/i)) {
                    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(()=>{});
                    });
                }
            }

            const iCvs = document.createElement('canvas'); iCvs.width=512; iCvs.height=256;
            const iCtx = iCvs.getContext('2d');
            const name = handle.name.length > 15 ? handle.name.substring(0,12)+"..." : handle.name;
            const date = new Date().toLocaleDateString();
            
            if(mode === 'F2') {
                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 {
                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); iSprite.scale.set(5, 2.5, 1); }
            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;

            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);

                const { r1, r2, r3 } = reticleGroup.userData;
                // Layer Rotations (Cyber Style)
                if(r1) r1.rotation.z += r1.userData.speed;
                if(r2) r2.rotation.z += r2.userData.speed;
                if(r3) r3.rotation.z += r3.userData.speed;

                const s = (state.selected && popupMesh) ? 3 : 1;
                reticleGroup.scale.set(s,s,1);
            } else {
                reticleGroup.visible = false;
            }
            drawHUD(); composer.render();
        }
        animate();

        // --- HUD / Menu ---
        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;
        }

        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);
            }
        });

        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();
        
        // --- Dropping from Dock: REFRESH ---
        container.ondrop=e=>{
            e.preventDefault(); const i=e.dataTransfer.getData('i');
            if(i!==''){
                const h=state.shelfData[i]; state.shelfData.splice(i,1); renderShelf();
                // Instead of manual placement which caused Z-offset issues, just refresh folder
                enterFolder(state.currentHandle, false);
            }
        };

        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>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
SF映画っぽいエクスプローラ「Exp3D 1.4」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1