请使用3dweb技术，帮我创建制作一个高尔顿板模拟器，以便可以更直观地理解概率、统计、正态分布、大数法则、二项分布等抽象数学概念

注意：

1、所有代码都在index.html上

2、通过图表自动统计和显示模拟结果

3、页面需要进行移动端适配

4、确保移动端下控制面板和图表统计结果面板不会挡住模拟器的显示和相关操作



好的，下面是一个使用 Three.js（一个流行的 3D JavaScript 库）和 Cannon-es（一个物理引擎）来创建一个高尔顿板模拟器的 `index.html` 文件。这个模拟器将包含 3D 可视化、物理模拟、结果统计图表，并进行了移动端适配。

**核心技术:**

1.  **Three.js:** 用于创建 3D 场景、物体（小球、钉子、隔板）、相机、光照和渲染。
2.  **Cannon-es:** 用于处理 3D 物理模拟（重力、碰撞）。它是 Cannon.js 的一个维护分支。
3.  **Chart.js:** 用于绘制柱状图，显示小球落入不同区间的统计结果。
4.  **HTML/CSS:** 用于页面结构、布局和样式，包括响应式设计以适应移动端。

**`index.html` 文件内容:**

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>高尔顿板 (Galton Board) 模拟器</title>
    <style>
        body {
            margin: 0;
            overflow: hidden; /* 防止滚动条 */
            font-family: sans-serif;
            background-color: #f0f0f0;
            display: flex;
            flex-direction: column;
            height: 100vh; /* 占满整个视窗高度 */
        }
        #simulation-container {
            flex-grow: 1; /* 占据剩余空间 */
            position: relative; /* 为内部绝对定位元素提供基准 */
            min-height: 200px; /* 保证模拟区域有最小高度 */
            overflow: hidden; /* 防止渲染区域溢出 */
        }
        #info-panels {
            display: flex;
            flex-wrap: wrap; /* 在小屏幕上换行 */
            justify-content: space-around;
            background-color: #e0e0e0;
            padding: 5px 0;
            width: 100%;
            box-sizing: border-box;
        }
        .panel {
            background-color: #fff;
            padding: 10px;
            margin: 5px;
            border-radius: 5px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            min-width: 250px; /* 面板最小宽度 */
            box-sizing: border-box;
        }
        #controls {
            display: flex;
            flex-direction: column;
            align-items: flex-start; /* 左对齐 */
        }
        #controls label, #controls button {
            margin: 3px 0;
        }
        #controls input[type="range"] {
            width: 150px;
        }
        #chart-container {
           max-height: 200px; /* 限制图表高度 */
           min-height: 150px;
           width: 100%;
           max-width: 400px; /* 限制图表最大宽度 */
        }
        canvas#resultsChart {
           display: block;
           height: 100% !important; /* Chart.js 高度控制 */
           width: 100% !important; /* Chart.js 宽度控制 */
        }

        /* 移动端适配 */
        @media (max-width: 600px) {
            body {
                height: auto; /* 允许内容滚动 */
                min-height: 100vh;
            }
            #simulation-container {
                height: 60vh; /* 固定模拟区域高度 */
                min-height: 250px;
            }
             #info-panels {
                flex-direction: column; /* 垂直堆叠面板 */
                align-items: center; /* 居中对齐 */
                max-height: 40vh; /* 限制信息面板区域高度 */
                overflow-y: auto; /* 如果内容过多，允许滚动 */
            }
            .panel {
                 width: 90%; /* 面板宽度适应屏幕 */
                 min-width: unset;
                 margin: 5px auto;
            }
             #chart-container {
                max-height: 180px;
                width: 100%;
                max-width: none; /* 移除最大宽度限制 */
            }
        }
    </style>
</head>
<body>

    <div id="simulation-container"></div>

    <div id="info-panels">
        <div id="controls" class="panel">
            <h3>控制面板</h3>
            <div>
                <label for="ballCount">小球数量: <span id="ballCountLabel">500</span></label>
                <input type="range" id="ballCount" min="100" max="2000" step="100" value="500">
            </div>
            <div>
                <label for="rows">钉子行数: <span id="rowsLabel">12</span></label>
                <input type="range" id="rows" min="4" max="20" step="1" value="12">
            </div>
             <div>
                <label for="dropDelay">投放间隔 (ms): <span id="dropDelayLabel">50</span></label>
                <input type="range" id="dropDelay" min="10" max="500" step="10" value="50">
            </div>
            <button id="startButton">开始/重置模拟</button>
            <p id="status">状态: 未开始</p>
        </div>

        <div id="chart-container" class="panel">
             <h3>统计结果 (正态分布)</h3>
             <canvas id="resultsChart"></canvas>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cannon-es/0.19.0/cannon-es.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

    <script>
        // --- 全局变量 ---
        let scene, camera, renderer;
        let world; // Cannon-es 物理世界
        let balls = []; // 存储小球 (Three.js Mesh 和 Cannon.js Body)
        let pegs = []; // 存储钉子
        let dividers = []; // 存储底部隔板
        let binCounts = []; // 存储每个槽的小球数量
        let groundBody; // 地面物理实体

        let chart; // Chart.js 实例
        let animationFrameId; // 用于取消动画帧
        let dropIntervalId; // 用于控制小球投放

        // --- DOM 元素引用 ---
        const container = document.getElementById('simulation-container');
        const ballCountSlider = document.getElementById('ballCount');
        const ballCountLabel = document.getElementById('ballCountLabel');
        const rowsSlider = document.getElementById('rows');
        const rowsLabel = document.getElementById('rowsLabel');
        const dropDelaySlider = document.getElementById('dropDelay');
        const dropDelayLabel = document.getElementById('dropDelayLabel');
        const startButton = document.getElementById('startButton');
        const statusElement = document.getElementById('status');
        const chartCanvas = document.getElementById('resultsChart');

        // --- 配置参数 ---
        const config = {
            ballRadius: 0.15,
            pegRadius: 0.08,
            pegSpacing: 0.6, // 水平和垂直间距
            numRows: parseInt(rowsSlider.value),
            numBalls: parseInt(ballCountSlider.value),
            dropDelay: parseInt(dropDelaySlider.value), // ms
            restitution: 0.3, // 碰撞恢复系数 (弹性)
            friction: 0.1, // 摩擦力
            boardWidth: 0, // 将在 setup 中计算
            binHeight: 2,
            binWallThickness: 0.1,
            dropHeightOffset: 1, // 小球初始高度偏移
        };

        // --- 初始化函数 ---
        function init() {
            // 1. Three.js 设置
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xabcdef); // 天蓝色背景

            const aspect = container.clientWidth / container.clientHeight;
            camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
            // 调整相机位置以便观察
            camera.position.z = config.numRows * config.pegSpacing * 0.8 + 5; // 动态调整Z轴距离
            camera.position.y = config.numRows * config.pegSpacing * 0.5; // 稍微抬高视角
            camera.lookAt(0, 0, 0); // 相机看向原点

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(container.clientWidth, container.clientHeight);
            renderer.shadowMap.enabled = true; // 开启阴影
            container.appendChild(renderer.domElement);

            // 添加光照
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
            directionalLight.position.set(5, 10, 7.5);
            directionalLight.castShadow = true;
            // 配置阴影参数（可选，增加真实感）
            directionalLight.shadow.mapSize.width = 1024;
            directionalLight.shadow.mapSize.height = 1024;
            directionalLight.shadow.camera.near = 0.5;
            directionalLight.shadow.camera.far = 50;
             // 调整阴影相机范围以覆盖高尔顿板区域
            const shadowCamSize = config.numRows * config.pegSpacing;
            directionalLight.shadow.camera.left = -shadowCamSize;
            directionalLight.shadow.camera.right = shadowCamSize;
            directionalLight.shadow.camera.top = shadowCamSize;
            directionalLight.shadow.camera.bottom = -shadowCamSize;

            scene.add(directionalLight);

            // 2. Cannon-es 设置
            world = new CANNON.World();
            world.gravity.set(0, -9.82, 0); // 设置重力
            world.broadphase = new CANNON.NaiveBroadphase(); // 碰撞检测策略
            world.solver.iterations = 10; // 物理求解器迭代次数

             // 创建地面 (防止小球无限下落)
            const groundShape = new CANNON.Plane();
            groundBody = new CANNON.Body({ mass: 0 }); // mass 0 表示静态物体
            groundBody.addShape(groundShape);
            groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); // 使平面水平
            groundBody.position.set(0, -config.binHeight - 1, 0); // 放在隔板下方
            world.addBody(groundBody);

            // 3. Chart.js 设置
            const ctx = chartCanvas.getContext('2d');
            chart = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: [], // 将在 setupBoard 中填充
                    datasets: [{
                        label: '小球分布',
                        data: [], // 将在 setupBoard 中填充
                        backgroundColor: 'rgba(75, 192, 192, 0.6)',
                        borderColor: 'rgba(75, 192, 192, 1)',
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false, // 允许图表调整高度
                    scales: {
                        y: {
                            beginAtZero: true,
                            title: { display: true, text: '数量' }
                        },
                         x: {
                            title: { display: true, text: '落点区间' }
                        }
                    },
                    animation: {
                        duration: 300 // 平滑更新动画
                    }
                }
            });

            // 4. 创建高尔顿板
            setupBoard();

            // 5. 窗口大小调整处理
            window.addEventListener('resize', onWindowResize, false);

             // 6. 初始化滑块标签
            updateLabels();
        }

        // --- 创建高尔顿板几何体和物理体 ---
        function setupBoard() {
            // 清理旧的物体
            clearBoard();

            config.numRows = parseInt(rowsSlider.value);
            const numBins = config.numRows + 1;
            config.boardWidth = config.numRows * config.pegSpacing; // 板子大致宽度

            // --- 创建钉子 ---
            const pegGeometry = new THREE.CylinderGeometry(config.pegRadius, config.pegRadius, 0.2, 16); // 使用圆柱体更像钉子
            pegGeometry.rotateX(Math.PI / 2); // 竖直圆柱
            const pegMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, metalness: 0.3, roughness: 0.6 }); // 金属感材质
            const pegShape = new CANNON.Sphere(config.pegRadius); // 物理引擎用球体近似简化计算
            const pegBodyMaterial = new CANNON.Material('pegMaterial'); // 定义物理材质

            for (let i = 0; i < config.numRows; i++) {
                const y = (config.numRows / 2 - i) * config.pegSpacing - config.pegSpacing / 2; // Y 坐标从上到下
                const numPegsInRow = i + 1;
                for (let j = 0; j < numPegsInRow; j++) {
                    const x = (j - (numPegsInRow - 1) / 2) * config.pegSpacing;

                    // Three.js Mesh
                    const pegMesh = new THREE.Mesh(pegGeometry, pegMaterial);
                    pegMesh.position.set(x, y, 0);
                    pegMesh.castShadow = true;
                    pegMesh.receiveShadow = true;
                    scene.add(pegMesh);

                    // Cannon.js Body
                    const pegBody = new CANNON.Body({
                        mass: 0, // 静态
                        position: new CANNON.Vec3(x, y, 0),
                        shape: pegShape,
                        material: pegBodyMaterial
                    });
                    world.addBody(pegBody);
                    pegs.push({ mesh: pegMesh, body: pegBody });
                }
            }

            // --- 创建底部隔板 ---
            const dividerHeight = config.binHeight;
            const dividerDepth = 0.5;
            const dividerGeometry = new THREE.BoxGeometry(config.binWallThickness, dividerHeight, dividerDepth);
            const dividerMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.1, roughness: 0.8 });
            const dividerShape = new CANNON.Box(new CANNON.Vec3(config.binWallThickness / 2, dividerHeight / 2, dividerDepth / 2));
            const dividerBodyMaterial = new CANNON.Material('dividerMaterial');

            const startY = - (config.numRows / 2) * config.pegSpacing - dividerHeight / 2; // Y 坐标在最底行钉子下方
            const binWidth = config.pegSpacing; // 每个槽的宽度约等于钉子间距

            for (let i = 0; i <= numBins; i++) { // 需要 numBins + 1 个隔板
                 const x = (i - numBins / 2) * binWidth - config.binWallThickness / 2;

                 // Three.js Mesh
                 const dividerMesh = new THREE.Mesh(dividerGeometry, dividerMaterial);
                 dividerMesh.position.set(x, startY, 0);
                 dividerMesh.castShadow = true;
                 dividerMesh.receiveShadow = true;
                 scene.add(dividerMesh);

                 // Cannon.js Body
                 const dividerBody = new CANNON.Body({
                     mass: 0, // 静态
                     position: new CANNON.Vec3(x, startY, 0),
                     shape: dividerShape,
                     material: dividerBodyMaterial
                 });
                 world.addBody(dividerBody);
                 dividers.push({ mesh: dividerMesh, body: dividerBody });
            }

             // 添加左右边界墙 (可选，防止球滚出太远)
            const wallHeight = config.numRows * config.pegSpacing + config.binHeight + 2;
            const wallGeometry = new THREE.BoxGeometry(config.binWallThickness, wallHeight, dividerDepth);
            const wallShape = new CANNON.Box(new CANNON.Vec3(config.binWallThickness / 2, wallHeight / 2, dividerDepth / 2));

            const leftWallX = - (numBins / 2) * binWidth - config.binWallThickness;
            const rightWallX = (numBins / 2) * binWidth;

             [-1, 1].forEach(side => {
                 const wallX = side === -1 ? leftWallX : rightWallX;
                 const wallY = startY + dividerHeight/2 - wallHeight/2 + (config.numRows / 2 * config.pegSpacing) + config.pegSpacing/2; // 调整Y使墙壁从顶部开始

                 const wallMesh = new THREE.Mesh(wallGeometry, dividerMaterial);
                 wallMesh.position.set(wallX, wallY, 0);
                 wallMesh.castShadow = true;
                 wallMesh.receiveShadow = true;
                 scene.add(wallMesh);

                 const wallBody = new CANNON.Body({
                     mass: 0,
                     position: new CANNON.Vec3(wallX, wallY, 0),
                     shape: wallShape,
                     material: dividerBodyMaterial
                 });
                 world.addBody(wallBody);
                 dividers.push({ mesh: wallMesh, body: wallBody }); // 也视为隔板的一部分
             });


            // --- 初始化/重置统计数据和图表 ---
            binCounts = new Array(numBins).fill(0);
            updateChart();

             // 更新相机位置以适应新的板子尺寸
            camera.position.z = config.numRows * config.pegSpacing * 0.8 + 5;
            camera.position.y = config.numRows * config.pegSpacing * 0.5;
            camera.lookAt(0, 0, 0);

            // 定义物理材质间的相互作用
            const ballMaterial = new CANNON.Material('ballMaterial');
            const contactMaterialOptions = {
                 friction: config.friction,
                 restitution: config.restitution
            };
            world.addContactMaterial(new CANNON.ContactMaterial(ballMaterial, pegBodyMaterial, contactMaterialOptions));
            world.addContactMaterial(new CANNON.ContactMaterial(ballMaterial, dividerBodyMaterial, contactMaterialOptions));
             // 球与地面的接触
            const ballGroundContact = new CANNON.ContactMaterial(ballMaterial, world.defaultMaterial, {
                friction: 0.5, // 地面摩擦大一些
                restitution: 0.1 // 减少弹跳
            });
            world.addContactMaterial(ballGroundContact);

        }

        // --- 清理板子和球 ---
        function clearBoard() {
             // 清理小球
            balls.forEach(ball => {
                scene.remove(ball.mesh);
                world.removeBody(ball.body);
            });
            balls = [];

            // 清理钉子
            pegs.forEach(peg => {
                scene.remove(peg.mesh);
                world.removeBody(peg.body);
            });
            pegs = [];

            // 清理隔板
            dividers.forEach(divider => {
                scene.remove(divider.mesh);
                world.removeBody(divider.body);
            });
            dividers = [];

            // 重置统计
             binCounts = [];
        }

        // --- 创建单个小球 ---
        function createBall() {
            const ballGeometry = new THREE.SphereGeometry(config.ballRadius, 16, 16);
            const ballColor = new THREE.Color(Math.random(), Math.random(), Math.random());
            const ballMaterial = new THREE.MeshStandardMaterial({
                color: ballColor,
                metalness: 0.2,
                roughness: 0.5
            });
            const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
            ballMesh.castShadow = true; // 小球产生阴影
            ballMesh.receiveShadow = true; // 小球接收阴影

            const ballShape = new CANNON.Sphere(config.ballRadius);
            const ballBody = new CANNON.Body({
                mass: 0.1, // 小球质量
                shape: ballShape,
                material: world.materials.find(m => m.name === 'ballMaterial') // 获取之前定义的球材质
            });

            // 初始位置在顶行钉子上方，带微小随机偏移
            const startX = (Math.random() - 0.5) * config.pegRadius * 0.5; // 微小X偏移
            const startY = (config.numRows / 2) * config.pegSpacing + config.dropHeightOffset;
            ballBody.position.set(startX, startY, 0);
            ballMesh.position.copy(ballBody.position); // 同步初始位置

            scene.add(ballMesh);
            world.addBody(ballBody);
            balls.push({ mesh: ballMesh, body: ballBody, settled: false });
        }

        // --- 模拟循环 ---
        let lastTime;
        let ballsDropped = 0;
        let ballsSettled = 0;
        let simulationRunning = false;

        function animate(time) {
            if (!simulationRunning) return; // 如果模拟停止则退出循环

            animationFrameId = requestAnimationFrame(animate);

            const deltaTime = lastTime ? (time - lastTime) / 1000 : 1 / 60; // 计算时间差，防止标签页切换导致卡顿
            lastTime = time;

            // 1. 更新物理世界
            world.step(1 / 60, deltaTime, 3); // 固定的时间步长，实际时间差，最大子步数

            // 2. 同步 Three.js 物体和 Cannon.js 物体
            let needsChartUpdate = false;
            for (let i = balls.length - 1; i >= 0; i--) {
                 const ball = balls[i];
                 if (ball.body) { // 确保物理体存在
                    ball.mesh.position.copy(ball.body.position);
                    ball.mesh.quaternion.copy(ball.body.quaternion);

                     // 3. 检查小球是否落入底部并停止运动
                    const yPos = ball.body.position.y;
                    const velocity = ball.body.velocity.length();
                    const bottomThreshold = - (config.numRows / 2) * config.pegSpacing; // 大约在隔板顶部

                    if (!ball.settled && yPos < bottomThreshold && velocity < 0.1) {
                         // 判定落入哪个槽
                        const xPos = ball.body.position.x;
                        const binWidth = config.pegSpacing;
                        const numBins = config.numRows + 1;
                        // 计算相对于中心的位置，并映射到 bin 索引
                        const binIndexFloat = (xPos / binWidth) + (numBins / 2) - 0.5;
                        let binIndex = Math.round(binIndexFloat);

                        // 边界检查
                        binIndex = Math.max(0, Math.min(numBins - 1, binIndex));

                         if (binIndex >= 0 && binIndex < numBins) {
                            binCounts[binIndex]++;
                            ball.settled = true;
                            ballsSettled++;
                            needsChartUpdate = true;

                             // 可选: 当球停止时，将其变为静态以提高性能
                             // ball.body.type = CANNON.Body.STATIC;
                             // scene.remove(ball.mesh); // 或者直接移除
                             // world.removeBody(ball.body);
                             // balls.splice(i, 1); // 从数组移除
                        }
                    }
                     // 可选: 移除掉落出边界很远的球
                     if (Math.abs(ball.body.position.x) > config.boardWidth * 1.5 || yPos < -config.binHeight * 2) {
                          scene.remove(ball.mesh);
                          world.removeBody(ball.body);
                          balls.splice(i, 1);
                          if (!ball.settled) {
                            ballsDropped--; // 如果未计入结果就移除了，要减少总投放数？（或者视为无效投放）
                          }
                     }
                }
            }

            // 4. 更新图表 (不需要每一帧都更新，可以优化)
            if (needsChartUpdate) {
                 updateChart();
                 updateStatus(); // 更新状态显示
            }

            // 5. 渲染场景
            renderer.render(scene, camera);

             // 检查模拟是否完成
            if (ballsDropped >= config.numBalls && ballsSettled >= ballsDropped) {
                 stopSimulation('完成');
             } else if (ballsDropped >= config.numBalls && balls.length === 0) {
                 // 所有球都已投放，并且场景中没有球了（可能有些球滚出去了）
                 stopSimulation('完成 (部分小球可能丢失)');
             }
        }

        // --- 更新图表数据 ---
        function updateChart() {
            const numBins = config.numRows + 1;
            const labels = [];
            for(let i=0; i<numBins; ++i) {
                labels.push(i + 1); // 从 1 开始标记区间
            }
            chart.data.labels = labels;
            chart.data.datasets[0].data = binCounts;
            chart.update();
        }

         // --- 更新状态显示 ---
        function updateStatus() {
            statusElement.textContent = `状态: 运行中 | 已投放: ${ballsDropped}/${config.numBalls} | 已统计: ${ballsSettled}`;
        }

        // --- 开始/重置模拟 ---
        function startSimulation() {
            stopSimulation('重置中...'); // 先停止当前的
            statusElement.textContent = "状态: 初始化...";

            // 应用滑块设置
            config.numBalls = parseInt(ballCountSlider.value);
            config.dropDelay = parseInt(dropDelaySlider.value);
            const newRows = parseInt(rowsSlider.value);

            // 如果行数改变，需要重建板子
            if (newRows !== config.numRows || pegs.length === 0) {
                config.numRows = newRows;
                setupBoard(); // 重建物理和视觉元素
            } else {
                 // 仅清理小球和重置计数
                 clearBallsAndCounts();
            }


            ballsDropped = 0;
            ballsSettled = 0;
            lastTime = null;
            simulationRunning = true;
            statusElement.textContent = "状态: 运行中...";

            // 使用 setInterval 逐个投放小球
             let ballsToDrop = config.numBalls;
             dropIntervalId = setInterval(() => {
                if (ballsDropped < ballsToDrop && simulationRunning) {
                    createBall();
                    ballsDropped++;
                    updateStatus();
                } else {
                    clearInterval(dropIntervalId); // 所有球投放完毕
                    dropIntervalId = null;
                     // 不需要在此处停止动画，等待球落下
                     statusElement.textContent = `状态: 所有 ${ballsToDrop} 个球已投放，等待统计...`;
                }
            }, config.dropDelay);


            // 启动动画循环 (如果尚未运行)
            if (!animationFrameId) {
               animate();
            }
        }

        // 清理小球和重置计数，但不重建板子
        function clearBallsAndCounts() {
             balls.forEach(ball => {
                if(ball.mesh) scene.remove(ball.mesh);
                if(ball.body) world.removeBody(ball.body);
            });
            balls = [];
            binCounts = new Array(config.numRows + 1).fill(0);
            updateChart();
        }


        // --- 停止模拟 ---
        function stopSimulation(reason = '已停止') {
             simulationRunning = false;
             if (animationFrameId) {
                cancelAnimationFrame(animationFrameId);
                animationFrameId = null;
            }
            if (dropIntervalId) {
                 clearInterval(dropIntervalId);
                 dropIntervalId = null;
            }
            statusElement.textContent = `状态: ${reason}`;
            lastTime = null; // 重置 lastTime
        }


        // --- 窗口大小调整处理 ---
        function onWindowResize() {
            const width = container.clientWidth;
            const height = container.clientHeight;

            if (width > 0 && height > 0) { // 确保宽高有效
                camera.aspect = width / height;
                camera.updateProjectionMatrix();
                renderer.setSize(width, height);
            }
             // 可以在这里调整图表容器的大小，如果需要的话
        }

         // --- 更新滑块标签 ---
        function updateLabels() {
             ballCountLabel.textContent = ballCountSlider.value;
             rowsLabel.textContent = rowsSlider.value;
             dropDelayLabel.textContent = dropDelaySlider.value;
         }

        // --- 事件监听器 ---
        startButton.addEventListener('click', startSimulation);
        ballCountSlider.addEventListener('input', updateLabels);
        rowsSlider.addEventListener('input', updateLabels);
        dropDelaySlider.addEventListener('input', updateLabels);


        // --- 页面加载后初始化 ---
        window.addEventListener('DOMContentLoaded', () => {
            init();
             // 初始时不自动开始，等待用户点击
             statusElement.textContent = "状态: 准备就绪，点击开始";
             // 调用 onWindowResize 确保初始布局正确
             onWindowResize();
        });

    </script>
</body>
</html>
```

**代码说明:**

1.  **HTML 结构:**
    * `simulation-container`: 用于显示 Three.js 的 3D 场景。
    * `info-panels`: 包含控制面板和图表，使用 Flexbox 布局。
    * `controls`: 包含滑块（小球数量、钉子行数、投放间隔）和开始按钮。
    * `chart-container`: 包含 Chart.js 的 `<canvas>` 元素。
    * 引入了 Three.js, Cannon-es, 和 Chart.js 的 CDN 链接。

2.  **CSS 样式:**
    * 基本的页面布局，确保模拟器区域和信息面板区域合理分配空间。
    * **移动端适配 (`@media (max-width: 600px)`)**:
        * 当屏幕宽度小于 600px 时，将 `info-panels` 从横向排列改为纵向堆叠。
        * 给模拟器区域 `simulation-container` 设置一个相对固定的视口高度 (`60vh`)，信息面板区域 (`info-panels`) 占据剩余高度 (`max-height: 40vh`) 并允许内部滚动 (`overflow-y: auto`)，这样它们就不会互相遮挡。
        * 面板宽度调整为接近屏幕宽度。
    * 确保 Chart.js 的画布正确显示和缩放。

3.  **JavaScript 逻辑:**
    * **全局变量:** 存储场景、相机、渲染器、物理世界、物体数组、统计数据等。
    * **`init()`:** 初始化 Three.js（场景、相机、光照、渲染器）、Cannon-es（物理世界、重力）、Chart.js（图表实例）和事件监听器，并调用 `setupBoard()` 创建初始的高尔顿板。
    * **`setupBoard()`:**
        * 核心函数，用于创建高尔顿板的物理和视觉元素。
        * **清理:** 先调用 `clearBoard()` 移除旧的物体。
        * **创建钉子 (Pegs):** 循环创建指定行数的钉子。每个钉子包含一个 Three.js 的 `Mesh` (用于显示) 和一个 Cannon-es 的 `Body` (用于物理计算，设为静态 `mass: 0`)。钉子在物理引擎中用球体近似，计算更简单。
        * **创建隔板 (Dividers):** 在底部创建一系列垂直的隔板，形成收集小球的槽。同样包含 `Mesh` 和静态 `Body`。还添加了左右边界墙。
        * **初始化统计:** 创建 `binCounts` 数组（长度为 `行数 + 1`），全部初始化为 0。
        * **更新图表:** 设置图表的标签和初始数据。
        * **物理材质:** 定义球、钉子、隔板的物理材质及它们之间的接触属性（摩擦力 `friction`、恢复系数/弹性 `restitution`）。
    * **`clearBoard()`:** 移除场景和物理世界中的所有钉子、隔板和小球。
    * **`createBall()`:** 创建单个小球，包含随机颜色的 `Mesh` 和动态的 `Body` (有质量 `mass: 0.1`)。初始位置设置在顶部中央略上方，带有微小的随机水平偏移。
    * **`animate()`:** 模拟的主循环，由 `requestAnimationFrame` 驱动。
        * 更新物理世界 (`world.step()`)。
        * 遍历所有小球，将 Three.js `Mesh` 的位置和旋转与 Cannon-es `Body` 同步。
        * **统计:** 检查每个小球是否落入底部（低于某个 Y 阈值且速度很低）。如果落入，根据其 X 坐标计算所属的槽 (`binIndex`)，增加对应槽的计数 (`binCounts[binIndex]++`)，并将小球标记为 `settled`。可选地，可以将稳定的小球从物理模拟中移除或设为静态以提高性能。
        * **移除越界球:** 检查并移除飞出边界太远的小球。
        * **更新图表:** 如果有小球落入槽中，则调用 `updateChart()` 更新图表显示。
        * 渲染 Three.js 场景 (`renderer.render()`)。
        * 检查模拟是否结束（所有球已投放且已统计）。
    * **`updateChart()`:** 更新 Chart.js 图表的数据和标签。
    * **`startSimulation()`:** 由“开始/重置”按钮触发。
        * 停止任何正在进行的模拟。
        * 读取滑块的值更新配置。
        * 如果行数改变，调用 `setupBoard()` 重建板子；否则只清理小球和计数。
        * 重置计数器和状态。
        * 使用 `setInterval` 按设定的 `dropDelay` 逐个创建和投放小球。
        * 启动 `animate` 循环。
    * **`stopSimulation()`:** 停止动画循环和投放小球的定时器。
    * **`onWindowResize()`:** 响应浏览器窗口大小变化，调整相机宽高比和渲染器尺寸。
    * **事件监听:** 为按钮和滑块添加事件监听器。
    * **初始化调用:** 页面加载完成后调用 `init()`。

**如何使用:**

1.  将以上所有代码复制并粘贴到一个名为 `index.html` 的文本文件中。
2.  用现代的网页浏览器（如 Chrome, Firefox, Edge, Safari）打开这个 `index.html` 文件。
3.  你应该能看到一个 3D 的高尔顿板。
4.  调整滑块来设置小球数量、钉子行数和投放速度。
5.  点击“开始/重置模拟”按钮，小球将开始下落。
6.  底部的柱状图会实时更新，显示落入每个槽的小球数量，最终形成近似正态分布的形状。
7.  尝试在桌面浏览器和手机浏览器上访问，观察响应式布局的效果。控制面板和图表在小屏幕上会堆叠在模拟区域下方，并且可以滚动查看。

这个模拟器可以帮助你直观地理解：

* **概率与随机性:** 每个小球碰到钉子都有近似 50% 的概率向左或向右。
* **二项分布:** 每层的结果可以看作一次伯努利试验，多次试验（多行钉子）后，小球最终位置的分布趋向于二项分布。
* **正态分布 (中心极限定理):** 当行数足够多时，二项分布近似于正态分布，你会看到中间槽的小球最多，两边逐渐减少，形成钟形曲线。
* **大数法则:** 模拟的小球数量越多，最终得到的分布形状越接近理论上的正态分布曲线。