Advertisement
Guest User

Untitled

a guest
Apr 29th, 2025
57
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.35 KB | None | 0 0
  1.  
  2. <!DOCTYPE html>
  3. <html lang="zh">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>可调参数的旋转六边形抛球模拟</title>
  7. <style>
  8. body {
  9. margin: 0;
  10. background: #222;
  11. display:flex; flex-direction:column;
  12. align-items:center; justify-content:center;
  13. min-height: 100vh;
  14. }
  15. canvas {
  16. background: #222;
  17. box-shadow: 0 4px 32px #111;
  18. display:block;
  19. margin-bottom: 12px;
  20. }
  21.  
  22. /* UI美化 */
  23. #controls {
  24. background: #181818;
  25. border-radius: 8px;
  26. padding: 16px 22px 10px 22px;
  27. box-shadow: 0 2px 14px #0008;
  28. margin-bottom: 14px;
  29. color: #f0e6cc;
  30. min-width: 390px;
  31. font-family: sans-serif;
  32. }
  33.  
  34. .param-group {
  35. display: flex; align-items: center;
  36. margin-bottom: 10px;
  37. }
  38. .param-group label { width: 115px }
  39. .param-group input[type=range] { flex:1; margin: 0 8px; }
  40. .param-group .val { width:54px; text-align:right;}
  41.  
  42. #btns {
  43. text-align: right; padding-top: 5px;
  44. }
  45. button {
  46. margin-left: 10px;
  47. padding: 7px 21px;
  48. border-radius: 5px;
  49. border: none;
  50. background: #FFEB3B;
  51. color: #444;
  52. font-weight: bold;
  53. cursor: pointer;
  54. transition:.2s;
  55. font-size:17px;
  56. }
  57. button:active { box-shadow: inset 0 2px 8px #fff5,0 1px 2px #0004; }
  58. button[disabled] {
  59. filter: grayscale(70%);
  60. background:#999;
  61. color:#ddd;
  62. cursor:not-allowed;
  63. }
  64. </style>
  65. </head>
  66. <body>
  67. <canvas id="canvas" width="600" height="600"></canvas>
  68. <div id="controls">
  69. <div class="param-group">
  70. <label>重力 g (px/s²)</label>
  71. <input id="paramG" type="range" min="100" max="2000" value="700" step="1">
  72. <span id="valG" class="val">700</span>
  73. </div>
  74. <div class="param-group">
  75. <label>六边形边长</label>
  76. <input id="paramHexR" type="range" min="60" max="290" value="200" step="1">
  77. <span id="valHexR" class="val">200</span>
  78. </div>
  79. <div class="param-group">
  80. <label>六边形旋转速度</label>
  81. <input id="paramAngV" type="range" min="0" max="628" value="104" step="1">
  82. <span id="valAngV" class="val">0.52</span>
  83. <span style="font-size:12px;">(rad/s)</span>
  84. </div>
  85. <div class="param-group">
  86. <label>小球半径</label>
  87. <input id="paramBallR" type="range" min="7" max="45" value="14" step="1">
  88. <span id="valBallR" class="val">14</span>
  89. </div>
  90. <div class="param-group">
  91. <label>初速度幅度</label>
  92. <input id="paramV0" type="range" min="0" max="150" value="20" step="1">
  93. <span id="valV0" class="val">20</span>
  94. <span style="font-size:12px;">(px/s)</span>
  95. </div>
  96. <div id="btns">
  97. <button id="btnStart">Start</button>
  98. <button id="btnReset">Reset</button>
  99. </div>
  100. <div style="font-size:12px; color:#ccc; margin-top:7px;">
  101. 六边形旋转速度:0为不旋转,π=3.14,请微调。Start后可反复点击画布增加小球。
  102. </div>
  103. </div>
  104. <script>
  105. // 获取控件
  106. const el = s => document.querySelector(s);
  107. const canvas = el('#canvas');
  108. const ctx = canvas.getContext('2d');
  109. const center = {x: canvas.width/2, y: canvas.height/2};
  110.  
  111. // 参数定义与UI同步
  112. let params = {
  113. gravity: 700, // px/s^2
  114. hexRadius: 200, // 六边形外接圆半径
  115. hexOmega: 0.52, // 旋转角速度,单位rad/s
  116. ballRadius: 14, // 小球半径
  117. v0: 20 // 初速(-v0到+v0)
  118. };
  119. /* 说明: 旋转速度滑块最大628, 实际对应6.28 (2π) rad/s, User输入52就是0.52 */
  120. function updateSliderValue(id, val, decimal=0) {
  121. el('#val'+id).textContent = decimal>0 ? (+val).toFixed(decimal): +val;
  122. }
  123. el('#paramG').addEventListener('input',e=>{
  124. params.gravity = Number(e.target.value);
  125. updateSliderValue('G',params.gravity)
  126. });
  127. el('#paramHexR').addEventListener('input',e=>{
  128. params.hexRadius = Number(e.target.value);
  129. updateSliderValue('HexR',params.hexRadius)
  130. });
  131. el('#paramAngV').addEventListener('input',e=>{
  132. params.hexOmega = Number(e.target.value)/100;
  133. updateSliderValue('AngV',params.hexOmega,2)
  134. });
  135. el('#paramBallR').addEventListener('input',e=>{
  136. params.ballRadius = Number(e.target.value);
  137. updateSliderValue('BallR',params.ballRadius)
  138. });
  139. el('#paramV0').addEventListener('input',e=>{
  140. params.v0 = Number(e.target.value);
  141. updateSliderValue('V0',params.v0)
  142. });
  143. // 初始化各值
  144. updateSliderValue('G',params.gravity);
  145. updateSliderValue('HexR',params.hexRadius);
  146. updateSliderValue('AngV',params.hexOmega,2);
  147. updateSliderValue('BallR',params.ballRadius);
  148. updateSliderValue('V0',params.v0);
  149.  
  150. // 物理主体
  151. const hexSides = 6;
  152. let hexAngle = 0;
  153.  
  154. // 支持多个小球
  155. let balls = [];
  156. let running = false;
  157. let lastTime = null;
  158.  
  159. function getHexVertices(cx, cy, r, theta) {
  160. const vs = [];
  161. for(let i = 0; i<hexSides; i++){
  162. const ang = theta + i*2*Math.PI/hexSides;
  163. vs.push({x:cx + r*Math.cos(ang), y:cy + r*Math.sin(ang)});
  164. }
  165. return vs;
  166. }
  167. function closestPointOnSegment(px, py, x1, y1, x2, y2) {
  168. const dx = x2-x1, dy = y2-y1;
  169. if (dx===0 && dy===0) return {x:x1, y:y1};
  170. const t = Math.max(0, Math.min(1, ((px-x1)*dx + (py-y1)*dy)/(dx*dx+dy*dy)));
  171. return {x:x1+t*dx, y:y1+t*dy};
  172. }
  173. function drawHex(vertices) {
  174. ctx.save();
  175. ctx.beginPath();
  176. ctx.moveTo(vertices[0].x, vertices[0].y);
  177. for(let i=1;i<vertices.length;i++){
  178. ctx.lineTo(vertices[i].x, vertices[i].y);
  179. }
  180. ctx.closePath();
  181. ctx.lineWidth = 7;
  182. ctx.strokeStyle = "#FFEB3B";
  183. ctx.shadowColor = "#FFFCC5";
  184. ctx.shadowBlur = 17;
  185. ctx.stroke();
  186. ctx.restore();
  187. }
  188. function drawBall(ball) {
  189. ctx.save();
  190. ctx.beginPath();
  191. ctx.arc(ball.x, ball.y, ball.r, 0, 2*Math.PI);
  192. ctx.fillStyle = "#4FFFEE";
  193. ctx.shadowColor = "#4FFFF0";
  194. ctx.shadowBlur = 18;
  195. ctx.fill();
  196. ctx.strokeStyle = "#26A69A";
  197. ctx.lineWidth = 2;
  198. ctx.stroke();
  199. ctx.restore();
  200. }
  201.  
  202. function draw() {
  203. ctx.clearRect(0,0,canvas.width,canvas.height);
  204. const vs = getHexVertices(center.x, center.y, params.hexRadius, hexAngle);
  205. drawHex(vs);
  206. balls.forEach(drawBall);
  207. }
  208.  
  209. function updateBall(ball,dt,hexVs) {
  210. // 重力
  211. ball.vy += params.gravity*dt;
  212. // 预测
  213. let nextX = ball.x + ball.vx*dt, nextY = ball.y + ball.vy*dt;
  214. for(let i=0;i<hexSides;i++){
  215. const v1 = hexVs[i], v2 = hexVs[(i+1)%hexSides];
  216. const closest = closestPointOnSegment(nextX, nextY, v1.x,v1.y, v2.x,v2.y);
  217. const dx = nextX-closest.x, dy = nextY-closest.y;
  218. const dist = Math.sqrt(dx*dx+dy*dy);
  219. if(dist < ball.r){
  220. let nx=dx, ny=dy;
  221. const l=Math.sqrt(nx*nx+ny*ny);
  222. if(l===0)continue;
  223. nx/=l; ny/=l;
  224. const vDotN = ball.vx*nx + ball.vy*ny;
  225. ball.vx -= 2*vDotN*nx;
  226. ball.vy -= 2*vDotN*ny;
  227. nextX = closest.x + nx*ball.r*1.06;
  228. nextY = closest.y + ny*ball.r*1.06;
  229. }
  230. }
  231. ball.x = nextX; ball.y = nextY;
  232. }
  233.  
  234. function animate(t){
  235. if(!running) return;
  236. if (!lastTime) lastTime = t;
  237. let dtTotal = Math.min((t - lastTime)/1000, 0.022);
  238. lastTime = t;
  239. let steps = 3;
  240. let dt = dtTotal / steps;
  241. for (let i = 0; i < steps; i++) {
  242. hexAngle += params.hexOmega * dt;
  243. hexAngle %= Math.PI * 2;
  244. const hexVs = getHexVertices(center.x, center.y, params.hexRadius, hexAngle);
  245. for (let ball of balls) updateBall(ball, dt, hexVs);
  246. }
  247. draw();
  248. requestAnimationFrame(animate);
  249. }
  250.  
  251. function addBall(){
  252. let vx = (Math.random()-0.5)*2*params.v0;
  253. let vy = -Math.random()*params.v0/2;
  254. balls.push({
  255. x:center.x,
  256. y:center.y,
  257. vx:vx,
  258. vy:vy,
  259. r:params.ballRadius
  260. });
  261. }
  262. function resetSim(){
  263. running=false;
  264. balls=[];
  265. lastTime=null;
  266. hexAngle=0;
  267. draw();
  268. }
  269.  
  270. // 事件绑定
  271. el('#btnStart').onclick=()=>{
  272. if(!running){
  273. resetSim();
  274. addBall();
  275. running=true;
  276. lastTime = null;
  277. requestAnimationFrame(animate);
  278. }
  279. };
  280. el('#btnReset').onclick=()=>{
  281. // 重置一切
  282. resetSim();
  283. };
  284.  
  285. canvas.addEventListener('click',(e)=>{
  286. if(running){
  287. // 画布内坐标,可实现点击处出球,但这里始终从中心
  288. addBall();
  289. }
  290. });
  291.  
  292. draw();
  293. </script>
  294. </body>
  295. </html>
  296.  
  297.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement