🔬 AI発明ギャラリー momoka2 清流Aquarium
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>清流の魚とカニの3D水族館</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #f0f4f8; 
            font-family: 'Helvetica Neue', Arial, sans-serif;
            touch-action: none; /* ブラウザのデフォルトのタッチ操作(スクロール等)を抑制 */
        }
        #ui {
            position: absolute;
            top: env(safe-area-inset-top, 20px);
            left: env(safe-area-inset-left, 20px);
            color: #333333;
            text-shadow: 1px 1px 2px rgba(255,255,255,0.8);
            pointer-events: none;
            z-index: 10;
        }
        #ui h1 {
            margin: 0;
            font-size: clamp(1rem, 5vw, 1.5rem); /* 画面サイズに合わせて自動調整 */
        }
        #ui p {
            margin: 5px 0 0 0;
            font-size: clamp(0.7rem, 3vw, 0.9rem);
        }
        #instructions {
            position: absolute;
            bottom: env(safe-area-inset-bottom, 20px);
            width: 100%;
            text-align: center;
            color: #555555;
            font-size: clamp(0.6rem, 2.5vw, 0.8rem);
            text-shadow: 1px 1px 1px rgba(255,255,255,0.8);
            pointer-events: none;
            z-index: 10;
            padding: 0 10px;
            box-sizing: border-box;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>

    <div id="ui">
        <h1>Mountain Stream Aquarium</h1>
        <p>魚100匹とカニ35匹の清流アクアリウム</p>
    </div>

    <div id="instructions">
        ドラッグ・スワイプで回転 / スクロール・ピンチでズーム
    </div>

    <!-- Three.js Library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    
    <script>
        let scene, camera, renderer, fishes = [], bubbles = [], rocks = [], crabs = [], plants = [];
        let targetRotationX = 0.2, targetRotationY = 0;
        let cameraDistance = 600; 
        let isMouseDown = false;
        let lastMouseX = 0, lastMouseY = 0;
        
        // タッチ操作用変数
        let lastTouchX = 0, lastTouchY = 0;
        let lastPinchDistance = 0;

        const FISH_COUNT = 100; 
        const NIGOI_COUNT = 10; 
        const CRAB_COUNT = 35;  
        const BUBBLE_COUNT = 60;
        const PLANT_COUNT = 40; 
        const TANK_SIZE = { w: 160, h: 90, d: 90 };

        window.onload = function() {
            init();
            animate();
        };

        // 各種テクスチャ生成
        function createYamameTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 256; canvas.height = 128;
            const ctx = canvas.getContext('2d');
            const grad = ctx.createLinearGradient(0, 0, 0, 128);
            grad.addColorStop(0, '#5d5d4a');
            grad.addColorStop(0.3, '#a3a380'); 
            grad.addColorStop(0.5, '#d1d1c1');
            grad.addColorStop(1, '#ffffff');
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, 256, 128);
            ctx.fillStyle = 'rgba(60, 65, 50, 0.7)';
            for(let i = 0; i < 8; i++) {
                ctx.beginPath();
                ctx.ellipse(40 + i * 25, 64, 8, 15, 0, 0, Math.PI * 2);
                ctx.fill();
            }
            ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
            for(let i = 0; i < 150; i++) {
                ctx.beginPath();
                ctx.arc(Math.random() * 256, Math.random() * 60, 0.5 + Math.random(), 0, Math.PI * 2);
                ctx.fill();
            }
            return new THREE.CanvasTexture(canvas);
        }

        function createRainbowTroutTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 256; canvas.height = 128;
            const ctx = canvas.getContext('2d');
            const grad = ctx.createLinearGradient(0, 0, 0, 128);
            grad.addColorStop(0, '#4a5d4a');
            grad.addColorStop(0.4, '#d1d1c1');
            grad.addColorStop(1, '#ffffff');
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, 256, 128);
            const redStripeGrad = ctx.createLinearGradient(0, 60, 0, 75);
            redStripeGrad.addColorStop(0, 'rgba(180, 50, 50, 0)');
            redStripeGrad.addColorStop(0.5, 'rgba(200, 60, 60, 0.6)');
            redStripeGrad.addColorStop(1, 'rgba(180, 50, 50, 0)');
            ctx.fillStyle = redStripeGrad;
            ctx.fillRect(0, 60, 256, 15);
            ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
            for(let i = 0; i < 300; i++) {
                const rx = Math.random() * 256, ry = Math.random() * 128;
                if (ry > 100 && Math.random() > 0.3) continue;
                ctx.beginPath();
                ctx.arc(rx, ry, 0.5 + Math.random() * 0.8, 0, Math.PI * 2);
                ctx.fill();
            }
            return new THREE.CanvasTexture(canvas);
        }

        function createNigoiTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 256; canvas.height = 128;
            const ctx = canvas.getContext('2d');
            const grad = ctx.createLinearGradient(0, 0, 0, 128);
            grad.addColorStop(0, '#222222'); 
            grad.addColorStop(0.3, '#666666'); 
            grad.addColorStop(0.6, '#bbbbbb'); 
            grad.addColorStop(1, '#dddddd'); 
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, 256, 128);
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
            ctx.lineWidth = 1;
            for(let x = 0; x < 256; x += 8) {
                for(let y = 0; y < 128; y += 6) {
                    ctx.beginPath();
                    ctx.arc(x + (y%12===0?0:4), y, 4, 0, Math.PI);
                    ctx.stroke();
                }
            }
            return new THREE.CanvasTexture(canvas);
        }

        function createCrabTexture() {
            const canvas = document.createElement('canvas');
            canvas.width = 128; canvas.height = 128;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = '#e09f7d';
            ctx.fillRect(0, 0, 128, 128);
            ctx.fillStyle = 'rgba(165, 42, 42, 0.4)';
            for(let i=0; i<200; i++) {
                ctx.beginPath();
                ctx.arc(Math.random()*128, Math.random()*128, 1+Math.random()*2, 0, Math.PI*2);
                ctx.fill();
            }
            return new THREE.CanvasTexture(canvas);
        }

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xf0f4f8); 
            scene.fog = new THREE.Fog(0xf0f4f8, 300, 1800);

            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 4000);
            camera.position.set(0, 100, cameraDistance);

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // モバイルの負荷軽減
            renderer.shadowMap.enabled = true;
            document.body.appendChild(renderer.domElement);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
            scene.add(ambientLight);

            const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
            dirLight.position.set(50, 200, 100);
            dirLight.castShadow = true;
            scene.add(dirLight);

            const tankLight = new THREE.PointLight(0x4fc3f7, 1.2, 1000);
            tankLight.position.set(0, 100, 0);
            scene.add(tankLight);

            const sandGeo = new THREE.PlaneGeometry(3000, 3000);
            const sandMat = new THREE.MeshPhongMaterial({ color: 0xdfd4b0 });
            const sand = new THREE.Mesh(sandGeo, sandMat);
            sand.rotation.x = -Math.PI / 2;
            sand.position.y = -TANK_SIZE.h / 2;
            sand.receiveShadow = true;
            scene.add(sand);

            createRocks();
            createGlassTank();
            createPlants();
            createRockTunnel({x: -120, z: 0}, Math.PI / 4);
            createRockTunnel({x: 120, z: -50}, -Math.PI / 3);

            const yamameTex = createYamameTexture();
            const rainbowTex = createRainbowTroutTexture();
            const nigoiTex = createNigoiTexture();

            for(let i=0; i < FISH_COUNT; i++) {
                createFish((i % 2 === 0) ? yamameTex : rainbowTex, 1.0);
            }
            for(let i=0; i < NIGOI_COUNT; i++) {
                createFish(nigoiTex, 2.5); 
            }
            const crabTex = createCrabTexture();
            for(let i=0; i < CRAB_COUNT; i++) {
                createCrab(crabTex);
            }
            for(let i=0; i < BUBBLE_COUNT; i++) {
                createBubble();
            }

            // イベントリスナー
            window.addEventListener('resize', onWindowResize, false);
            
            // マウス
            document.addEventListener('mousedown', () => isMouseDown = true);
            document.addEventListener('mouseup', () => isMouseDown = false);
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('wheel', onMouseWheel, { passive: false });
            
            // タッチ (スマホ・タブレット対応)
            document.addEventListener('touchstart', onTouchStart, { passive: false });
            document.addEventListener('touchmove', onTouchMove, { passive: false });
            document.addEventListener('touchend', onTouchEnd);
        }

        function createRockTunnel(pos, rotation) {
            const tunnelGroup = new THREE.Group();
            const rockMat = new THREE.MeshPhongMaterial({ color: 0x607d8b, flatShading: true });
            const segments = 8;
            const radius = 25;
            for (let i = 0; i < segments; i++) {
                const angle = (i / (segments - 1)) * Math.PI;
                const x = Math.cos(angle) * radius;
                const y = Math.sin(angle) * radius;
                for (let zOffset = -20; zOffset <= 20; zOffset += 15) {
                    const size = 12 + Math.random() * 8;
                    const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(size, 0), rockMat);
                    rock.position.set(x, y - 5, zOffset);
                    rock.rotation.set(Math.random(), Math.random(), Math.random());
                    rock.receiveShadow = true; rock.castShadow = true;
                    tunnelGroup.add(rock);
                }
            }
            tunnelGroup.position.set(pos.x, -TANK_SIZE.h / 2 + 5, pos.z);
            tunnelGroup.rotation.y = rotation;
            scene.add(tunnelGroup);
        }

        function createPlants() {
            const plantMat = new THREE.MeshPhongMaterial({
                color: 0x2e7d32, side: THREE.DoubleSide, flatShading: true
            });
            for (let i = 0; i < PLANT_COUNT; i++) {
                const plantGroup = new THREE.Group();
                const bladeCount = 3 + Math.floor(Math.random() * 4);
                for (let j = 0; j < bladeCount; j++) {
                    const height = 12 + Math.random() * 25;
                    const width = 1 + Math.random() * 2;
                    const geo = new THREE.PlaneGeometry(width, height, 1, 4);
                    const posAttr = geo.attributes.position;
                    for (let k = 0; k < posAttr.count; k++) {
                        const y = posAttr.getY(k);
                        const normalizedY = (y + height/2) / height;
                        posAttr.setX(k, posAttr.getX(k) + Math.pow(normalizedY, 2) * 2);
                    }
                    const blade = new THREE.Mesh(geo, plantMat);
                    blade.position.y = height / 2;
                    blade.rotation.y = Math.random() * Math.PI;
                    blade.position.set((Math.random()-0.5)*5, height/2, (Math.random()-0.5)*5);
                    plantGroup.add(blade);
                }
                const px = (Math.random() - 0.5) * TANK_SIZE.w * 3.8;
                const pz = (Math.random() - 0.5) * TANK_SIZE.d * 3.8;
                plantGroup.position.set(px, -TANK_SIZE.h / 2, pz);
                scene.add(plantGroup);
                plants.push({ mesh: plantGroup, phase: Math.random() * Math.PI * 2 });
            }
        }

        function createGlassTank() {
            const width = TANK_SIZE.w * 4.6;
            const height = TANK_SIZE.h * 3.2;
            const depth = TANK_SIZE.d * 4.6;
            const glassGeo = new THREE.BoxGeometry(width, height, depth);
            const glassMat = new THREE.MeshStandardMaterial({
                color: 0xccf2ff, transparent: true, opacity: 0.18, metalness: 0.1, roughness: 0.1, side: THREE.DoubleSide
            });
            const glass = new THREE.Mesh(glassGeo, glassMat);
            glass.position.y = height / 2 - TANK_SIZE.h / 2 - 5;
            scene.add(glass);
            const wireframe = new THREE.LineSegments(new THREE.EdgesGeometry(glassGeo), new THREE.LineBasicMaterial({ color: 0xbbbbbb }));
            wireframe.position.copy(glass.position);
            scene.add(wireframe);
            const columnGeo = new THREE.BoxGeometry(5, height, 5);
            const columnMat = new THREE.MeshStandardMaterial({ color: 0x666666 });
            const corners = [{x: 0.5, z: 0.5}, {x: -0.5, z: 0.5}, {x: 0.5, z: -0.5}, {x: -0.5, z: -0.5}];
            corners.forEach(c => {
                const column = new THREE.Mesh(columnGeo, columnMat);
                column.position.set(c.x * width, glass.position.y, c.z * depth);
                scene.add(column);
            });
        }

        function createRocks() {
            for(let i=0; i<35; i++) {
                const size = 4 + Math.random() * 20;
                const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(size, 0), new THREE.MeshPhongMaterial({ color: 0x78909c, flatShading: true }));
                rock.position.set((Math.random()-0.5)*TANK_SIZE.w*4.5, -TANK_SIZE.h/2 + size*0.5, (Math.random()-0.5)*TANK_SIZE.d*4.2);
                rock.rotation.set(Math.random(), Math.random(), Math.random());
                rock.receiveShadow = true; rock.castShadow = true;
                scene.add(rock);
            }
        }

        function createFish(texture, baseScale) {
            const group = new THREE.Group();
            const body = new THREE.Mesh(
                new THREE.SphereGeometry(1, 32, 16),
                new THREE.MeshPhongMaterial({ map: texture, shininess: 80 })
            );
            const isNigoi = baseScale > 2.0;
            body.scale.set(4 * baseScale, isNigoi ? 1.1 * baseScale : 1.4 * baseScale, 0.9 * baseScale);
            body.rotation.y = Math.PI / 2;
            group.add(body);
            const eyeMat = new THREE.MeshBasicMaterial({ color: 0x111111 });
            const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.15 * baseScale, 8, 8), eyeMat);
            eyeL.position.set(0.5 * baseScale, 0.1 * baseScale, -3.4 * baseScale); group.add(eyeL);
            const eyeR = eyeL.clone(); eyeR.position.x = -0.5 * baseScale; group.add(eyeR);

            const finMat = new THREE.MeshPhongMaterial({ color: 0x666666, side: THREE.DoubleSide, transparent: true, opacity: 0.7 });
            const tail = new THREE.Mesh(new THREE.PlaneGeometry(2.5 * baseScale, 2.8 * baseScale), finMat);
            tail.position.z = 4.2 * baseScale; tail.rotation.y = Math.PI / 2; group.add(tail);
            const dorsalFin = new THREE.Mesh(new THREE.PlaneGeometry(1.5 * baseScale, 2.2 * baseScale), finMat);
            dorsalFin.position.set(0, 1.3 * baseScale, 0.3 * baseScale); dorsalFin.rotation.y = Math.PI / 2; group.add(dorsalFin);
            const pectoralL = new THREE.Mesh(new THREE.PlaneGeometry(1.2 * baseScale, 0.8 * baseScale), finMat);
            pectoralL.position.set(0.8 * baseScale, -0.4 * baseScale, -2.0 * baseScale); pectoralL.rotation.set(0.3, 0.8, -0.5); group.add(pectoralL);
            const pectoralR = pectoralL.clone(); pectoralR.position.x = -0.8 * baseScale; pectoralR.rotation.y = -0.8; group.add(pectoralR);
            const pelvicL = new THREE.Mesh(new THREE.PlaneGeometry(0.8 * baseScale, 0.6 * baseScale), finMat);
            pelvicL.position.set(0.4 * baseScale, -1.2 * baseScale, 0); pelvicL.rotation.set(0.5, 0.2, 0); group.add(pelvicL);
            const pelvicR = pelvicL.clone(); pelvicR.position.x = -0.4 * baseScale; group.add(pelvicR);
            const analFin = new THREE.Mesh(new THREE.PlaneGeometry(1.2 * baseScale, 1.0 * baseScale), finMat);
            analFin.position.set(0, -1.2 * baseScale, 2.2 * baseScale); analFin.rotation.y = Math.PI / 2; group.add(analFin);

            const posY = isNigoi ? (-TANK_SIZE.h/2 + 15 + Math.random()*30) : (Math.random()-0.5)*TANK_SIZE.h*1.3;
            group.position.set((Math.random()-0.5)*TANK_SIZE.w*2.5, posY, (Math.random()-0.5)*TANK_SIZE.d*2.5);
            const speed = (0.08 + Math.random() * 0.25) * (1.5 / baseScale);
            fishes.push({ 
                mesh: group, tail: tail, pectoralL: pectoralL, pectoralR: pectoralR, 
                speed: speed, angle: Math.random()*Math.PI*2, phase: Math.random()*Math.PI*2, isNigoi 
            });
            scene.add(group);
        }

        function createCrab(texture) {
            const group = new THREE.Group();
            const body = new THREE.Mesh(new THREE.SphereGeometry(1, 16, 12), new THREE.MeshPhongMaterial({ map: texture }));
            body.scale.set(1.5, 0.8, 1.2); group.add(body);
            const stalkMat = new THREE.MeshPhongMaterial({ color: 0xdba28c });
            for(let i=0; i<2; i++) {
                const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 1), stalkMat);
                stalk.position.set(i===0?0.3:-0.3, 0.6, -0.6); stalk.rotation.x = -0.4; group.add(stalk);
                const eyeball = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), new THREE.MeshBasicMaterial({ color: 0x000000 }));
                eyeball.position.set(i===0?0.3:-0.3, 1.1, -0.8); group.add(eyeball);
            }
            const legMat = new THREE.MeshPhongMaterial({ color: 0xdba28c });
            for(let i=0; i<4; i++) {
                for(let side=0; side<2; side++) {
                    const leg = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.15, 0.15), legMat);
                    leg.position.set(side===0?1.2:-1.2, -0.3, -0.6 + i*0.4);
                    leg.rotation.z = side===0? -0.5 : 0.5; group.add(leg);
                }
            }
            const clawMat = new THREE.MeshPhongMaterial({ color: 0xff6347 });
            const leftClaw = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 8), clawMat);
            leftClaw.scale.set(1.5, 1, 1); leftClaw.position.set(1.2, 0, -1.2); group.add(leftClaw);
            const rightClaw = leftClaw.clone(); rightClaw.scale.set(0.8, 0.6, 0.6); rightClaw.position.set(-1.0, 0, -1.2); group.add(rightClaw);
            group.position.set((Math.random()-0.5)*TANK_SIZE.w*2.5, -TANK_SIZE.h/2 + 0.5, (Math.random()-0.5)*TANK_SIZE.d*2.5);
            group.scale.set(1.8, 1.8, 1.8);
            crabs.push({ mesh: group, speed: 0.05 + Math.random() * 0.15, direction: Math.random() > 0.5 ? 1 : -1, wait: 0 });
            scene.add(group);
        }

        function createBubble() {
            const bubble = new THREE.Mesh(new THREE.SphereGeometry(0.1+Math.random()*0.6, 8, 8), new THREE.MeshPhongMaterial({ color: 0xffffff, transparent: true, opacity: 0.3 }));
            resetBubble(bubble); scene.add(bubble); bubbles.push(bubble);
        }

        function resetBubble(bubble) {
            bubble.position.set((Math.random()-0.5)*400, -TANK_SIZE.h/2, (Math.random()-0.5)*350);
            bubble.userData.speed = 0.06 + Math.random() * 0.3;
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight; 
            camera.updateProjectionMatrix(); 
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        // マウスイベント
        function onMouseMove(event) {
            if (isMouseDown) {
                targetRotationY += (event.clientX - lastMouseX) * 0.01; 
                targetRotationX += (event.clientY - lastMouseY) * 0.01;
                targetRotationX = Math.max(-0.5, Math.min(1.2, targetRotationX));
            }
            lastMouseX = event.clientX; lastMouseY = event.clientY;
        }

        function onMouseWheel(event) {
            event.preventDefault();
            cameraDistance += event.deltaY * 0.5;
            cameraDistance = Math.max(150, Math.min(1200, cameraDistance));
        }

        // タッチイベント (スマホ・タブレット対応)
        function onTouchStart(event) {
            if (event.touches.length === 1) {
                lastTouchX = event.touches[0].clientX;
                lastTouchY = event.touches[0].clientY;
            } else if (event.touches.length === 2) {
                lastPinchDistance = getTouchDistance(event.touches[0], event.touches[1]);
            }
        }

        function onTouchMove(event) {
            event.preventDefault(); // デフォルトのスクロール等を防止
            
            if (event.touches.length === 1) {
                // 1本指: 視点回転
                const touch = event.touches[0];
                const dx = touch.clientX - lastTouchX;
                const dy = touch.clientY - lastTouchY;
                
                targetRotationY += dx * 0.01;
                targetRotationX += dy * 0.01;
                targetRotationX = Math.max(-0.5, Math.min(1.2, targetRotationX));
                
                lastTouchX = touch.clientX;
                lastTouchY = touch.clientY;
            } else if (event.touches.length === 2) {
                // 2本指: ピンチズーム
                const distance = getTouchDistance(event.touches[0], event.touches[1]);
                const delta = lastPinchDistance - distance;
                
                cameraDistance += delta * 2.0;
                cameraDistance = Math.max(150, Math.min(1200, cameraDistance));
                
                lastPinchDistance = distance;
            }
        }

        function onTouchEnd() {
            // 特にリセットの必要なし
        }

        function getTouchDistance(t1, t2) {
            const dx = t1.clientX - t2.clientX;
            const dy = t1.clientY - t2.clientY;
            return Math.sqrt(dx * dx + dy * dy);
        }

        function animate() {
            requestAnimationFrame(animate);
            const time = Date.now() * 0.001;

            const targetX = Math.sin(targetRotationY) * cameraDistance;
            const targetZ = Math.cos(targetRotationY) * cameraDistance;
            const targetY = targetRotationX * (cameraDistance * 0.3) + 50;

            camera.position.x += (targetX - camera.position.x) * 0.05;
            camera.position.y += (targetY - camera.position.y) * 0.05;
            camera.position.z += (targetZ - camera.position.z) * 0.05;
            camera.lookAt(0, 0, 0);

            fishes.forEach(f => {
                f.mesh.position.x += Math.cos(f.angle) * f.speed;
                f.mesh.position.z += Math.sin(f.angle) * f.speed;
                f.mesh.position.y += Math.sin(time + f.phase) * (f.isNigoi ? 0.02 : 0.05);
                f.mesh.rotation.y = -f.angle + Math.PI;
                f.tail.rotation.y = Math.PI / 2 + Math.sin(time * (f.isNigoi ? 10 : 15) + f.phase) * 0.7;
                f.pectoralL.rotation.z = -0.5 + Math.sin(time * 5 + f.phase) * 0.2;
                f.pectoralR.rotation.z = 0.5 - Math.sin(time * 5 + f.phase) * 0.2;
                if (Math.abs(f.mesh.position.x) > TANK_SIZE.w * 2.3) f.angle = Math.PI - f.angle;
                if (Math.abs(f.mesh.position.z) > TANK_SIZE.d * 2.3) f.angle = -f.angle;
            });

            plants.forEach(p => {
                p.mesh.children.forEach((blade, index) => {
                    blade.rotation.z = Math.sin(time * 1.5 + p.phase + index) * 0.1;
                });
            });

            crabs.forEach(c => {
                if (c.wait > 0) { c.wait--; } else {
                    c.mesh.position.x += c.speed * c.direction;
                    c.mesh.position.y = -TANK_SIZE.h/2 + 0.5 + Math.abs(Math.sin(time * 12)) * 0.2;
                    if (Math.abs(c.mesh.position.x) > TANK_SIZE.w * 2.2 || Math.random() < 0.005) {
                        c.wait = 40 + Math.random() * 120; if (Math.abs(c.mesh.position.x) > TANK_SIZE.w * 2.2) c.direction *= -1;
                    }
                }
            });

            bubbles.forEach(b => {
                b.position.y += b.userData.speed; if(b.position.y > TANK_SIZE.h * 1.5) resetBubble(b);
            });

            renderer.render(scene, camera);
        }
    </script>
</body>
</html>

清流Aquarium

by momoka2

癒やされます。眺めて下さい。

🤖 使用したAI
Gemini
👁 4 回閲覧
📅 投稿:2026年4月11日 🔄 更新:2026年4月13日
応援スタンプを押してみよう!