<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Neon Cone Tree v3</title>
<style>
body { margin: 0 ; overflow: hidden; background-color: #000205; font-family: 'Segoe UI', sans-serif; }
#ui-layer { position: absolute; top: 20px; left: 20px; z-index: 10; pointer-events: none; }
button {
pointer-events: auto;
background: rgba(0 , 100 , 100 , 0.2 );
border: 1 px solid #00aaaa;
color: #00ffff;
padding: 12 px 24 px;
font-size: 14 px;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2 px;
backdrop-filter: blur(4 px);
transition: all 0.3 s;
}
button:hover { background: rgba(0 , 255 , 255 , 0.3 ); border-color: #00ffff; box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); }
#loading { display: none; color: #00ffff; margin-top: 15px; font-weight: bold; text-shadow: 0 0 5px #00ffff; }
.instructions {
color: rgba(200 ,220 ,255 ,0.7 );
font-size: 13 px;
margin-top: 15 px;
line-height: 1.6 ;
background: rgba(0 ,0 ,0 ,0.8 );
padding: 15 px;
border-radius: 4 px;
border-left: 3 px solid #00aaaa;
}
</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" >
<button id="selectFolderBtn" >Select Root Folder</button>
<div id="loading" >Calculating Topology...</div>
<div class ="instructions" >
<b>NAVIGATION:</b><br>
DRAG UP/DOWN: Glide Forward/Backward<br>
DRAG LEFT/RIGHT: Rotate Cone<br>
CLICK: <b>Center Folder</b>
</div>
</div>
<script type="module" >
import * as THREE from 'three' ;
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 TWEEN from 'three/addons/libs/tween.module.js' ;
const CONFIG = {
layerDepth: 400 ,
baseRadius: 150 ,
nodeVisualWidth: 80 ,
spacingMultiplier: 2.2 ,
colorLine: 0x00ffff ,
colorBoxBg: 'rgba(0, 10, 20, 0.85)' ,
colorBoxBorder: '#004455' ,
colorText: '#aaaaaa' ,
bloomStrength: 2.5 ,
bloomThreshold: 0.6 ,
bloomRadius: 0.8 ,
camHoverHeight: 180 ,
camFollowDist: 400 ,
camLookAhead: 600
};
let scene, camera, renderer, composer;
let worldContainer = new THREE.Group();
let nodes = [];
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let calculatedSlope = 0.5 ;
let focusPointZ = 0 ;
let targetFocusZ = 0 ;
let currentRotation = 0 ;
let targetRotation = 0 ;
let isDragging = false ;
let lastMouse = { x: 0 , y: 0 };
init();
animate();
function init ( ) {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000205 , 0.0005 );
camera = new THREE.PerspectiveCamera(50 , window.innerWidth / window.innerHeight, 1 , 30000 );
renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2 ));
document.body.appendChild(renderer.domElement);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
CONFIG.bloomStrength,
CONFIG.bloomRadius,
CONFIG.bloomThreshold
);
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
const gridHelper = new THREE.GridHelper(40000 , 400 , 0x112233 , 0x050510 );
gridHelper.position.y = -1200 ;
scene.add (gridHelper);
scene.add (worldContainer);
window.addEventListener('resize' , onWindowResize);
document.getElementById('selectFolderBtn' ).addEventListener('click' , handleFolderSelect);
document.addEventListener('mousedown' , onMouseDown);
document.addEventListener('mousemove' , onMouseMove);
document.addEventListener('mouseup' , onMouseUp);
document.addEventListener('click' , onMouseClick);
}
async function handleFolderSelect ( ) {
try {
const dirHandle = await window.showDirectoryPicker();
document.getElementById('loading' ).style.display = 'block' ;
while (worldContainer.children.length > 0 ){
worldContainer.remove (worldContainer.children[0 ]);
}
nodes = [];
focusPointZ = 0 ;
targetFocusZ = 0 ;
targetRotation = 0 ;
currentRotation = 0 ;
worldContainer.rotation.z = 0 ;
const treeRoot = await scanDirectory(dirHandle);
adjustConeSize(treeRoot);
calculateLayout(treeRoot);
renderTree(treeRoot);
document.getElementById('loading' ).style.display = 'none' ;
} catch (err) {
console.error(err);
document.getElementById('loading' ).innerText = "Aborted / Error" ;
}
}
async function scanDirectory (handle, depth = 0 , parent = null ) {
const node = {
name: handle.name,
kind: handle.kind,
children: [],
depth: depth,
parent: parent,
requiredArc: 0 ,
angle: 0 ,
x: 0 , y: 0 , z: 0
};
if (depth > 8 ) return node;
if (handle.kind === 'directory' ) {
for await (const entry of handle.values( )) {
node.children.push(await scanDirectory(entry, depth + 1 , node));
}
}
return node;
}
function adjustConeSize (rootNode ) {
const countsPerDepth = {};
let maxDepth = 0 ;
function count (node ) {
if (!countsPerDepth[node.depth]) countsPerDepth[node.depth] = 0 ;
countsPerDepth[node.depth]++;
if (node.depth > maxDepth) maxDepth = node.depth;
node.children.forEach(count);
}
count(rootNode);
let maxNodesInLayer = 0 ;
let busiestDepth = 0 ;
for (const d in countsPerDepth) {
if (countsPerDepth[d] > maxNodesInLayer) {
maxNodesInLayer = countsPerDepth[d];
busiestDepth = parseInt(d);
}
}
const requiredCircumference = maxNodesInLayer * (CONFIG.nodeVisualWidth * CONFIG.spacingMultiplier);
const requiredRadius = requiredCircumference / (2 * Math.PI);
if (busiestDepth > 0 ) {
let slope = (requiredRadius - CONFIG.baseRadius) / (busiestDepth * CONFIG.layerDepth);
if (slope < 0.2 ) slope = 0.2 ;
calculatedSlope = slope;
} else {
calculatedSlope = 0.4 ;
}
console.log(`Auto-adjusted Cone Slope: ${calculatedSlope}, Max Nodes: ${maxNodesInLayer} at depth ${busiestDepth}`);
}
function getConeRadius (z ) {
return CONFIG.baseRadius + Math.abs(z) * calculatedSlope;
}
function calculateLayout (rootNode ) {
function calcRequiredArc (node ) {
const selfArc = CONFIG.nodeVisualWidth * CONFIG.spacingMultiplier;
if (!node.children || node.children.length === 0 ) {
node.requiredArc = selfArc;
} else {
let childrenSum = 0 ;
node.children.forEach(child => {
calcRequiredArc(child);
childrenSum += child.requiredArc;
});
node.requiredArc = Math.max(selfArc, childrenSum);
}
}
calcRequiredArc(rootNode);
function assignPositions (node, startAngle, angleRange ) {
node.z = -node.depth * CONFIG.layerDepth;
const radius = getConeRadius(node.z);
const myAngle = startAngle + (angleRange / 2 );
node.angle = myAngle;
node.x = Math.cos(myAngle) * radius;
node.y = Math.sin(myAngle) * radius;
if (!node.children || node.children.length === 0 ) return ;
const totalChildrenArc = node.children.reduce((sum, c) => sum + c.requiredArc, 0 );
let currentStart = startAngle;
node.children.forEach(child => {
const ratio = child.requiredArc / totalChildrenArc;
const childAngleRange = angleRange * ratio;
assignPositions(child, currentStart, childAngleRange);
currentStart += childAngleRange;
});
}
rootNode.z = 0 ;
rootNode.x = 0 ;
rootNode.y = 0 ;
rootNode.angle = -Math.PI/2 ;
if (rootNode.children.length > 0 ) {
const totalArc = rootNode.children.reduce((sum,c)=>sum+c.requiredArc, 0 );
let currentStart = 0 ;
rootNode.children.forEach(child => {
const ratio = child.requiredArc / totalArc;
const range = Math.PI * 2 * ratio;
assignPositions(child, currentStart, range);
currentStart += range;
});
}
}
function renderTree (node ) {
createNodeVisual(node);
if (node.children) node.children.forEach(renderTree);
}
function createNodeVisual (node ) {
const w = CONFIG.nodeVisualWidth;
const h = 40 ;
const canvas = document.createElement('canvas' );
const scale = 2 ;
canvas.width = w * scale;
canvas.height = h * scale;
const ctx = canvas.getContext('2d' );
ctx.fillStyle = CONFIG.colorBoxBg;
ctx.fillRect(0 ,0 ,canvas.width, canvas.height);
ctx.strokeStyle = CONFIG.colorBoxBorder;
ctx.lineWidth = 4 ;
ctx.strokeRect(0 ,0 ,canvas.width, canvas.height);
const fontSize = 14 * scale;
ctx.font = `bold ${fontSize}px "Segoe UI" , Arial`;
ctx.fillStyle = CONFIG.colorText;
ctx.textAlign = 'center' ;
ctx.textBaseline = 'middle' ;
const textMetrics = ctx.measureText(node.name);
const maxWidth = canvas.width * 0.9 ;
ctx.save();
ctx.translate(canvas.width/2 , canvas.height/2 );
if (textMetrics.width > maxWidth) ctx.scale(maxWidth / textMetrics.width, 1 );
ctx.fillText(node.name, 0 , 0 );
ctx.restore();
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true ,
depthWrite: false
});
const sprite = new THREE.Sprite(material);
sprite.position.set (node.x, node.y, node.z);
sprite.scale.set (w, h, 1 );
sprite.userData = { node: node };
worldContainer.add (sprite);
nodes.push(sprite);
if (node.parent) {
createConnection(node.parent, node);
}
}
function createConnection (pNode, cNode ) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(pNode.x, pNode.y, pNode.z),
new THREE.Vector3(cNode.x, cNode.y, cNode.z)
]);
const material = new THREE.LineBasicMaterial({
color: CONFIG.colorLine,
transparent: true ,
opacity: 0.9
});
const line = new THREE.Line(geometry, material);
worldContainer.add (line);
}
function updateCamera ( ) {
const camZ = focusPointZ + CONFIG.camFollowDist;
const lookZ = focusPointZ - CONFIG.camLookAhead;
const camRadius = getConeRadius(camZ);
const lookRadius = getConeRadius(lookZ);
camera.position.set (0 , camRadius + CONFIG.camHoverHeight, camZ);
camera.lookAt(0 , lookRadius, lookZ);
}
function onMouseDown (e ) {
isDragging = true ;
lastMouse.x = e.clientX;
lastMouse.y = e.clientY;
}
function onMouseUp ( ) { isDragging = false ; }
function onMouseMove (e ) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1 ;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1 ;
if (isDragging) {
const dx = e.clientX - lastMouse.x;
const dy = e.clientY - lastMouse.y;
targetFocusZ += dy * 3.0 ;
if (targetFocusZ > 0 ) targetFocusZ = 0 ;
targetRotation += dx * 0.005 ;
lastMouse.x = e.clientX;
lastMouse.y = e.clientY;
}
}
function onMouseClick (e ) {
if (isDragging) return ;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodes);
if (intersects.length > 0 ) focusNode(intersects[0 ].object );
}
function focusNode (sprite ) {
const node = sprite.userData.node;
const targetZ = node.z + CONFIG.camLookAhead;
const targetRot = (Math.PI / 2 ) - node.angle;
new TWEEN.Tween({ z: targetFocusZ, rot: targetRotation })
.to({ z: targetZ, rot: targetRot }, 1000 )
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate((obj) => {
targetFocusZ = obj.z;
targetRotation = obj.rot;
})
.start();
}
function onWindowResize ( ) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function animate ( ) {
requestAnimationFrame(animate);
TWEEN.update();
focusPointZ += (targetFocusZ - focusPointZ) * 0.1 ;
currentRotation += (targetRotation - currentRotation) * 0.1 ;
worldContainer.rotation.z = currentRotation;
updateCamera();
composer.render();
}
</script>
</body>
</html>
copy
コメント