Skip to content

Instantly share code, notes, and snippets.

@bustesoul
Created April 29, 2025 09:20
Show Gist options
  • Save bustesoul/addc0606fce88d920e4997885ee2923a to your computer and use it in GitHub Desktop.
Save bustesoul/addc0606fce88d920e4997885ee2923a to your computer and use it in GitHub Desktop.
ball_from_gpt4.1.html
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment