<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Cone Tree Visualizer</title>
<style>
body { margin: 0 ; overflow: hidden; background-color: #000; 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 , 255 , 255 , 0.1 );
border: 1 px solid #00ffff;
color: #00ffff;
padding: 10 px 20 px;
font-size: 14 px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2 px;
transition: all 0.3 s;
box-shadow: 0 0 10 px rgba (0 , 255 , 255 , 0.2 ) ;
}
button:hover { background: rgba(0 , 255 , 255 , 0.3 ); box-shadow: 0 0 20 px rgba (0 , 255 , 255 , 0.6 ) ; }
#loading { display: none; color: #00ffff; margin-top: 10px; text-shadow: 0 0 5px #00ffff; }
.instructions { color: rgba(255 ,255 ,255 ,0.5 ); font-size: 12 px; margin-top: 10 px; line-height: 1.5 ; }
</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 Folder</button>
<div id="loading" >Scanning Directory...</div>
<div class ="instructions" >
DRAG UP/DOWN: Move In/Out<br>
DRAG LEFT/RIGHT: Rotate Cone<br>
CLICK: Focus Node
</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 = {
layerHeight: 300 ,
baseRadius: 100 ,
nodeColor: 0x00ffff ,
lineColor: 0x0088ff ,
bloomStrength: 1.5 ,
bloomRadius: 0.4 ,
bloomThreshold: 0 ,
textScale: 30
};
let scene, camera, renderer, composer;
let rootNodeVisuals = new THREE.Group();
let nodes = [];
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let cameraZ = 200 ;
let coneRotation = 0 ;
let isDragging = false ;
let previousMousePosition = { x: 0 , y: 0 };
let targetCameraZ = 200 ;
let targetRotation = 0 ;
init();
animate();
function init ( ) {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000 , 0.0008 );
camera = new THREE.PerspectiveCamera(60 , window.innerWidth / window.innerHeight, 1 , 10000 );
camera.position.set (0 , 0 , cameraZ);
camera.lookAt(0 , 0 , -1000 );
renderer = new THREE.WebGLRenderer({ antialias: false });
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 = CONFIG.bloomThreshold;
bloomPass.strength = CONFIG.bloomStrength;
bloomPass.radius = CONFIG.bloomRadius;
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
scene.add (rootNodeVisuals);
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 (rootNodeVisuals.children.length > 0 ){
rootNodeVisuals.remove (rootNodeVisuals.children[0 ]);
}
nodes = [];
targetCameraZ = 200 ;
targetRotation = 0 ;
camera.position.z = 200 ;
rootNodeVisuals.rotation.z = 0 ;
const treeData = await scanDirectory(dirHandle);
buildConeTree(treeData);
document.getElementById('loading' ).style.display = 'none' ;
} catch (err) {
console.error(err);
document.getElementById('loading' ).innerText = "Canceled or Error" ;
}
}
async function scanDirectory (handle, depth = 0 ) {
const node = {
name: handle.name,
kind: handle.kind,
children: [],
depth: depth
};
if (depth > 5 ) return node;
if (handle.kind === 'directory' ) {
for await (const entry of handle.values( )) {
node.children.push(await scanDirectory(entry, depth + 1 ));
}
}
return node;
}
function calculateSubtreeWeight (node ) {
if (!node.children || node.children.length === 0 ) {
node.weight = 1 ;
} else {
node.weight = node.children.reduce((sum, child) => sum + calculateSubtreeWeight(child), 0 );
}
return node.weight;
}
function buildConeTree (rootData ) {
calculateSubtreeWeight(rootData);
createNodeVisual(rootData, 0 , 0 , 0 , 0 );
layoutChildren(rootData, 0 , 0 , Math.PI * 2 , 0 );
}
function layoutChildren (parentNode, parentX, parentY, angleRange, startAngle ) {
if (!parentNode.children || parentNode.children.length === 0 ) return ;
const currentDepth = parentNode.depth + 1 ;
const z = -currentDepth * CONFIG.layerHeight;
const radius = currentDepth * CONFIG.baseRadius;
let currentStartAngle = startAngle;
parentNode.children.forEach(child => {
const childAngleRange = (child.weight / parentNode.weight) * angleRange;
const midAngle = currentStartAngle + (childAngleRange / 2 );
const x = Math.cos(midAngle) * radius;
const y = Math.sin(midAngle) * radius;
const childVisual = createNodeVisual(child, x, y, z, midAngle);
createConnection(parentX, parentY, -(currentDepth -1 ) * CONFIG.layerHeight, x, y, z);
layoutChildren(child, x, y, childAngleRange, currentStartAngle);
currentStartAngle += childAngleRange;
});
}
function createNodeVisual (node, x, y, z, angle ) {
const canvas = document.createElement('canvas' );
const ctx = canvas.getContext('2d' );
const fontSize = 40 ;
ctx.font = `Bold ${fontSize}px Arial`;
const textWidth = ctx.measureText(node.name).width;
canvas.width = textWidth + 20 ;
canvas.height = fontSize + 20 ;
ctx.shadowColor = "#00ffff" ;
ctx.shadowBlur = 10 ;
ctx.fillStyle = node.kind === 'directory' ? "#ffffff" : "#aaaaaa" ;
ctx.font = `Bold ${fontSize}px Arial`;
ctx.fillText(node.name, 10 , fontSize);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
sprite.position.set (x, y, z);
sprite.scale.set (canvas.width / 2 , canvas.height / 2 , 1 );
sprite.userData = {
node: node,
targetZ: -z + 200 ,
angle: angle
};
rootNodeVisuals.add (sprite);
nodes.push(sprite);
return sprite;
}
function createConnection (x1, y1, z1, x2, y2, z2 ) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(x1, y1, z1),
new THREE.Vector3(x2, y2, z2)
]);
const material = new THREE.LineBasicMaterial({
color: CONFIG.lineColor,
transparent: true ,
opacity: 0.6
});
const line = new THREE.Line(geometry, material);
rootNodeVisuals.add (line);
}
function onMouseDown (event ) {
isDragging = true ;
previousMousePosition = { x: event .clientX, y: event .clientY };
}
function onMouseUp ( ) { isDragging = false ; }
function onMouseMove (event ) {
mouse.x = (event .clientX / window.innerWidth) * 2 - 1 ;
mouse.y = -(event .clientY / window.innerHeight) * 2 + 1 ;
if (isDragging) {
const deltaX = event .clientX - previousMousePosition.x;
const deltaY = event .clientY - previousMousePosition.y;
targetCameraZ += deltaY * 2 ;
targetRotation += deltaX * 0.005 ;
previousMousePosition = { x: event .clientX, y: event .clientY };
}
}
function onMouseClick (event ) {
if (isDragging) return ;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodes);
if (intersects.length > 0 ) {
const target = intersects[0 ].object ;
focusNode(target);
}
}
function focusNode (sprite ) {
const nodeAngle = sprite.userData.angle;
const targetRot = -nodeAngle - (Math.PI / 2 );
let currentRot = rootNodeVisuals.rotation.z;
new TWEEN.Tween(rootNodeVisuals.rotation)
.to({ z: targetRot }, 1000 )
.easing(TWEEN.Easing.Exponential.Out)
.start();
targetRotation = targetRot;
}
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();
camera.position.z += (targetCameraZ - camera.position.z) * 0.1 ;
if (!TWEEN.getAll().length) {
rootNodeVisuals.rotation.z += (targetRotation - rootNodeVisuals.rotation.z) * 0.1 ;
} else {
targetRotation = rootNodeVisuals.rotation.z;
}
composer.render();
}
</script>
</body>
</html>
copy
コメント