円錐形のフォルダビューワver2


画像
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Cyber Stream Navigation v2</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #050500; 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(50, 40, 0, 0.6);
            border: 1px solid #ffcc00;
            color: #ffcc00;
            padding: 12px 24px;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            text-transform: uppercase;
            letter-spacing: 2px;
            box-shadow: 0 0 15px rgba(255, 200, 0, 0.3);
            backdrop-filter: blur(4px);
            transition: all 0.2s;
        }
        button:hover { background: rgba(255, 200, 0, 0.2); box-shadow: 0 0 25px rgba(255, 200, 0, 0.8); }
        #loading { display: none; color: #ffaa00; margin-top: 15px; font-weight: bold; text-shadow: 0 0 10px orange; }
        .instructions { 
            color: rgba(255, 240, 200, 0.8); 
            font-size: 13px; 
            margin-top: 15px; 
            line-height: 1.6; 
            background: rgba(20, 15, 0, 0.8); 
            padding: 15px; 
            border-radius: 4px; 
            border-left: 3px solid #ffcc00;
        }
        .key { color: #fff; border: 1px solid #555; padding: 2px 4px; border-radius: 3px; background: #222; }
    </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">Initialize System</button>
        <div id="loading">System Scanning...</div>
        <div class="instructions">
            <b>KEYBOARD NAV:</b><br>
            <span class="key">↑</span> / <span class="key">PgUp</span> : Dive to Target (Child)<br>
            <span class="key">↓</span> / <span class="key">PgDn</span> : Ascend (Parent)<br>
            <span class="key">←</span> <span class="key">→</span> : Switch Target<br>
            <br>
            <b>MOUSE:</b><br>
            CLICK: Select / Expand<br>
            DRAG: Move & Rotate
        </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';

        // --- 設定 (Configuration) ---
        const CONFIG = {
            layerDepth: 450,
            baseRadius: 120,
            nodeVisualWidth: 80,    
            spacingMultiplier: 2.8,
            
            // 色設定
            colorMain: 0xffcc00,
            colorDim: 0x443300,
            colorTarget: 0xff3300, 
            
            // レーザー設定
            laserThickness: 3.0,
            laserColorDefault: 0x664400,
            laserColorActive: 0xffaa00,
            
            // Bloom設定
            bloomStrength: 2.5,     
            bloomRadius: 0.6,
            bloomThreshold: 0.15,    

            // カメラ
            camHoverHeight: 200,
            camFollowDist: 500,
            camLookAhead: 700
        };

        // --- グローバル変数 ---
        let scene, camera, renderer, composer;
        let worldContainer = new THREE.Group(); 
        let cursorMesh;     
        let targetMesh;     
        
        let fullTreeData = null;
        let visibleNodes = [];   
        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 selectedNodeData = null; 
        let targetChildIndex = 0;    

        // ドラッグ制御
        let isDragging = false;
        let dragAxis = null;
        let lastMouse = { x: 0, y: 0 };

        // パーティクル
        let particles = [];
        let arrowTexture;

        init();
        animate();

        function init() {
            scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x050500, 0.0004);

            camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 40000);
            
            renderer = new THREE.WebGLRenderer({ antialias: false });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            document.body.appendChild(renderer.domElement);

            // Bloom
            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(50000, 400, 0x332200, 0x110d00);
            gridHelper.position.y = -1500;
            scene.add(gridHelper);

            scene.add(worldContainer);
            
            createCursors();
            createArrowTexture();

            // イベント
            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);
            document.addEventListener('dblclick', onMouseDoubleClick);
            document.addEventListener('keydown', onKeyDown);
        }

        // --- アセット生成 ---
        function createCursors() {
            const geo1 = new THREE.TorusGeometry(60, 2, 16, 100);
            const mat1 = new THREE.MeshBasicMaterial({ color: CONFIG.colorMain, transparent: true, opacity: 0.9 });
            cursorMesh = new THREE.Mesh(geo1, mat1);
            cursorMesh.visible = false;
            scene.add(cursorMesh);

            const geo2 = new THREE.TorusGeometry(30, 2, 8, 50); 
            const mat2 = new THREE.MeshBasicMaterial({ color: CONFIG.colorTarget, transparent: true, opacity: 0.8 });
            targetMesh = new THREE.Mesh(geo2, mat2);
            targetMesh.visible = false;
            scene.add(targetMesh); 
        }

        function createArrowTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 64;
            canvas.height = 64;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = '#ffaa00';
            ctx.beginPath();
            ctx.moveTo(32, 10);
            ctx.lineTo(54, 54);
            ctx.lineTo(10, 54);
            ctx.closePath();
            ctx.fill();
            arrowTexture = new THREE.CanvasTexture(canvas);
        }

        // --- パーティクルシステム ---
        function spawnArrow(startPos, endPos) {
            const material = new THREE.SpriteMaterial({ 
                map: arrowTexture, 
                color: 0xffffff, 
                transparent: true, 
                opacity: 1.0,
                depthTest: false 
            });
            const sprite = new THREE.Sprite(material);
            sprite.scale.set(15, 15, 1);
            
            const p = {
                mesh: sprite,
                start: startPos.clone(),
                end: endPos.clone(),
                progress: 0,
                speed: 0.03 + Math.random() * 0.01 
            };
            
            sprite.position.copy(startPos);
            scene.add(sprite); 
            particles.push(p);
        }

        function updateParticles() {
            if (selectedNodeData && selectedNodeData.children && selectedNodeData.children.length > 0) {
                const targetNode = selectedNodeData.children[targetChildIndex];
                if (selectedNodeData.isExpanded && targetNode && targetNode.visualObj && selectedNodeData.visualObj) {
                    const startPos = new THREE.Vector3();
                    const endPos = new THREE.Vector3();
                    selectedNodeData.visualObj.getWorldPosition(startPos);
                    targetNode.visualObj.getWorldPosition(endPos);

                    if (Math.random() < 0.2) spawnArrow(startPos, endPos);
                }
            }

            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.progress += p.speed;
                if (p.progress >= 1.0) {
                    scene.remove(p.mesh);
                    particles.splice(i, 1);
                } else {
                    p.mesh.position.lerpVectors(p.start, p.end, p.progress);
                    if (p.progress > 0.8) p.mesh.material.opacity = (1.0 - p.progress) * 5;
                }
            }
        }

        // --- キーボード操作ロジック ---
        function onKeyDown(e) {
            if (!selectedNodeData) return;

            if (e.key === 'ArrowUp' || e.key === 'PageUp') {
                e.preventDefault();
                navigateDownToChild();
            }
            else if (e.key === 'ArrowDown' || e.key === 'PageDown') {
                e.preventDefault();
                navigateUpToParent();
            }
            else if (e.key === 'ArrowLeft') {
                e.preventDefault();
                // 逆転修正: -1 ではなく 1
                changeTarget(1);
            }
            else if (e.key === 'ArrowRight') {
                e.preventDefault();
                // 逆転修正: 1 ではなく -1
                changeTarget(-1);
            }
        }

        function navigateDownToChild() {
            if (!selectedNodeData.children || selectedNodeData.children.length === 0) return;
            if (!selectedNodeData.isExpanded) {
                selectedNodeData.isExpanded = true;
                refreshTree();
                return;
            }
            const nextNode = selectedNodeData.children[targetChildIndex];
            if (nextNode) {
                selectNode(nextNode);
                targetChildIndex = 0; 
            }
        }

        function navigateUpToParent() {
            if (selectedNodeData.parent) {
                const current = selectedNodeData;
                const parent = selectedNodeData.parent;
                const myIndex = parent.children.indexOf(current);
                selectNode(parent);
                targetChildIndex = Math.max(0, myIndex); 
            }
        }

        function changeTarget(dir) {
            if (!selectedNodeData.children || selectedNodeData.children.length === 0) return;
            const len = selectedNodeData.children.length;
            targetChildIndex = (targetChildIndex + dir + len) % len; 
        }

        function selectNode(node) {
            selectedNodeData = node;
            if(node.children && node.children.length > 0 && !node.isExpanded) {
                node.isExpanded = true; 
                refreshTree();
            } else {
                updateHighlights(); 
            }
            focusNode(node);
        }

        // --- フォルダ処理 ---
        async function handleFolderSelect() {
            try {
                const dirHandle = await window.showDirectoryPicker();
                document.getElementById('loading').style.display = 'block';
                fullTreeData = await scanDirectory(dirHandle);
                fullTreeData.isExpanded = true; 
                selectNode(fullTreeData);
                refreshTree();
                document.getElementById('loading').style.display = 'none';
            } catch (err) {
                console.error(err);
                document.getElementById('loading').innerText = "Aborted";
            }
        }

        async function scanDirectory(handle, depth = 0, parent = null) {
            const node = {
                name: handle.name,
                kind: handle.kind,
                children: [],
                depth: depth,
                parent: parent,
                isExpanded: false, 
                requiredArc: 0,
                angle: 0, x: 0, y: 0, z: 0,
                visualObj: null,
                visualText: null, // テキストスプライト参照用
                visualLaser: null,
                scaleMultiplier: 1.0 // アニメーション用スケール値
            };
            if (depth > 15) 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 refreshTree() {
            while(worldContainer.children.length > 0){ 
                worldContainer.remove(worldContainer.children[0]); 
            }
            visibleNodes = []; 
            adjustConeSize(fullTreeData);
            calculateLayout(fullTreeData);
            renderTree(fullTreeData);
            updateHighlights();
        }

        function adjustConeSize(rootNode) {
            const countsPerDepth = {};
            function countVisible(node) {
                if(!countsPerDepth[node.depth]) countsPerDepth[node.depth] = 0;
                countsPerDepth[node.depth]++;
                if(node.isExpanded && node.children) {
                    node.children.forEach(countVisible);
                }
            }
            countVisible(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.25) slope = 0.25;
                calculatedSlope = slope;
            } else {
                calculatedSlope = 0.6;
            }
        }

        function getConeRadius(z) {
            return CONFIG.baseRadius + Math.abs(z) * calculatedSlope;
        }

        function calculateLayout(node) {
            function calcRequiredArc(n) {
                const selfArc = CONFIG.nodeVisualWidth * CONFIG.spacingMultiplier;
                if (!n.isExpanded || !n.children || n.children.length === 0) {
                    n.requiredArc = selfArc;
                } else {
                    let childrenSum = 0;
                    n.children.forEach(child => {
                        calcRequiredArc(child);
                        childrenSum += child.requiredArc;
                    });
                    n.requiredArc = Math.max(selfArc, childrenSum);
                }
            }
            calcRequiredArc(node);

            function assignPositions(n, startAngle, angleRange) {
                n.z = -n.depth * CONFIG.layerDepth;
                const radius = getConeRadius(n.z);
                const myAngle = startAngle + (angleRange / 2);
                n.angle = myAngle;
                n.x = Math.cos(myAngle) * radius;
                n.y = Math.sin(myAngle) * radius;

                if (!n.isExpanded || !n.children || n.children.length === 0) return;

                const totalChildrenArc = n.children.reduce((sum, c) => sum + c.requiredArc, 0);
                let currentStart = startAngle;
                n.children.forEach(child => {
                    const ratio = child.requiredArc / totalChildrenArc;
                    const childAngleRange = angleRange * ratio;
                    assignPositions(child, currentStart, childAngleRange);
                    currentStart += childAngleRange;
                });
            }

            node.z = 0; node.x = 0; node.y = 0; node.angle = -Math.PI/2;
            if(node.isExpanded && node.children.length > 0) {
                const totalArc = node.children.reduce((sum,c)=>sum+c.requiredArc, 0);
                let currentStart = 0;
                node.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.isExpanded && node.children) {
                node.children.forEach(renderTree);
            }
        }

        // --- ビジュアル生成 (改善版) ---
        function createNodeVisual(node) {
            const hasChildren = node.children && node.children.length > 0;
            const group = new THREE.Group();
            group.position.set(node.x, node.y, node.z);
            
            // --- テキスト生成 (高画質・黒縁) ---
            const w = CONFIG.nodeVisualWidth;
            const h = 40;
            const scale = 6; // 解像度アップ (ぼやけ防止)
            
            const canvas = document.createElement('canvas');
            canvas.width = w * scale;
            canvas.height = h * scale;
            const ctx = canvas.getContext('2d');

            const fontSize = 14 * scale;
            ctx.font = `bold ${fontSize}px "Segoe UI", Arial`;
            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);
            
            // 黒縁 (Stroke)
            ctx.strokeStyle = '#000000';
            ctx.lineWidth = 4 * (scale / 2); // 解像度に合わせて太く
            ctx.strokeText(node.name, 0, 0);
            
            // 白文字 (Fill)
            ctx.fillStyle = '#ffffff';
            ctx.fillText(node.name, 0, 0);
            
            ctx.restore();

            const texture = new THREE.CanvasTexture(canvas);
            texture.minFilter = THREE.LinearFilter;
            // 異方性フィルタリングを有効化(斜めから見たときの鮮明化)
            const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
            texture.anisotropy = maxAnisotropy;

            const textMat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false });
            const textSprite = new THREE.Sprite(textMat);
            textSprite.scale.set(w, h, 1);
            
            node.visualText = textSprite; // 参照保持(拡大用)

            let mainMesh;
            if (hasChildren) {
                const boxGeo = new THREE.BoxGeometry(w, w, w);
                const edges = new THREE.EdgesGeometry(boxGeo);
                const lineMat = new THREE.LineBasicMaterial({ color: CONFIG.colorMain });
                const cube = new THREE.LineSegments(edges, lineMat);
                
                textSprite.position.set(0, 0, 0); 
                group.add(cube);
                group.add(textSprite);
                
                group.userData.isCube = true;
                group.userData.cubeMesh = cube;

                const hitBox = new THREE.Mesh(boxGeo, new THREE.MeshBasicMaterial({ visible: false }));
                group.add(hitBox);
                hitBox.userData.node = node;
                visibleNodes.push(hitBox); 
                mainMesh = group;

            } else {
                // 背景ありプレート (高解像度化はしないが視認性のため)
                const plateCanvas = document.createElement('canvas');
                plateCanvas.width = w * 2;
                plateCanvas.height = h * 2;
                const pCtx = plateCanvas.getContext('2d');
                pCtx.fillStyle = 'rgba(20, 15, 0, 0.8)'; 
                pCtx.fillRect(0,0,plateCanvas.width, plateCanvas.height);
                pCtx.strokeStyle = '#aa8800'; 
                pCtx.lineWidth = 4;
                pCtx.strokeRect(0,0,plateCanvas.width, plateCanvas.height);
                
                const plateTex = new THREE.CanvasTexture(plateCanvas);
                const plateMat = new THREE.SpriteMaterial({ map: plateTex, transparent: true });
                const bgSprite = new THREE.Sprite(plateMat);
                bgSprite.scale.set(w, h, 1);
                
                group.add(bgSprite);
                textSprite.position.z = 1;
                group.add(textSprite);

                mainMesh = bgSprite;
                bgSprite.userData.node = node;
                visibleNodes.push(bgSprite);
            }

            worldContainer.add(group);
            node.visualObj = group;

            if (node.parent) {
                createLaserConnection(node.parent, node);
            }
        }

        function createLaserConnection(pNode, cNode) {
            const path = new THREE.LineCurve3(
                new THREE.Vector3(pNode.x, pNode.y, pNode.z),
                new THREE.Vector3(cNode.x, cNode.y, cNode.z)
            );
            const geometry = new THREE.TubeGeometry(path, 1, CONFIG.laserThickness, 6, false);
            const material = new THREE.MeshBasicMaterial({ 
                color: CONFIG.laserColorDefault,
                transparent: true,
                opacity: 0.4
            });
            const mesh = new THREE.Mesh(geometry, material);
            worldContainer.add(mesh);
            cNode.visualLaser = mesh;
        }

        // --- ハイライト & テキスト拡大 ---
        function updateHighlights() {
            visibleNodes.forEach(obj => {
                const node = obj.userData.node;
                if(node.visualLaser) {
                    node.visualLaser.material.color.setHex(CONFIG.laserColorDefault);
                    node.visualLaser.material.opacity = 0.3;
                }
            });

            if (!selectedNodeData) return;

            // パスハイライト
            let cursor = selectedNodeData;
            while(cursor) {
                if (cursor.visualLaser) {
                    cursor.visualLaser.material.color.setHex(CONFIG.laserColorActive);
                    cursor.visualLaser.material.opacity = 1.0; 
                }
                cursor = cursor.parent;
            }
        }

        // --- テキストのスケーリング更新 ---
        function updateTextScales() {
            visibleNodes.forEach(obj => {
                const node = obj.userData.node;
                if (!node.visualText) return;

                let targetScale = 1.0;

                // 条件1: 現在選択中のフォルダ
                if (node === selectedNodeData) {
                    targetScale = 1.8;
                } 
                // 条件2: ターゲット中の子フォルダ(展開されている場合)
                else if (selectedNodeData && selectedNodeData.isExpanded && selectedNodeData.children) {
                    const targetChild = selectedNodeData.children[targetChildIndex];
                    if (targetChild === node) {
                        targetScale = 1.5;
                    }
                }

                // 線形補間でスムーズにアニメーション
                node.scaleMultiplier += (targetScale - node.scaleMultiplier) * 0.1;

                // ベースサイズ(w, h) に multiplier を掛ける
                const w = CONFIG.nodeVisualWidth;
                const h = 40;
                node.visualText.scale.set(
                    w * node.scaleMultiplier, 
                    h * node.scaleMultiplier, 
                    1
                );
            });
        }

        function updateCursors() {
            if (selectedNodeData && selectedNodeData.visualObj) {
                cursorMesh.visible = true;
                const targetPos = new THREE.Vector3();
                selectedNodeData.visualObj.getWorldPosition(targetPos);

                new TWEEN.Tween(cursorMesh.position)
                    .to({ x: targetPos.x, y: targetPos.y, z: targetPos.z }, 300)
                    .easing(TWEEN.Easing.Cubic.Out)
                    .start();
            } else {
                cursorMesh.visible = false;
            }

            if (selectedNodeData && selectedNodeData.children && selectedNodeData.children.length > 0) {
                const targetNode = selectedNodeData.children[targetChildIndex];
                if (targetNode && targetNode.visualObj && selectedNodeData.isExpanded) {
                    targetMesh.visible = true;
                    const tPos = new THREE.Vector3();
                    targetNode.visualObj.getWorldPosition(tPos);

                    new TWEEN.Tween(targetMesh.position)
                        .to({ x: tPos.x, y: tPos.y, z: tPos.z }, 200)
                        .start();
                } else {
                    targetMesh.visible = false;
                }
            } else {
                targetMesh.visible = false;
            }
        }

        // --- マウス操作 ---
        let clickTimeout = null;
        function onMouseClick(e) {
            if (isDragging) return;
            if (clickTimeout !== null) {
                clearTimeout(clickTimeout);
                clickTimeout = null;
                return;
            }
            clickTimeout = setTimeout(() => {
                const node = getIntersectedNode();
                if (node) selectNode(node);
                clickTimeout = null;
            }, 250);
        }

        function onMouseDoubleClick(e) {
            if (clickTimeout !== null) {
                clearTimeout(clickTimeout);
                clickTimeout = null;
            }
            const node = getIntersectedNode();
            if (node) {
                if (node.children && node.children.length > 0) {
                    node.isExpanded = !node.isExpanded;
                    refreshTree();
                }
                selectNode(node);
            }
        }

        function getIntersectedNode() {
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(visibleNodes);
            if (intersects.length > 0) return intersects[0].object.userData.node;
            return null;
        }

        function onMouseDown(e) {
            isDragging = true;
            dragAxis = null; 
            lastMouse.x = e.clientX;
            lastMouse.y = e.clientY;
        }
        function onMouseUp() { setTimeout(() => { isDragging = false; }, 50); }
        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;
                if (!dragAxis) {
                    dragAxis = (Math.abs(dx) > Math.abs(dy)) ? 'x' : 'y';
                }
                if (dragAxis === 'y') {
                    targetFocusZ += dy * 4.0; 
                    if(targetFocusZ > 0) targetFocusZ = 0;
                } else if (dragAxis === 'x') {
                    targetRotation += dx * 0.005;
                }
                lastMouse.x = e.clientX;
                lastMouse.y = e.clientY;
            }
        }

        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 focusNode(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 }, 800)
                .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;
            
            worldContainer.children.forEach(child => {
                if(child.userData.isCube) {
                    child.userData.cubeMesh.rotation.x += 0.01;
                    child.userData.cubeMesh.rotation.y += 0.02;
                }
            });

            updateTextScales(); // テキストサイズ更新
            updateCursors();
            
            if (cursorMesh.visible) {
                const s = 1.0 + Math.sin(Date.now() * 0.005) * 0.1;
                cursorMesh.scale.set(s, s, s);
                const worldCameraPos = new THREE.Vector3();
                camera.getWorldPosition(worldCameraPos);
                cursorMesh.lookAt(worldCameraPos);
            }
            if (targetMesh.visible) {
                const worldCameraPos = new THREE.Vector3();
                camera.getWorldPosition(worldCameraPos);
                targetMesh.lookAt(worldCameraPos);
                targetMesh.rotateZ(Date.now() * 0.005);
            }

            updateParticles();
            updateCamera();
            composer.render();
        }
    </script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
円錐形のフォルダビューワver2|古井和雄
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