<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Sci -Fi 3D Explorer Pro </title>
<style>
:root {
--primary: #00ffff;
--secondary: #ff00ff;
--bg: #050510 ;
--glass: rgba(0 , 255 , 255 , 0.1 );
}
body { margin: 0 ; overflow: hidden; background-color: var (--bg); font-family: 'Segoe UI' , sans-serif; user-select: none ; }
#ui-layer { position: absolute; top: 0 ; left : 0 ; width: 100 %; height: 100 %; pointer-events: none ; z-index: 10 ; }
#header { padding: 20px; display: flex; align-items: center; gap: 20px; pointer-events: auto; }
h1 { margin: 0 ; color: var (--primary); font-size: 24px; text-shadow: 0 0 10px var (--primary); letter-spacing: 2px; }
#path-display { color: #fff; font-family: monospace; opacity: 0.8 ; background: rgba(0 ,0 ,0 ,0.5 ); padding: 5px 10px; border: 1px solid #333 ; }
button {
background: rgba(0 ,0 ,0 ,0.6 ); border: 1px solid var (--primary); color: var (--primary);
padding: 8px 20px; cursor: pointer; text-transform: uppercase; font-weight: bold;
transition: 0 .3s; box-shadow: 0 0 5px var (--primary);
}
button:hover { background: var (--primary); color: #000 ; box-shadow: 0 0 20px var (--primary); }
#preview-overlay {
position: absolute; top: 0 ; left : 0 ; width: 100 %; height: 100 %;
display: flex; justify-content: center; align-items: center;
background: rgba(0 ,0 ,0 ,0.8 ); backdrop-filter : blur(5px);
opacity: 0 ; visibility: hidden; transition: opacity 0 .3s; pointer-events: auto;
}
#preview-overlay.active { opacity: 1 ; visibility: visible; }
#preview-card {
width: 70 %; height: 80 %; background: rgba(10 , 20 , 30 , 0.95 );
border: 1px solid var (--primary);
box-shadow: 0 0 50px rgba(0 , 255 , 255 , 0.2 );
transform: perspective(1000px) rotateY(90deg) scale(0.5 );
transition: transform 0 .6s cubic-bezier(0.16 , 1 , 0.3 , 1 );
display: flex; flex-direction: column; overflow: hidden; position: relative;
}
#preview-overlay.active #preview-card {
transform: perspective(1000px) rotateY(0deg) scale(1 );
}
#card-header {
padding: 15px; border-bottom: 1px solid #333 ; display: flex; justify-content: space-between;
color: var (--primary); font-family: monospace; background: rgba(0 , 255 , 255 , 0.1 );
}
#card-content { flex: 1 ; overflow: hidden; position: relative; padding: 0 ; }
textarea#text-editor {
width: 100 %; height: 100 %; background: transparent; color: #e0e0e0;
border: none ; padding: 20px; font-family: monospace; font-size: 14px;
resize: none ; outline: none ; box-sizing: border-box;
}
#image-viewer-container {
width: 100 %; height: 100 %; overflow: hidden; display: flex; justify-content: center; align-items: center; cursor: grab;
}
#image-viewer { max -width: 100 %; max -height: 100 %; transition: transform 0 .1s; transform-origin: center; }
#bottom-shelf {
position: absolute; bottom: -100px; left : 10 %; width: 80 %; height: 140px;
background: linear-gradient(to top, rgba(0 , 50 , 60 , 0.9 ), rgba(0 ,0 ,0 ,0 ));
border-top: 2px solid var (--secondary);
transition: bottom 0 .4s cubic-bezier(0.2 , 1 , 0.3 , 1 );
pointer-events: auto; z-index: 20 ;
display: flex; align-items: center; justify-content: center; color: var (--secondary);
backdrop-filter : blur(10px);
}
#bottom-shelf:hover, #bottom-shelf.drag-active { bottom: 0 ; }
#shelf-handle {
position: absolute; top: -30px; left : 50 %; transform: translateX(-50 %);
width: 100px; height: 30px; background: var (--secondary);
clip-path: polygon(10 % 0 , 90 % 0 , 100 % 100 %, 0 % 100 %);
color: #000 ; text-align: center; line-height: 30px; font-weight: bold; cursor: pointer;
box-shadow: 0 -5px 15px rgba(255 , 0 , 255 , 0.4 );
}
#shelf-content { font-size: 20px; letter-spacing: 2px; text-shadow: 0 0 10px var (--secondary); pointer-events: none ;}
.scan-lines {
position: fixed; top: 0 ; left : 0 ; width: 100 %; height: 100 %; pointer-events: none ;
background: repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0 , 255 , 255 , 0.03 ) 2px, rgba(0 , 255 , 255 , 0.03 ) 3px);
z-index: 2 ;
}
</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="ui-layer" >
<div id="header" >
<button id="btn-select-root" >SELECT ROOT FOLDER </button>
<div id="path-display" >NO FOLDER SELECTED </div>
</div>
</div>
<div id="preview-overlay" >
<div id="preview-card" >
<div id="card-header" >
<span id="file-name" >FILENAME .EXT </span>
<div>
<button id="btn-save" style="display:none; font-size:10px; padding: 2px 10px;" >SAVE </button>
<button id="btn-close" style="font-size:10px; padding: 2px 10px;" >CLOSE </button>
</div>
</div>
<div id="card-content" >
</div>
</div>
</div>
<div id="bottom-shelf" >
<div id="shelf-handle" >:: TEMP ::</div>
<div id="shelf-content" >DROP FILES HERE TO MOVE TO "_TEMP_WORKSPACE" </div>
</div>
<div class ="scan -lines "></div >
<script type ="module ">
import * as THREE from 'three ';
import { OrbitControls } from 'three/addons/controls/OrbitControls .js';
import { DragControls } from 'three/addons/controls/DragControls.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';
const state = {
rootHandle: null,
currentDirHandle: null,
tempDirHandle: null,
files: [],
isDragActive: false
};
const CONFIG = {
colorFolder: 0x00ffff ,
colorFile: 0x0088aa ,
colorSelected: 0xff00ff ,
gridSize: 5
};
const scene = new THREE .Scene ();
scene.fog = new THREE .FogExp2 (0x050510 , 0.04 );
const camera = new THREE .PerspectiveCamera (60 , window.innerWidth / window.innerHeight, 0.1 , 100 );
camera.position.set (0 , 8 , 12 );
const renderer = new THREE .WebGLRenderer ({ antialias: true , alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
const renderScene = new RenderPass (scene, camera);
const bloomPass = new UnrealBloomPass (new THREE .Vector2 (window.innerWidth, window.innerHeight), 1.5 , 0.4 , 0.85 );
bloomPass.threshold = 0.2 ;
bloomPass.strength = 1.2 ;
bloomPass.radius = 0.5 ;
const composer = new EffectComposer (renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
scene.add(new THREE .AmbientLight (0x404040 , 2 ));
const grid = new THREE .GridHelper (50 , 50 , 0x004455 , 0x001122 );
scene.add(grid);
const dirLight = new THREE .DirectionalLight (0xffffff , 2 );
dirLight.position.set (5 , 10 , 7 );
scene.add(dirLight);
const controls = new OrbitControls (camera, renderer.domElement);
controls.enableDamping = true ;
const fileGroup = new THREE .Group ();
scene.add(fileGroup);
async function initFileSystem() {
try {
state.rootHandle = await window.showDirectoryPicker();
state.currentDirHandle = state.rootHandle;
state.tempDirHandle = await state.rootHandle.getDirectoryHandle('_TEMP_WORKSPACE', { create: true });
updatePathDisplay(state.rootHandle.name);
await loadFiles(state.currentDirHandle);
} catch (e) {
console.error(e);
alert("フォルダへのアクセスがキャンセルされたか、エラーが発生しました。" );
}
}
async function loadFiles(dirHandle) {
while (fileGroup.children.length > 0 ){
fileGroup.remove(fileGroup.children[0 ]);
}
state.files = [];
const entries = [];
for await (const entry of dirHandle.values()) {
if (entry.name !== '_TEMP_WORKSPACE') {
entries.push(entry);
}
}
let idx = 0 ;
const cols = Math .ceil(Math .sqrt(entries.length));
const spacing = 2.5 ;
const offset = (cols * spacing) / 2 ;
for (const entry of entries) {
const x = (idx % cols) * spacing - offset;
const z = Math .floor(idx / cols) * spacing - offset;
createFileMesh(entry, x, z);
idx++;
}
initDragControls();
}
function createFileMesh(handle, x, z) {
const isFolder = handle.kind === 'directory';
const geometry = isFolder ? new THREE .BoxGeometry (1.5 , 1.5 , 1.5 ) : new THREE .BoxGeometry (1 , 1.5 , 0.2 );
const material = new THREE .MeshPhysicalMaterial ({
color: isFolder ? CONFIG .colorFolder : CONFIG .colorFile,
metalness: 0.8 , roughness: 0.1 , transmission: 0.2 , opacity: 0.8 , transparent: true ,
wireframe: false
});
const mesh = new THREE .Mesh (geometry, material);
mesh.position.set (x, 1 , z);
const edges = new THREE .EdgesGeometry (geometry);
const line = new THREE .LineSegments (edges, new THREE .LineBasicMaterial ({ color: 0xffffff , transparent: true , opacity: 0.3 }));
mesh.add(line);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256 ; canvas.height = 64 ;
context.fillStyle = isFolder ? '#00ffff' : '#ccccff';
context.font = 'bold 24px monospace';
context.textAlign = 'center';
context.fillText(handle.name.substring(0 , 15 ), 128 , 30 );
const labelMap = new THREE .CanvasTexture (canvas);
const labelMat = new THREE .SpriteMaterial ({ map : labelMap });
const label = new THREE .Sprite (labelMat);
label.position.set (0 , -1.2 , 0 );
label.scale.set (3 , 0.75 , 1 );
mesh.add(label);
mesh.userData = { handle: handle, isFolder: isFolder, originalPos: new THREE .Vector3 (x, 1 , z) };
fileGroup.add(mesh);
state.files.push(mesh);
}
const raycaster = new THREE .Raycaster ();
const mouse = new THREE .Vector2 ();
let dragControls;
function initDragControls() {
if (dragControls) dragControls.dispose();
dragControls = new DragControls (state.files, camera, renderer.domElement);
dragControls.addEventListener('dragstart', function (event) {
controls.enabled = false ;
event.object.material.emissive.set (0xffffff );
event.object.material.emissiveIntensity = 0.5 ;
state.isDragActive = true ;
document.getElementById('bottom-shelf').classList.add('drag-active');
});
dragControls.addEventListener('dragend', async function (event) {
controls.enabled = true ;
event.object.material.emissive.set (0x000000 );
state.isDragActive = false ;
document.getElementById('bottom-shelf').classList.remove('drag-active');
const vector = event.object.position.clone();
vector.project(camera);
if (vector.y < -0.6 ) {
await moveFileToTemp(event.object.userData.handle);
fileGroup.remove(event.object);
} else {
const original = event.object.userData.originalPos;
event.object.position.copy(original);
}
});
}
async function moveFileToTemp(handle) {
try {
if (handle.move) {
await handle.move(state.tempDirHandle);
alert(`${handle.name} を _TEMP_WORKSPACE に移動しました。`);
} else {
alert("このブラウザはファイルの直接移動をサポートしていない可能性があります。" );
}
} catch (e) {
console.error("Move failed:" , e);
alert("移動に失敗しました: " + e.message);
}
}
renderer.domElement.addEventListener('click', async (e) => {
if (state.isDragActive) return ;
mouse.x = (e.clientX / window.innerWidth) * 2 - 1 ;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1 ;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(state.files);
if (intersects.length > 0 ) {
const object = intersects[0 ].object;
const handle = object.userData.handle;
if (object.userData.isFolder) {
state.currentDirHandle = handle;
updatePathDisplay(handle.name);
await loadFiles(handle);
} else {
openPreview(handle);
}
}
});
const previewOverlay = document.getElementById('preview-overlay');
const cardContent = document.getElementById('card-content');
const btnSave = document.getElementById('btn-save');
let currentFileHandle = null;
async function openPreview(handle) {
currentFileHandle = handle;
const file = await handle.getFile();
const name = file.name.toLowerCase();
document.getElementById('file-name').textContent = file.name;
cardContent.innerHTML = '';
btnSave.style.display = 'none ';
if (name.endsWith('.txt') || name.endsWith('.md') || name.endsWith('.js') || name.endsWith('.html') || name.endsWith('.json')) {
const text = await file.text();
const textarea = document.createElement('textarea');
textarea.id = 'text-editor';
textarea.value = text;
cardContent.appendChild(textarea);
btnSave.style.display = 'inline-block';
} else if (name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.gif') || name.endsWith('.webp')) {
const blob = await file.slice(0 , file.size, file.type);
const url = URL .createObjectURL(blob);
const container = document.createElement('div');
container.id = 'image-viewer-container';
const img = document.createElement('img');
img.id = 'image-viewer';
img.src = url;
container.appendChild(img);
cardContent.appendChild(container);
let scale = 1 ;
let panning = false ;
let pointX = 0 , pointY = 0 , startX = 0 , startY = 0 ;
container.onwheel = (e) => {
e.preventDefault();
scale += e.deltaY * -0.001 ;
scale = Math .min (Math .max (.125 , scale), 4 );
img.style.transform = `translate(${pointX}px, ${pointY}px) scale(${scale})`;
};
container.onmousedown = (e) => { e.preventDefault(); panning = true ; startX = e.clientX - pointX; startY = e.clientY - pointY; }
container.onmouseup = () => { panning = false ; }
container.onmousemove = (e) => {
if (!panning) return ;
e.preventDefault();
pointX = e.clientX - startX;
pointY = e.clientY - startY;
img.style.transform = `translate(${pointX}px, ${pointY}px) scale(${scale})`;
}
} else {
cardContent.innerHTML = '<div style="color:#fff; padding:20px;" >PREVIEW NOT AVAILABLE </div>';
}
previewOverlay.classList.add('active');
}
function closePreview() {
previewOverlay.classList.remove('active');
setTimeout(() => {
cardContent.innerHTML = '';
}, 300 );
}
previewOverlay.addEventListener('click', (e) => {
if (e.target === previewOverlay) closePreview();
});
document.getElementById('btn-close').addEventListener('click', closePreview);
btnSave.addEventListener('click', async () => {
const textarea = document.getElementById('text-editor');
if (textarea && currentFileHandle) {
try {
const writable = await currentFileHandle.createWritable();
await writable.write(textarea.value);
await writable.close();
const originalBg = textarea.style.background;
textarea.style.background = 'rgba(0 ,255 ,0 ,0.2 )';
setTimeout(()=> textarea.style.background = originalBg, 200 );
} catch (e) {
alert('保存できませんでした: ' + e);
}
}
});
document.getElementById('btn-select-root').addEventListener('click', initFileSystem);
function updatePathDisplay(name) {
document.getElementById('path-display').textContent = `ROOT /${name}`;
}
const shelf = document.getElementById('bottom-shelf');
shelf.addEventListener('click', () => {
});
function animate() {
requestAnimationFrame(animate);
controls.update();
state.files.forEach(mesh => {
if (!state.isDragActive) {
mesh.rotation.y += 0.005 ;
mesh.rotation.z = Math .sin(Date .now() * 0.001 + mesh.position.x) * 0.05 ;
}
});
composer.render();
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
copy
コメント