Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <title>可调参数的旋转六边形抛球模拟</title>
- <style>
- body {
- margin: 0;
- background: #222;
- display:flex; flex-direction:column;
- align-items:center; justify-content:center;
- min-height: 100vh;
- }
- canvas {
- background: #222;
- box-shadow: 0 4px 32px #111;
- display:block;
- margin-bottom: 12px;
- }
- /* UI美化 */
- #controls {
- background: #181818;
- border-radius: 8px;
- padding: 16px 22px 10px 22px;
- box-shadow: 0 2px 14px #0008;
- margin-bottom: 14px;
- color: #f0e6cc;
- min-width: 390px;
- font-family: sans-serif;
- }
- .param-group {
- display: flex; align-items: center;
- margin-bottom: 10px;
- }
- .param-group label { width: 115px }
- .param-group input[type=range] { flex:1; margin: 0 8px; }
- .param-group .val { width:54px; text-align:right;}
- #btns {
- text-align: right; padding-top: 5px;
- }
- button {
- margin-left: 10px;
- padding: 7px 21px;
- border-radius: 5px;
- border: none;
- background: #FFEB3B;
- color: #444;
- font-weight: bold;
- cursor: pointer;
- transition:.2s;
- font-size:17px;
- }
- button:active { box-shadow: inset 0 2px 8px #fff5,0 1px 2px #0004; }
- button[disabled] {
- filter: grayscale(70%);
- background:#999;
- color:#ddd;
- cursor:not-allowed;
- }
- </style>
- </head>
- <body>
- <canvas id="canvas" width="600" height="600"></canvas>
- <div id="controls">
- <div class="param-group">
- <label>重力 g (px/s²)</label>
- <input id="paramG" type="range" min="100" max="2000" value="700" step="1">
- <span id="valG" class="val">700</span>
- </div>
- <div class="param-group">
- <label>六边形边长</label>
- <input id="paramHexR" type="range" min="60" max="290" value="200" step="1">
- <span id="valHexR" class="val">200</span>
- </div>
- <div class="param-group">
- <label>六边形旋转速度</label>
- <input id="paramAngV" type="range" min="0" max="628" value="104" step="1">
- <span id="valAngV" class="val">0.52</span>
- <span style="font-size:12px;">(rad/s)</span>
- </div>
- <div class="param-group">
- <label>小球半径</label>
- <input id="paramBallR" type="range" min="7" max="45" value="14" step="1">
- <span id="valBallR" class="val">14</span>
- </div>
- <div class="param-group">
- <label>初速度幅度</label>
- <input id="paramV0" type="range" min="0" max="150" value="20" step="1">
- <span id="valV0" class="val">20</span>
- <span style="font-size:12px;">(px/s)</span>
- </div>
- <div id="btns">
- <button id="btnStart">Start</button>
- <button id="btnReset">Reset</button>
- </div>
- <div style="font-size:12px; color:#ccc; margin-top:7px;">
- 六边形旋转速度:0为不旋转,π=3.14,请微调。Start后可反复点击画布增加小球。
- </div>
- </div>
- <script>
- // 获取控件
- const el = s => document.querySelector(s);
- const canvas = el('#canvas');
- const ctx = canvas.getContext('2d');
- const center = {x: canvas.width/2, y: canvas.height/2};
- // 参数定义与UI同步
- let params = {
- gravity: 700, // px/s^2
- hexRadius: 200, // 六边形外接圆半径
- hexOmega: 0.52, // 旋转角速度,单位rad/s
- ballRadius: 14, // 小球半径
- v0: 20 // 初速(-v0到+v0)
- };
- /* 说明: 旋转速度滑块最大628, 实际对应6.28 (2π) rad/s, User输入52就是0.52 */
- function updateSliderValue(id, val, decimal=0) {
- el('#val'+id).textContent = decimal>0 ? (+val).toFixed(decimal): +val;
- }
- el('#paramG').addEventListener('input',e=>{
- params.gravity = Number(e.target.value);
- updateSliderValue('G',params.gravity)
- });
- el('#paramHexR').addEventListener('input',e=>{
- params.hexRadius = Number(e.target.value);
- updateSliderValue('HexR',params.hexRadius)
- });
- el('#paramAngV').addEventListener('input',e=>{
- params.hexOmega = Number(e.target.value)/100;
- updateSliderValue('AngV',params.hexOmega,2)
- });
- el('#paramBallR').addEventListener('input',e=>{
- params.ballRadius = Number(e.target.value);
- updateSliderValue('BallR',params.ballRadius)
- });
- el('#paramV0').addEventListener('input',e=>{
- params.v0 = Number(e.target.value);
- updateSliderValue('V0',params.v0)
- });
- // 初始化各值
- updateSliderValue('G',params.gravity);
- updateSliderValue('HexR',params.hexRadius);
- updateSliderValue('AngV',params.hexOmega,2);
- updateSliderValue('BallR',params.ballRadius);
- updateSliderValue('V0',params.v0);
- // 物理主体
- const hexSides = 6;
- let hexAngle = 0;
- // 支持多个小球
- let balls = [];
- let running = false;
- let lastTime = null;
- function getHexVertices(cx, cy, r, theta) {
- const vs = [];
- for(let i = 0; i<hexSides; i++){
- const ang = theta + i*2*Math.PI/hexSides;
- vs.push({x:cx + r*Math.cos(ang), y:cy + r*Math.sin(ang)});
- }
- return vs;
- }
- function closestPointOnSegment(px, py, x1, y1, x2, y2) {
- const dx = x2-x1, dy = y2-y1;
- if (dx===0 && dy===0) return {x:x1, y:y1};
- const t = Math.max(0, Math.min(1, ((px-x1)*dx + (py-y1)*dy)/(dx*dx+dy*dy)));
- return {x:x1+t*dx, y:y1+t*dy};
- }
- function drawHex(vertices) {
- ctx.save();
- ctx.beginPath();
- ctx.moveTo(vertices[0].x, vertices[0].y);
- for(let i=1;i<vertices.length;i++){
- ctx.lineTo(vertices[i].x, vertices[i].y);
- }
- ctx.closePath();
- ctx.lineWidth = 7;
- ctx.strokeStyle = "#FFEB3B";
- ctx.shadowColor = "#FFFCC5";
- ctx.shadowBlur = 17;
- ctx.stroke();
- ctx.restore();
- }
- function drawBall(ball) {
- ctx.save();
- ctx.beginPath();
- ctx.arc(ball.x, ball.y, ball.r, 0, 2*Math.PI);
- ctx.fillStyle = "#4FFFEE";
- ctx.shadowColor = "#4FFFF0";
- ctx.shadowBlur = 18;
- ctx.fill();
- ctx.strokeStyle = "#26A69A";
- ctx.lineWidth = 2;
- ctx.stroke();
- ctx.restore();
- }
- function draw() {
- ctx.clearRect(0,0,canvas.width,canvas.height);
- const vs = getHexVertices(center.x, center.y, params.hexRadius, hexAngle);
- drawHex(vs);
- balls.forEach(drawBall);
- }
- function updateBall(ball,dt,hexVs) {
- // 重力
- ball.vy += params.gravity*dt;
- // 预测
- let nextX = ball.x + ball.vx*dt, nextY = ball.y + ball.vy*dt;
- for(let i=0;i<hexSides;i++){
- const v1 = hexVs[i], v2 = hexVs[(i+1)%hexSides];
- const closest = closestPointOnSegment(nextX, nextY, v1.x,v1.y, v2.x,v2.y);
- const dx = nextX-closest.x, dy = nextY-closest.y;
- const dist = Math.sqrt(dx*dx+dy*dy);
- if(dist < ball.r){
- let nx=dx, ny=dy;
- const l=Math.sqrt(nx*nx+ny*ny);
- if(l===0)continue;
- nx/=l; ny/=l;
- const vDotN = ball.vx*nx + ball.vy*ny;
- ball.vx -= 2*vDotN*nx;
- ball.vy -= 2*vDotN*ny;
- nextX = closest.x + nx*ball.r*1.06;
- nextY = closest.y + ny*ball.r*1.06;
- }
- }
- ball.x = nextX; ball.y = nextY;
- }
- function animate(t){
- if(!running) return;
- if (!lastTime) lastTime = t;
- let dtTotal = Math.min((t - lastTime)/1000, 0.022);
- lastTime = t;
- let steps = 3;
- let dt = dtTotal / steps;
- for (let i = 0; i < steps; i++) {
- hexAngle += params.hexOmega * dt;
- hexAngle %= Math.PI * 2;
- const hexVs = getHexVertices(center.x, center.y, params.hexRadius, hexAngle);
- for (let ball of balls) updateBall(ball, dt, hexVs);
- }
- draw();
- requestAnimationFrame(animate);
- }
- function addBall(){
- let vx = (Math.random()-0.5)*2*params.v0;
- let vy = -Math.random()*params.v0/2;
- balls.push({
- x:center.x,
- y:center.y,
- vx:vx,
- vy:vy,
- r:params.ballRadius
- });
- }
- function resetSim(){
- running=false;
- balls=[];
- lastTime=null;
- hexAngle=0;
- draw();
- }
- // 事件绑定
- el('#btnStart').onclick=()=>{
- if(!running){
- resetSim();
- addBall();
- running=true;
- lastTime = null;
- requestAnimationFrame(animate);
- }
- };
- el('#btnReset').onclick=()=>{
- // 重置一切
- resetSim();
- };
- canvas.addEventListener('click',(e)=>{
- if(running){
- // 画布内坐标,可实现点击处出球,但这里始终从中心
- addBall();
- }
- });
- draw();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement