HTML5倾斜迷宫小游戏

HTML源代码:

<!DOCTYPE html>
<html lang="en" >
<head>
    <meta charset="UTF-8">
    <title>HTML5倾斜迷宫</title>

    <link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="center">
    <div id="game">
        <div id="maze">
            <div id="end"></div>
        </div>
        <div id="joystick">
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div id="joystick-head"></div>
        </div>
        <div id="note">
            点击操纵杆开始!
            <p>把每个球移到中心。准备好进入硬模式了吗?按H</p>
        </div>
    </div>
</div>
<div id="youtube-card">
    如何用JavaScript模拟迷宫中的球运动
</div>
<script  src="js/script.js"></script>
</body>
</html>

CSS(css/style.css)源代码:

  1 body {
  2   /* https://coolors.co/f06449-ede6e3-7d82b8-36382e-613f75  */
  3   --background-color: #ede6e3;
  4   --wall-color: #36382e;
  5   --joystick-color: #210124;
  6   --joystick-head-color: #f06449;
  7   --ball-color: #f06449;
  8   --end-color: #7d82b8;
  9   --text-color: #210124;
 10 
 11   font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
 12   background-color: var(--background-color);
 13 }
 14 
 15 html,
 16 body {
 17   height: 100%;
 18   margin: 0;
 19 }
 20 
 21 #center {
 22   display: flex;
 23   align-items: center;
 24   justify-content: center;
 25   height: 100%;
 26 }
 27 
 28 #game {
 29   display: grid;
 30   grid-template-columns: auto 150px;
 31   grid-template-rows: 1fr auto 1fr;
 32   gap: 30px;
 33   perspective: 600px;
 34 }
 35 
 36 #maze {
 37   position: relative;
 38   grid-row: 1 / -1;
 39   grid-column: 1;
 40   width: 350px;
 41   height: 315px;
 42   display: flex;
 43   justify-content: center;
 44   align-items: center;
 45 }
 46 
 47 #end {
 48   width: 65px;
 49   height: 65px;
 50   border: 5px dashed var(--end-color);
 51   border-radius: 50%;
 52 }
 53 
 54 #joystick {
 55   position: relative;
 56   background-color: var(--joystick-color);
 57   border-radius: 50%;
 58   width: 50px;
 59   height: 50px;
 60   display: flex;
 61   align-items: center;
 62   justify-content: center;
 63   margin: 10px 50px;
 64   grid-row: 2;
 65 }
 66 
 67 #joystick-head {
 68   position: relative;
 69   background-color: var(--joystick-head-color);
 70   border-radius: 50%;
 71   width: 20px;
 72   height: 20px;
 73   cursor: grab;
 74 
 75   animation-name: glow;
 76   animation-duration: 0.6s;
 77   animation-iteration-count: infinite;
 78   animation-direction: alternate;
 79   animation-timing-function: ease-in-out;
 80   animation-delay: 4s;
 81 }
 82 
 83 @keyframes glow {
 84   0% {
 85     transform: scale(1);
 86   }
 87   100% {
 88     transform: scale(1.2);
 89   }
 90 }
 91 
 92 .joystick-arrow:nth-of-type(1) {
 93   position: absolute;
 94   bottom: 55px;
 95 
 96   width: 0;
 97   height: 0;
 98   border-left: 10px solid transparent;
 99   border-right: 10px solid transparent;
100 
101   border-bottom: 10px solid var(--joystick-color);
102 }
103 
104 .joystick-arrow:nth-of-type(2) {
105   position: absolute;
106   top: 55px;
107 
108   width: 0;
109   height: 0;
110   border-left: 10px solid transparent;
111   border-right: 10px solid transparent;
112 
113   border-top: 10px solid var(--joystick-color);
114 }
115 
116 .joystick-arrow:nth-of-type(3) {
117   position: absolute;
118   left: 55px;
119 
120   width: 0;
121   height: 0;
122   border-top: 10px solid transparent;
123   border-bottom: 10px solid transparent;
124 
125   border-left: 10px solid var(--joystick-color);
126 }
127 
128 .joystick-arrow:nth-of-type(4) {
129   position: absolute;
130   right: 55px;
131 
132   width: 0;
133   height: 0;
134   border-top: 10px solid transparent;
135   border-bottom: 10px solid transparent;
136 
137   border-right: 10px solid var(--joystick-color);
138 }
139 
140 #note {
141   grid-row: 3;
142   grid-column: 2;
143   text-align: center;
144   font-size: 0.8em;
145   color: var(--text-color);
146   transition: opacity 2s;
147 }
148 
149 a:visited {
150   color: inherit;
151 }
152 
153 .ball {
154   position: absolute;
155   margin-top: -5px;
156   margin-left: -5px;
157   border-radius: 50%;
158   background-color: var(--ball-color);
159   width: 10px;
160   height: 10px;
161 }
162 
163 .wall {
164   position: absolute;
165   background-color: var(--wall-color);
166   transform-origin: top center;
167   margin-left: -5px;
168 }
169 
170 .wall::before,
171 .wall::after {
172   display: block;
173   content: "";
174   width: 10px;
175   height: 10px;
176   background-color: inherit;
177   border-radius: 50%;
178   position: absolute;
179 }
180 
181 .wall::before {
182   top: -5px;
183 }
184 
185 .wall::after {
186   bottom: -5px;
187 }
188 
189 .black-hole {
190   position: absolute;
191   margin-top: -9px;
192   margin-left: -9px;
193   border-radius: 50%;
194   background-color: black;
195   width: 18px;
196   height: 18px;
197 }
198 
199 #youtube,
200 #youtube-card {
201   display: none;
202 }
203 
204 @media (min-height: 425px) {
205   /** Youtube logo by https://codepen.io/alvaromontoro */
206   #youtube {
207     z-index: 2;
208     display: block;
209     width: 100px;
210     height: 70px;
211     position: absolute;
212     bottom: 20px;
213     right: 20px;
214     background: red;
215     border-radius: 50% / 11%;
216     transform: scale(0.8);
217     transition: transform 0.5s;
218   }
219 
220   #youtube:hover,
221   #youtube:focus {
222     transform: scale(0.9);
223   }
224 
225   #youtube::before {
226     content: "";
227     display: block;
228     position: absolute;
229     top: 7.5%;
230     left: -6%;
231     width: 112%;
232     height: 85%;
233     background: red;
234     border-radius: 9% / 50%;
235   }
236 
237   #youtube::after {
238     content: "";
239     display: block;
240     position: absolute;
241     top: 20px;
242     left: 40px;
243     width: 45px;
244     height: 30px;
245     border: 15px solid transparent;
246     box-sizing: border-box;
247     border-left: 30px solid white;
248   }
249 
250   #youtube span {
251     font-size: 0;
252     position: absolute;
253     width: 0;
254     height: 0;
255     overflow: hidden;
256   }
257 
258   #youtube:hover + #youtube-card {
259     display: block;
260     position: absolute;
261     bottom: 12px;
262     right: 10px;
263     padding: 25px 130px 25px 25px;
264     width: 300px;
265     background-color: white;
266   }
267 }

JS(js/script.js)源代码:

  1 /*
  2 
  3 If you want to know how this game works, you can find a source code walkthrough video here: https://youtu.be/bTk6dcAckuI
  4 
  5 Follow me on twitter for more: https://twitter.com/HunorBorbely
  6 
  7 */
  8 
  9 Math.minmax = (value, limit) => {
 10   return Math.max(Math.min(value, limit), -limit);
 11 };
 12 
 13 const distance2D = (p1, p2) => {
 14   return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
 15 };
 16 
 17 // Angle between the two points
 18 const getAngle = (p1, p2) => {
 19   let angle = Math.atan((p2.y - p1.y) / (p2.x - p1.x));
 20   if (p2.x - p1.x < 0) angle += Math.PI;
 21   return angle;
 22 };
 23 
 24 // The closest a ball and a wall cap can be
 25 const closestItCanBe = (cap, ball) => {
 26   let angle = getAngle(cap, ball);
 27 
 28   const deltaX = Math.cos(angle) * (wallW / 2 + ballSize / 2);
 29   const deltaY = Math.sin(angle) * (wallW / 2 + ballSize / 2);
 30 
 31   return { x: cap.x + deltaX, y: cap.y + deltaY };
 32 };
 33 
 34 // Roll the ball around the wall cap
 35 const rollAroundCap = (cap, ball) => {
 36   // The direction the ball can't move any further because the wall holds it back
 37   let impactAngle = getAngle(ball, cap);
 38 
 39   // The direction the ball wants to move based on it's velocity
 40   let heading = getAngle(
 41     { x: 0, y: 0 },
 42     { x: ball.velocityX, y: ball.velocityY }
 43   );
 44 
 45   // The angle between the impact direction and the ball's desired direction
 46   // The smaller this angle is, the bigger the impact
 47   // The closer it is to 90 degrees the smoother it gets (at 90 there would be no collision)
 48   let impactHeadingAngle = impactAngle - heading;
 49 
 50   // Velocity distance if not hit would have occurred
 51   const velocityMagnitude = distance2D(
 52     { x: 0, y: 0 },
 53     { x: ball.velocityX, y: ball.velocityY }
 54   );
 55   // Velocity component diagonal to the impact
 56   const velocityMagnitudeDiagonalToTheImpact =
 57     Math.sin(impactHeadingAngle) * velocityMagnitude;
 58 
 59   // How far should the ball be from the wall cap
 60   const closestDistance = wallW / 2 + ballSize / 2;
 61 
 62   const rotationAngle = Math.atan(
 63     velocityMagnitudeDiagonalToTheImpact / closestDistance
 64   );
 65 
 66   const deltaFromCap = {
 67     x: Math.cos(impactAngle + Math.PI - rotationAngle) * closestDistance,
 68     y: Math.sin(impactAngle + Math.PI - rotationAngle) * closestDistance
 69   };
 70 
 71   const x = ball.x;
 72   const y = ball.y;
 73   const velocityX = ball.x - (cap.x + deltaFromCap.x);
 74   const velocityY = ball.y - (cap.y + deltaFromCap.y);
 75   const nextX = x + velocityX;
 76   const nextY = y + velocityY;
 77 
 78   return { x, y, velocityX, velocityY, nextX, nextY };
 79 };
 80 
 81 // Decreases the absolute value of a number but keeps it's sign, doesn't go below abs 0
 82 const slow = (number, difference) => {
 83   if (Math.abs(number) <= difference) return 0;
 84   if (number > difference) return number - difference;
 85   return number + difference;
 86 };
 87 
 88 const mazeElement = document.getElementById("maze");
 89 const joystickHeadElement = document.getElementById("joystick-head");
 90 const noteElement = document.getElementById("note"); // Note element for instructions and game won, game failed texts
 91 
 92 let hardMode = false;
 93 let previousTimestamp;
 94 let gameInProgress;
 95 let mouseStartX;
 96 let mouseStartY;
 97 let accelerationX;
 98 let accelerationY;
 99 let frictionX;
100 let frictionY;
101 
102 const pathW = 25; // Path width
103 const wallW = 10; // Wall width
104 const ballSize = 10; // Width and height of the ball
105 const holeSize = 18;
106 
107 const debugMode = false;
108 
109 let balls = [];
110 let ballElements = [];
111 let holeElements = [];
112 
113 resetGame();
114 
115 // Draw balls for the first time
116 balls.forEach(({ x, y }) => {
117   const ball = document.createElement("div");
118   ball.setAttribute("class", "ball");
119   ball.style.cssText = `left: ${x}px; top: ${y}px; `;
120 
121   mazeElement.appendChild(ball);
122   ballElements.push(ball);
123 });
124 
125 // Wall metadata
126 const walls = [
127   // Border
128   { column: 0, row: 0, horizontal: true, length: 10 },
129   { column: 0, row: 0, horizontal: false, length: 9 },
130   { column: 0, row: 9, horizontal: true, length: 10 },
131   { column: 10, row: 0, horizontal: false, length: 9 },
132 
133   // Horizontal lines starting in 1st column
134   { column: 0, row: 6, horizontal: true, length: 1 },
135   { column: 0, row: 8, horizontal: true, length: 1 },
136 
137   // Horizontal lines starting in 2nd column
138   { column: 1, row: 1, horizontal: true, length: 2 },
139   { column: 1, row: 7, horizontal: true, length: 1 },
140 
141   // Horizontal lines starting in 3rd column
142   { column: 2, row: 2, horizontal: true, length: 2 },
143   { column: 2, row: 4, horizontal: true, length: 1 },
144   { column: 2, row: 5, horizontal: true, length: 1 },
145   { column: 2, row: 6, horizontal: true, length: 1 },
146 
147   // Horizontal lines starting in 4th column
148   { column: 3, row: 3, horizontal: true, length: 1 },
149   { column: 3, row: 8, horizontal: true, length: 3 },
150 
151   // Horizontal lines starting in 5th column
152   { column: 4, row: 6, horizontal: true, length: 1 },
153 
154   // Horizontal lines starting in 6th column
155   { column: 5, row: 2, horizontal: true, length: 2 },
156   { column: 5, row: 7, horizontal: true, length: 1 },
157 
158   // Horizontal lines starting in 7th column
159   { column: 6, row: 1, horizontal: true, length: 1 },
160   { column: 6, row: 6, horizontal: true, length: 2 },
161 
162   // Horizontal lines starting in 8th column
163   { column: 7, row: 3, horizontal: true, length: 2 },
164   { column: 7, row: 7, horizontal: true, length: 2 },
165 
166   // Horizontal lines starting in 9th column
167   { column: 8, row: 1, horizontal: true, length: 1 },
168   { column: 8, row: 2, horizontal: true, length: 1 },
169   { column: 8, row: 3, horizontal: true, length: 1 },
170   { column: 8, row: 4, horizontal: true, length: 2 },
171   { column: 8, row: 8, horizontal: true, length: 2 },
172 
173   // Vertical lines after the 1st column
174   { column: 1, row: 1, horizontal: false, length: 2 },
175   { column: 1, row: 4, horizontal: false, length: 2 },
176 
177   // Vertical lines after the 2nd column
178   { column: 2, row: 2, horizontal: false, length: 2 },
179   { column: 2, row: 5, horizontal: false, length: 1 },
180   { column: 2, row: 7, horizontal: false, length: 2 },
181 
182   // Vertical lines after the 3rd column
183   { column: 3, row: 0, horizontal: false, length: 1 },
184   { column: 3, row: 4, horizontal: false, length: 1 },
185   { column: 3, row: 6, horizontal: false, length: 2 },
186 
187   // Vertical lines after the 4th column
188   { column: 4, row: 1, horizontal: false, length: 2 },
189   { column: 4, row: 6, horizontal: false, length: 1 },
190 
191   // Vertical lines after the 5th column
192   { column: 5, row: 0, horizontal: false, length: 2 },
193   { column: 5, row: 6, horizontal: false, length: 1 },
194   { column: 5, row: 8, horizontal: false, length: 1 },
195 
196   // Vertical lines after the 6th column
197   { column: 6, row: 4, horizontal: false, length: 1 },
198   { column: 6, row: 6, horizontal: false, length: 1 },
199 
200   // Vertical lines after the 7th column
201   { column: 7, row: 1, horizontal: false, length: 4 },
202   { column: 7, row: 7, horizontal: false, length: 2 },
203 
204   // Vertical lines after the 8th column
205   { column: 8, row: 2, horizontal: false, length: 1 },
206   { column: 8, row: 4, horizontal: false, length: 2 },
207 
208   // Vertical lines after the 9th column
209   { column: 9, row: 1, horizontal: false, length: 1 },
210   { column: 9, row: 5, horizontal: false, length: 2 }
211 ].map((wall) => ({
212   x: wall.column * (pathW + wallW),
213   y: wall.row * (pathW + wallW),
214   horizontal: wall.horizontal,
215   length: wall.length * (pathW + wallW)
216 }));
217 
218 // Draw walls
219 walls.forEach(({ x, y, horizontal, length }) => {
220   const wall = document.createElement("div");
221   wall.setAttribute("class", "wall");
222   wall.style.cssText = `
223       left: ${x}px;
224       top: ${y}px;
225       width: ${wallW}px;
226       height: ${length}px;
227       transform: rotate(${horizontal ? -90 : 0}deg);
228     `;
229 
230   mazeElement.appendChild(wall);
231 });
232 
233 const holes = [
234   { column: 0, row: 5 },
235   { column: 2, row: 0 },
236   { column: 2, row: 4 },
237   { column: 4, row: 6 },
238   { column: 6, row: 2 },
239   { column: 6, row: 8 },
240   { column: 8, row: 1 },
241   { column: 8, row: 2 }
242 ].map((hole) => ({
243   x: hole.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
244   y: hole.row * (wallW + pathW) + (wallW / 2 + pathW / 2)
245 }));
246 
247 joystickHeadElement.addEventListener("mousedown", function (event) {
248   if (!gameInProgress) {
249     mouseStartX = event.clientX;
250     mouseStartY = event.clientY;
251     gameInProgress = true;
252     window.requestAnimationFrame(main);
253     noteElement.style.opacity = 0;
254     joystickHeadElement.style.cssText = `
255         animation: none;
256         cursor: grabbing;
257       `;
258   }
259 });
260 
261 window.addEventListener("mousemove", function (event) {
262   if (gameInProgress) {
263     const mouseDeltaX = -Math.minmax(mouseStartX - event.clientX, 15);
264     const mouseDeltaY = -Math.minmax(mouseStartY - event.clientY, 15);
265 
266     joystickHeadElement.style.cssText = `
267         left: ${mouseDeltaX}px;
268         top: ${mouseDeltaY}px;
269         animation: none;
270         cursor: grabbing;
271       `;
272 
273     const rotationY = mouseDeltaX * 0.8; // Max rotation = 12
274     const rotationX = mouseDeltaY * 0.8;
275 
276     mazeElement.style.cssText = `
277         transform: rotateY(${rotationY}deg) rotateX(${-rotationX}deg)
278       `;
279 
280     const gravity = 2;
281     const friction = 0.01; // Coefficients of friction
282 
283     accelerationX = gravity * Math.sin((rotationY / 180) * Math.PI);
284     accelerationY = gravity * Math.sin((rotationX / 180) * Math.PI);
285     frictionX = gravity * Math.cos((rotationY / 180) * Math.PI) * friction;
286     frictionY = gravity * Math.cos((rotationX / 180) * Math.PI) * friction;
287   }
288 });
289 
290 window.addEventListener("keydown", function (event) {
291   // If not an arrow key or space or H was pressed then return
292   if (![" ", "H", "h", "E", "e"].includes(event.key)) return;
293 
294   // If an arrow key was pressed then first prevent default
295   event.preventDefault();
296 
297   // If space was pressed restart the game
298   if (event.key == " ") {
299     resetGame();
300     return;
301   }
302 
303   // Set Hard mode
304   if (event.key == "H" || event.key == "h") {
305     hardMode = true;
306     resetGame();
307     return;
308   }
309 
310   // Set Easy mode
311   if (event.key == "E" || event.key == "e") {
312     hardMode = false;
313     resetGame();
314     return;
315   }
316 });
317 
318 function resetGame() {
319   previousTimestamp = undefined;
320   gameInProgress = false;
321   mouseStartX = undefined;
322   mouseStartY = undefined;
323   accelerationX = undefined;
324   accelerationY = undefined;
325   frictionX = undefined;
326   frictionY = undefined;
327 
328   mazeElement.style.cssText = `
329       transform: rotateY(0deg) rotateX(0deg)
330     `;
331 
332   joystickHeadElement.style.cssText = `
333       left: 0;
334       top: 0;
335       animation: glow;
336       cursor: grab;
337     `;
338 
339   if (hardMode) {
340     noteElement.innerHTML = `Click the joystick to start!
341         <p>Hard mode, Avoid black holes. Back to easy mode? Press E</p>`;
342   } else {
343     noteElement.innerHTML = `Click the joystick to start!
344         <p>Move every ball to the center. Ready for hard mode? Press H</p>`;
345   }
346   noteElement.style.opacity = 1;
347 
348   balls = [
349     { column: 0, row: 0 },
350     { column: 9, row: 0 },
351     { column: 0, row: 8 },
352     { column: 9, row: 8 }
353   ].map((ball) => ({
354     x: ball.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
355     y: ball.row * (wallW + pathW) + (wallW / 2 + pathW / 2),
356     velocityX: 0,
357     velocityY: 0
358   }));
359 
360   if (ballElements.length) {
361     balls.forEach(({ x, y }, index) => {
362       ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
363     });
364   }
365 
366   // Remove previous hole elements
367   holeElements.forEach((holeElement) => {
368     mazeElement.removeChild(holeElement);
369   });
370   holeElements = [];
371 
372   // Reset hole elements if hard mode
373   if (hardMode) {
374     holes.forEach(({ x, y }) => {
375       const ball = document.createElement("div");
376       ball.setAttribute("class", "black-hole");
377       ball.style.cssText = `left: ${x}px; top: ${y}px; `;
378 
379       mazeElement.appendChild(ball);
380       holeElements.push(ball);
381     });
382   }
383 }
384 
385 function main(timestamp) {
386   // It is possible to reset the game mid-game. This case the look should stop
387   if (!gameInProgress) return;
388 
389   if (previousTimestamp === undefined) {
390     previousTimestamp = timestamp;
391     window.requestAnimationFrame(main);
392     return;
393   }
394 
395   const maxVelocity = 1.5;
396 
397   // Time passed since last cycle divided by 16
398   // This function gets called every 16 ms on average so dividing by 16 will result in 1
399   const timeElapsed = (timestamp - previousTimestamp) / 16;
400 
401   try {
402     // If mouse didn't move yet don't do anything
403     if (accelerationX != undefined && accelerationY != undefined) {
404       const velocityChangeX = accelerationX * timeElapsed;
405       const velocityChangeY = accelerationY * timeElapsed;
406       const frictionDeltaX = frictionX * timeElapsed;
407       const frictionDeltaY = frictionY * timeElapsed;
408 
409       balls.forEach((ball) => {
410         if (velocityChangeX == 0) {
411           // No rotation, the plane is flat
412           // On flat surface friction can only slow down, but not reverse movement
413           ball.velocityX = slow(ball.velocityX, frictionDeltaX);
414         } else {
415           ball.velocityX = ball.velocityX + velocityChangeX;
416           ball.velocityX = Math.max(Math.min(ball.velocityX, 1.5), -1.5);
417           ball.velocityX =
418             ball.velocityX - Math.sign(velocityChangeX) * frictionDeltaX;
419           ball.velocityX = Math.minmax(ball.velocityX, maxVelocity);
420         }
421 
422         if (velocityChangeY == 0) {
423           // No rotation, the plane is flat
424           // On flat surface friction can only slow down, but not reverse movement
425           ball.velocityY = slow(ball.velocityY, frictionDeltaY);
426         } else {
427           ball.velocityY = ball.velocityY + velocityChangeY;
428           ball.velocityY =
429             ball.velocityY - Math.sign(velocityChangeY) * frictionDeltaY;
430           ball.velocityY = Math.minmax(ball.velocityY, maxVelocity);
431         }
432 
433         // Preliminary next ball position, only becomes true if no hit occurs
434         // Used only for hit testing, does not mean that the ball will reach this position
435         ball.nextX = ball.x + ball.velocityX;
436         ball.nextY = ball.y + ball.velocityY;
437 
438         if (debugMode) console.log("tick", ball);
439 
440         walls.forEach((wall, wi) => {
441           if (wall.horizontal) {
442             // Horizontal wall
443 
444             if (
445               ball.nextY + ballSize / 2 >= wall.y - wallW / 2 &&
446               ball.nextY - ballSize / 2 <= wall.y + wallW / 2
447             ) {
448               // Ball got within the strip of the wall
449               // (not necessarily hit it, could be before or after)
450 
451               const wallStart = {
452                 x: wall.x,
453                 y: wall.y
454               };
455               const wallEnd = {
456                 x: wall.x + wall.length,
457                 y: wall.y
458               };
459 
460               if (
461                 ball.nextX + ballSize / 2 >= wallStart.x - wallW / 2 &&
462                 ball.nextX < wallStart.x
463               ) {
464                 // Ball might hit the left cap of a horizontal wall
465                 const distance = distance2D(wallStart, {
466                   x: ball.nextX,
467                   y: ball.nextY
468                 });
469                 if (distance < ballSize / 2 + wallW / 2) {
470                   if (debugMode && wi > 4)
471                     console.warn("too close h head", distance, ball);
472 
473                   // Ball hits the left cap of a horizontal wall
474                   const closest = closestItCanBe(wallStart, {
475                     x: ball.nextX,
476                     y: ball.nextY
477                   });
478                   const rolled = rollAroundCap(wallStart, {
479                     x: closest.x,
480                     y: closest.y,
481                     velocityX: ball.velocityX,
482                     velocityY: ball.velocityY
483                   });
484 
485                   Object.assign(ball, rolled);
486                 }
487               }
488 
489               if (
490                 ball.nextX - ballSize / 2 <= wallEnd.x + wallW / 2 &&
491                 ball.nextX > wallEnd.x
492               ) {
493                 // Ball might hit the right cap of a horizontal wall
494                 const distance = distance2D(wallEnd, {
495                   x: ball.nextX,
496                   y: ball.nextY
497                 });
498                 if (distance < ballSize / 2 + wallW / 2) {
499                   if (debugMode && wi > 4)
500                     console.warn("too close h tail", distance, ball);
501 
502                   // Ball hits the right cap of a horizontal wall
503                   const closest = closestItCanBe(wallEnd, {
504                     x: ball.nextX,
505                     y: ball.nextY
506                   });
507                   const rolled = rollAroundCap(wallEnd, {
508                     x: closest.x,
509                     y: closest.y,
510                     velocityX: ball.velocityX,
511                     velocityY: ball.velocityY
512                   });
513 
514                   Object.assign(ball, rolled);
515                 }
516               }
517 
518               if (ball.nextX >= wallStart.x && ball.nextX <= wallEnd.x) {
519                 // The ball got inside the main body of the wall
520                 if (ball.nextY < wall.y) {
521                   // Hit horizontal wall from top
522                   ball.nextY = wall.y - wallW / 2 - ballSize / 2;
523                 } else {
524                   // Hit horizontal wall from bottom
525                   ball.nextY = wall.y + wallW / 2 + ballSize / 2;
526                 }
527                 ball.y = ball.nextY;
528                 ball.velocityY = -ball.velocityY / 3;
529 
530                 if (debugMode && wi > 4)
531                   console.error("crossing h line, HIT", ball);
532               }
533             }
534           } else {
535             // Vertical wall
536 
537             if (
538               ball.nextX + ballSize / 2 >= wall.x - wallW / 2 &&
539               ball.nextX - ballSize / 2 <= wall.x + wallW / 2
540             ) {
541               // Ball got within the strip of the wall
542               // (not necessarily hit it, could be before or after)
543 
544               const wallStart = {
545                 x: wall.x,
546                 y: wall.y
547               };
548               const wallEnd = {
549                 x: wall.x,
550                 y: wall.y + wall.length
551               };
552 
553               if (
554                 ball.nextY + ballSize / 2 >= wallStart.y - wallW / 2 &&
555                 ball.nextY < wallStart.y
556               ) {
557                 // Ball might hit the top cap of a horizontal wall
558                 const distance = distance2D(wallStart, {
559                   x: ball.nextX,
560                   y: ball.nextY
561                 });
562                 if (distance < ballSize / 2 + wallW / 2) {
563                   if (debugMode && wi > 4)
564                     console.warn("too close v head", distance, ball);
565 
566                   // Ball hits the left cap of a horizontal wall
567                   const closest = closestItCanBe(wallStart, {
568                     x: ball.nextX,
569                     y: ball.nextY
570                   });
571                   const rolled = rollAroundCap(wallStart, {
572                     x: closest.x,
573                     y: closest.y,
574                     velocityX: ball.velocityX,
575                     velocityY: ball.velocityY
576                   });
577 
578                   Object.assign(ball, rolled);
579                 }
580               }
581 
582               if (
583                 ball.nextY - ballSize / 2 <= wallEnd.y + wallW / 2 &&
584                 ball.nextY > wallEnd.y
585               ) {
586                 // Ball might hit the bottom cap of a horizontal wall
587                 const distance = distance2D(wallEnd, {
588                   x: ball.nextX,
589                   y: ball.nextY
590                 });
591                 if (distance < ballSize / 2 + wallW / 2) {
592                   if (debugMode && wi > 4)
593                     console.warn("too close v tail", distance, ball);
594 
595                   // Ball hits the right cap of a horizontal wall
596                   const closest = closestItCanBe(wallEnd, {
597                     x: ball.nextX,
598                     y: ball.nextY
599                   });
600                   const rolled = rollAroundCap(wallEnd, {
601                     x: closest.x,
602                     y: closest.y,
603                     velocityX: ball.velocityX,
604                     velocityY: ball.velocityY
605                   });
606 
607                   Object.assign(ball, rolled);
608                 }
609               }
610 
611               if (ball.nextY >= wallStart.y && ball.nextY <= wallEnd.y) {
612                 // The ball got inside the main body of the wall
613                 if (ball.nextX < wall.x) {
614                   // Hit vertical wall from left
615                   ball.nextX = wall.x - wallW / 2 - ballSize / 2;
616                 } else {
617                   // Hit vertical wall from right
618                   ball.nextX = wall.x + wallW / 2 + ballSize / 2;
619                 }
620                 ball.x = ball.nextX;
621                 ball.velocityX = -ball.velocityX / 3;
622 
623                 if (debugMode && wi > 4)
624                   console.error("crossing v line, HIT", ball);
625               }
626             }
627           }
628         });
629 
630         // Detect is a ball fell into a hole
631         if (hardMode) {
632           holes.forEach((hole, hi) => {
633             const distance = distance2D(hole, {
634               x: ball.nextX,
635               y: ball.nextY
636             });
637 
638             if (distance <= holeSize / 2) {
639               // The ball fell into a hole
640               holeElements[hi].style.backgroundColor = "red";
641               throw Error("The ball fell into a hole");
642             }
643           });
644         }
645 
646         // Adjust ball metadata
647         ball.x = ball.x + ball.velocityX;
648         ball.y = ball.y + ball.velocityY;
649       });
650 
651       // Move balls to their new position on the UI
652       balls.forEach(({ x, y }, index) => {
653         ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
654       });
655     }
656 
657     // Win detection
658     if (
659       balls.every(
660         (ball) => distance2D(ball, { x: 350 / 2, y: 315 / 2 }) < 65 / 2
661       )
662     ) {
663       noteElement.innerHTML = `Congrats, you did it!
664         ${!hardMode && "<p>Press H for hard mode</p>"}
665         <p>
666           Follow me
667           <a href="https://www.17sucai.com" , target="_blank"
668             >@HunorBorbely</a
669           >
670         </p>`;
671       noteElement.style.opacity = 1;
672       gameInProgress = false;
673     } else {
674       previousTimestamp = timestamp;
675       window.requestAnimationFrame(main);
676     }
677   } catch (error) {
678     if (error.message == "The ball fell into a hole") {
679       noteElement.innerHTML = `A ball fell into a black hole! Press space to reset the game.
680         <p>
681           Back to easy? Press E
682         </p>`;
683       noteElement.style.opacity = 1;
684       gameInProgress = false;
685     } else throw error;
686   }
687 }

 

上一篇:QT改变滑动条的显示样式


下一篇:[ATcoder Beginner Contest #143]