HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<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">
Click the joystick to start!
<p>Move every ball to the center. Ready for hard mode? Press H</p>
</div>
</div>
</div>
<a id="youtube" href="https://youtu.be/bTk6dcAckuI" target="_top">
<span>See how this game was made</span>
</a>
<div id="youtube-card">
How to simulate ball movement in a maze with JavaScript
</div>
body {
/* https://coolors.co/f06449-ede6e3-7d82b8-36382e-613f75 */
--background-color: #ede6e3;
--wall-color: #36382e;
--joystick-color: #210124;
--joystick-head-color: #f06449;
--ball-color: #f06449;
--end-color: #7d82b8;
--text-color: #210124;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
}
html,
body {
height: 100%;
margin: 0;
}
#center {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
#game {
display: grid;
grid-template-columns: auto 150px;
grid-template-rows: 1fr auto 1fr;
gap: 30px;
perspective: 600px;
}
#maze {
position: relative;
grid-row: 1 / -1;
grid-column: 1;
width: 350px;
height: 315px;
display: flex;
justify-content: center;
align-items: center;
}
#end {
width: 65px;
height: 65px;
border: 5px dashed var(--end-color);
border-radius: 50%;
}
#joystick {
position: relative;
background-color: var(--joystick-color);
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
margin: 10px 50px;
grid-row: 2;
}
#joystick-head {
position: relative;
background-color: var(--joystick-head-color);
border-radius: 50%;
width: 20px;
height: 20px;
cursor: grab;
animation-name: glow;
animation-duration: 0.6s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
animation-delay: 4s;
}
@keyframes glow {
0% {
transform: scale(1);
}
100% {
transform: scale(1.2);
}
}
.joystick-arrow:nth-of-type(1) {
position: absolute;
bottom: 55px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid var(--joystick-color);
}
.joystick-arrow:nth-of-type(2) {
position: absolute;
top: 55px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid var(--joystick-color);
}
.joystick-arrow:nth-of-type(3) {
position: absolute;
left: 55px;
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid var(--joystick-color);
}
.joystick-arrow:nth-of-type(4) {
position: absolute;
right: 55px;
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid var(--joystick-color);
}
#note {
grid-row: 3;
grid-column: 2;
text-align: center;
font-size: 0.8em;
color: var(--text-color);
transition: opacity 2s;
}
a:visited {
color: inherit;
}
.ball {
position: absolute;
margin-top: -5px;
margin-left: -5px;
border-radius: 50%;
background-color: var(--ball-color);
width: 10px;
height: 10px;
}
.wall {
position: absolute;
background-color: var(--wall-color);
transform-origin: top center;
margin-left: -5px;
}
.wall::before,
.wall::after {
display: block;
content: "";
width: 10px;
height: 10px;
background-color: inherit;
border-radius: 50%;
position: absolute;
}
.wall::before {
top: -5px;
}
.wall::after {
bottom: -5px;
}
.black-hole {
position: absolute;
margin-top: -9px;
margin-left: -9px;
border-radius: 50%;
background-color: black;
width: 18px;
height: 18px;
}
#youtube,
#youtube-card {
display: none;
}
@media (min-height: 425px) {
/** Youtube logo by https://codepen.io/alvaromontoro */
#youtube {
z-index: 2;
display: block;
width: 100px;
height: 70px;
position: absolute;
bottom: 20px;
right: 20px;
background: red;
border-radius: 50% / 11%;
transform: scale(0.8);
transition: transform 0.5s;
}
#youtube:hover,
#youtube:focus {
transform: scale(0.9);
}
#youtube::before {
content: "";
display: block;
position: absolute;
top: 7.5%;
left: -6%;
width: 112%;
height: 85%;
background: red;
border-radius: 9% / 50%;
}
#youtube::after {
content: "";
display: block;
position: absolute;
top: 20px;
left: 40px;
width: 45px;
height: 30px;
border: 15px solid transparent;
box-sizing: border-box;
border-left: 30px solid white;
}
#youtube span {
font-size: 0;
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
#youtube:hover + #youtube-card {
display: block;
position: absolute;
bottom: 12px;
right: 10px;
padding: 25px 130px 25px 25px;
width: 300px;
background-color: white;
}
}
/*
If you want to know how this game works, you can find a source code walkthrough video here: https://youtu.be/bTk6dcAckuI
Follow me on twitter for more: https://twitter.com/HunorBorbely
*/
Math.minmax = (value, limit) => {
return Math.max(Math.min(value, limit), -limit);
};
const distance2D = (p1, p2) => {
return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
};
// Angle between the two points
const getAngle = (p1, p2) => {
let angle = Math.atan((p2.y - p1.y) / (p2.x - p1.x));
if (p2.x - p1.x < 0) angle += Math.PI;
return angle;
};
// The closest a ball and a wall cap can be
const closestItCanBe = (cap, ball) => {
let angle = getAngle(cap, ball);
const deltaX = Math.cos(angle) * (wallW / 2 + ballSize / 2);
const deltaY = Math.sin(angle) * (wallW / 2 + ballSize / 2);
return { x: cap.x + deltaX, y: cap.y + deltaY };
};
// Roll the ball around the wall cap
const rollAroundCap = (cap, ball) => {
// The direction the ball can't move any further because the wall holds it back
let impactAngle = getAngle(ball, cap);
// The direction the ball wants to move based on it's velocity
let heading = getAngle(
{ x: 0, y: 0 },
{ x: ball.velocityX, y: ball.velocityY }
);
// The angle between the impact direction and the ball's desired direction
// The smaller this angle is, the bigger the impact
// The closer it is to 90 degrees the smoother it gets (at 90 there would be no collision)
let impactHeadingAngle = impactAngle - heading;
// Velocity distance if not hit would have occurred
const velocityMagnitude = distance2D(
{ x: 0, y: 0 },
{ x: ball.velocityX, y: ball.velocityY }
);
// Velocity component diagonal to the impact
const velocityMagnitudeDiagonalToTheImpact =
Math.sin(impactHeadingAngle) * velocityMagnitude;
// How far should the ball be from the wall cap
const closestDistance = wallW / 2 + ballSize / 2;
const rotationAngle = Math.atan(
velocityMagnitudeDiagonalToTheImpact / closestDistance
);
const deltaFromCap = {
x: Math.cos(impactAngle + Math.PI - rotationAngle) * closestDistance,
y: Math.sin(impactAngle + Math.PI - rotationAngle) * closestDistance
};
const x = ball.x;
const y = ball.y;
const velocityX = ball.x - (cap.x + deltaFromCap.x);
const velocityY = ball.y - (cap.y + deltaFromCap.y);
const nextX = x + velocityX;
const nextY = y + velocityY;
return { x, y, velocityX, velocityY, nextX, nextY };
};
// Decreases the absolute value of a number but keeps it's sign, doesn't go below abs 0
const slow = (number, difference) => {
if (Math.abs(number) <= difference) return 0;
if (number > difference) return number - difference;
return number + difference;
};
const mazeElement = document.getElementById("maze");
const joystickHeadElement = document.getElementById("joystick-head");
const noteElement = document.getElementById("note"); // Note element for instructions and game won, game failed texts
let hardMode = false;
let previousTimestamp;
let gameInProgress;
let mouseStartX;
let mouseStartY;
let accelerationX;
let accelerationY;
let frictionX;
let frictionY;
const pathW = 25; // Path width
const wallW = 10; // Wall width
const ballSize = 10; // Width and height of the ball
const holeSize = 18;
const debugMode = false;
let balls = [];
let ballElements = [];
let holeElements = [];
resetGame();
// Draw balls for the first time
balls.forEach(({ x, y }) => {
const ball = document.createElement("div");
ball.setAttribute("class", "ball");
ball.style.cssText = `left: ${x}px; top: ${y}px; `;
mazeElement.appendChild(ball);
ballElements.push(ball);
});
// Wall metadata
const walls = [
// Border
{ column: 0, row: 0, horizontal: true, length: 10 },
{ column: 0, row: 0, horizontal: false, length: 9 },
{ column: 0, row: 9, horizontal: true, length: 10 },
{ column: 10, row: 0, horizontal: false, length: 9 },
// Horizontal lines starting in 1st column
{ column: 0, row: 6, horizontal: true, length: 1 },
{ column: 0, row: 8, horizontal: true, length: 1 },
// Horizontal lines starting in 2nd column
{ column: 1, row: 1, horizontal: true, length: 2 },
{ column: 1, row: 7, horizontal: true, length: 1 },
// Horizontal lines starting in 3rd column
{ column: 2, row: 2, horizontal: true, length: 2 },
{ column: 2, row: 4, horizontal: true, length: 1 },
{ column: 2, row: 5, horizontal: true, length: 1 },
{ column: 2, row: 6, horizontal: true, length: 1 },
// Horizontal lines starting in 4th column
{ column: 3, row: 3, horizontal: true, length: 1 },
{ column: 3, row: 8, horizontal: true, length: 3 },
// Horizontal lines starting in 5th column
{ column: 4, row: 6, horizontal: true, length: 1 },
// Horizontal lines starting in 6th column
{ column: 5, row: 2, horizontal: true, length: 2 },
{ column: 5, row: 7, horizontal: true, length: 1 },
// Horizontal lines starting in 7th column
{ column: 6, row: 1, horizontal: true, length: 1 },
{ column: 6, row: 6, horizontal: true, length: 2 },
// Horizontal lines starting in 8th column
{ column: 7, row: 3, horizontal: true, length: 2 },
{ column: 7, row: 7, horizontal: true, length: 2 },
// Horizontal lines starting in 9th column
{ column: 8, row: 1, horizontal: true, length: 1 },
{ column: 8, row: 2, horizontal: true, length: 1 },
{ column: 8, row: 3, horizontal: true, length: 1 },
{ column: 8, row: 4, horizontal: true, length: 2 },
{ column: 8, row: 8, horizontal: true, length: 2 },
// Vertical lines after the 1st column
{ column: 1, row: 1, horizontal: false, length: 2 },
{ column: 1, row: 4, horizontal: false, length: 2 },
// Vertical lines after the 2nd column
{ column: 2, row: 2, horizontal: false, length: 2 },
{ column: 2, row: 5, horizontal: false, length: 1 },
{ column: 2, row: 7, horizontal: false, length: 2 },
// Vertical lines after the 3rd column
{ column: 3, row: 0, horizontal: false, length: 1 },
{ column: 3, row: 4, horizontal: false, length: 1 },
{ column: 3, row: 6, horizontal: false, length: 2 },
// Vertical lines after the 4th column
{ column: 4, row: 1, horizontal: false, length: 2 },
{ column: 4, row: 6, horizontal: false, length: 1 },
// Vertical lines after the 5th column
{ column: 5, row: 0, horizontal: false, length: 2 },
{ column: 5, row: 6, horizontal: false, length: 1 },
{ column: 5, row: 8, horizontal: false, length: 1 },
// Vertical lines after the 6th column
{ column: 6, row: 4, horizontal: false, length: 1 },
{ column: 6, row: 6, horizontal: false, length: 1 },
// Vertical lines after the 7th column
{ column: 7, row: 1, horizontal: false, length: 4 },
{ column: 7, row: 7, horizontal: false, length: 2 },
// Vertical lines after the 8th column
{ column: 8, row: 2, horizontal: false, length: 1 },
{ column: 8, row: 4, horizontal: false, length: 2 },
// Vertical lines after the 9th column
{ column: 9, row: 1, horizontal: false, length: 1 },
{ column: 9, row: 5, horizontal: false, length: 2 }
].map((wall) => ({
x: wall.column * (pathW + wallW),
y: wall.row * (pathW + wallW),
horizontal: wall.horizontal,
length: wall.length * (pathW + wallW)
}));
// Draw walls
walls.forEach(({ x, y, horizontal, length }) => {
const wall = document.createElement("div");
wall.setAttribute("class", "wall");
wall.style.cssText = `
left: ${x}px;
top: ${y}px;
width: ${wallW}px;
height: ${length}px;
transform: rotate(${horizontal ? -90 : 0}deg);
`;
mazeElement.appendChild(wall);
});
const holes = [
{ column: 0, row: 5 },
{ column: 2, row: 0 },
{ column: 2, row: 4 },
{ column: 4, row: 6 },
{ column: 6, row: 2 },
{ column: 6, row: 8 },
{ column: 8, row: 1 },
{ column: 8, row: 2 }
].map((hole) => ({
x: hole.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
y: hole.row * (wallW + pathW) + (wallW / 2 + pathW / 2)
}));
joystickHeadElement.addEventListener("mousedown", function (event) {
if (!gameInProgress) {
mouseStartX = event.clientX;
mouseStartY = event.clientY;
gameInProgress = true;
window.requestAnimationFrame(main);
noteElement.style.opacity = 0;
joystickHeadElement.style.cssText = `
animation: none;
cursor: grabbing;
`;
}
});
window.addEventListener("mousemove", function (event) {
if (gameInProgress) {
const mouseDeltaX = -Math.minmax(mouseStartX - event.clientX, 15);
const mouseDeltaY = -Math.minmax(mouseStartY - event.clientY, 15);
joystickHeadElement.style.cssText = `
left: ${mouseDeltaX}px;
top: ${mouseDeltaY}px;
animation: none;
cursor: grabbing;
`;
const rotationY = mouseDeltaX * 0.8; // Max rotation = 12
const rotationX = mouseDeltaY * 0.8;
mazeElement.style.cssText = `
transform: rotateY(${rotationY}deg) rotateX(${-rotationX}deg)
`;
const gravity = 2;
const friction = 0.01; // Coefficients of friction
accelerationX = gravity * Math.sin((rotationY / 180) * Math.PI);
accelerationY = gravity * Math.sin((rotationX / 180) * Math.PI);
frictionX = gravity * Math.cos((rotationY / 180) * Math.PI) * friction;
frictionY = gravity * Math.cos((rotationX / 180) * Math.PI) * friction;
}
});
window.addEventListener("keydown", function (event) {
// If not an arrow key or space or H was pressed then return
if (![" ", "H", "h", "E", "e"].includes(event.key)) return;
// If an arrow key was pressed then first prevent default
event.preventDefault();
// If space was pressed restart the game
if (event.key == " ") {
resetGame();
return;
}
// Set Hard mode
if (event.key == "H" || event.key == "h") {
hardMode = true;
resetGame();
return;
}
// Set Easy mode
if (event.key == "E" || event.key == "e") {
hardMode = false;
resetGame();
return;
}
});
function resetGame() {
previousTimestamp = undefined;
gameInProgress = false;
mouseStartX = undefined;
mouseStartY = undefined;
accelerationX = undefined;
accelerationY = undefined;
frictionX = undefined;
frictionY = undefined;
mazeElement.style.cssText = `
transform: rotateY(0deg) rotateX(0deg)
`;
joystickHeadElement.style.cssText = `
left: 0;
top: 0;
animation: glow;
cursor: grab;
`;
if (hardMode) {
noteElement.innerHTML = `Click the joystick to start!
<p>Hard mode, Avoid black holes. Back to easy mode? Press E</p>`;
} else {
noteElement.innerHTML = `Click the joystick to start!
<p>Move every ball to the center. Ready for hard mode? Press H</p>`;
}
noteElement.style.opacity = 1;
balls = [
{ column: 0, row: 0 },
{ column: 9, row: 0 },
{ column: 0, row: 8 },
{ column: 9, row: 8 }
].map((ball) => ({
x: ball.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
y: ball.row * (wallW + pathW) + (wallW / 2 + pathW / 2),
velocityX: 0,
velocityY: 0
}));
if (ballElements.length) {
balls.forEach(({ x, y }, index) => {
ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
});
}
// Remove previous hole elements
holeElements.forEach((holeElement) => {
mazeElement.removeChild(holeElement);
});
holeElements = [];
// Reset hole elements if hard mode
if (hardMode) {
holes.forEach(({ x, y }) => {
const ball = document.createElement("div");
ball.setAttribute("class", "black-hole");
ball.style.cssText = `left: ${x}px; top: ${y}px; `;
mazeElement.appendChild(ball);
holeElements.push(ball);
});
}
}
function main(timestamp) {
// It is possible to reset the game mid-game. This case the look should stop
if (!gameInProgress) return;
if (previousTimestamp === undefined) {
previousTimestamp = timestamp;
window.requestAnimationFrame(main);
return;
}
const maxVelocity = 1.5;
// Time passed since last cycle divided by 16
// This function gets called every 16 ms on average so dividing by 16 will result in 1
const timeElapsed = (timestamp - previousTimestamp) / 16;
try {
// If mouse didn't move yet don't do anything
if (accelerationX != undefined && accelerationY != undefined) {
const velocityChangeX = accelerationX * timeElapsed;
const velocityChangeY = accelerationY * timeElapsed;
const frictionDeltaX = frictionX * timeElapsed;
const frictionDeltaY = frictionY * timeElapsed;
balls.forEach((ball) => {
if (velocityChangeX == 0) {
// No rotation, the plane is flat
// On flat surface friction can only slow down, but not reverse movement
ball.velocityX = slow(ball.velocityX, frictionDeltaX);
} else {
ball.velocityX = ball.velocityX + velocityChangeX;
ball.velocityX = Math.max(Math.min(ball.velocityX, 1.5), -1.5);
ball.velocityX =
ball.velocityX - Math.sign(velocityChangeX) * frictionDeltaX;
ball.velocityX = Math.minmax(ball.velocityX, maxVelocity);
}
if (velocityChangeY == 0) {
// No rotation, the plane is flat
// On flat surface friction can only slow down, but not reverse movement
ball.velocityY = slow(ball.velocityY, frictionDeltaY);
} else {
ball.velocityY = ball.velocityY + velocityChangeY;
ball.velocityY =
ball.velocityY - Math.sign(velocityChangeY) * frictionDeltaY;
ball.velocityY = Math.minmax(ball.velocityY, maxVelocity);
}
// Preliminary next ball position, only becomes true if no hit occurs
// Used only for hit testing, does not mean that the ball will reach this position
ball.nextX = ball.x + ball.velocityX;
ball.nextY = ball.y + ball.velocityY;
if (debugMode) console.log("tick", ball);
walls.forEach((wall, wi) => {
if (wall.horizontal) {
// Horizontal wall
if (
ball.nextY + ballSize / 2 >= wall.y - wallW / 2 &&
ball.nextY - ballSize / 2 <= wall.y + wallW / 2
) {
// Ball got within the strip of the wall
// (not necessarily hit it, could be before or after)
const wallStart = {
x: wall.x,
y: wall.y
};
const wallEnd = {
x: wall.x + wall.length,
y: wall.y
};
if (
ball.nextX + ballSize / 2 >= wallStart.x - wallW / 2 &&
ball.nextX < wallStart.x
) {
// Ball might hit the left cap of a horizontal wall
const distance = distance2D(wallStart, {
x: ball.nextX,
y: ball.nextY
});
if (distance < ballSize / 2 + wallW / 2) {
if (debugMode && wi > 4)
console.warn("too close h head", distance, ball);
// Ball hits the left cap of a horizontal wall
const closest = closestItCanBe(wallStart, {
x: ball.nextX,
y: ball.nextY
});
const rolled = rollAroundCap(wallStart, {
x: closest.x,
y: closest.y,
velocityX: ball.velocityX,
velocityY: ball.velocityY
});
Object.assign(ball, rolled);
}
}
if (
ball.nextX - ballSize / 2 <= wallEnd.x + wallW / 2 &&
ball.nextX > wallEnd.x
) {
// Ball might hit the right cap of a horizontal wall
const distance = distance2D(wallEnd, {
x: ball.nextX,
y: ball.nextY
});
if (distance < ballSize / 2 + wallW / 2) {
if (debugMode && wi > 4)
console.warn("too close h tail", distance, ball);
// Ball hits the right cap of a horizontal wall
const closest = closestItCanBe(wallEnd, {
x: ball.nextX,
y: ball.nextY
});
const rolled = rollAroundCap(wallEnd, {
x: closest.x,
y: closest.y,
velocityX: ball.velocityX,
velocityY: ball.velocityY
});
Object.assign(ball, rolled);
}
}
if (ball.nextX >= wallStart.x && ball.nextX <= wallEnd.x) {
// The ball got inside the main body of the wall
if (ball.nextY < wall.y) {
// Hit horizontal wall from top
ball.nextY = wall.y - wallW / 2 - ballSize / 2;
} else {
// Hit horizontal wall from bottom
ball.nextY = wall.y + wallW / 2 + ballSize / 2;
}
ball.y = ball.nextY;
ball.velocityY = -ball.velocityY / 3;
if (debugMode && wi > 4)
console.error("crossing h line, HIT", ball);
}
}
} else {
// Vertical wall
if (
ball.nextX + ballSize / 2 >= wall.x - wallW / 2 &&
ball.nextX - ballSize / 2 <= wall.x + wallW / 2
) {
// Ball got within the strip of the wall
// (not necessarily hit it, could be before or after)
const wallStart = {
x: wall.x,
y: wall.y
};
const wallEnd = {
x: wall.x,
y: wall.y + wall.length
};
if (
ball.nextY + ballSize / 2 >= wallStart.y - wallW / 2 &&
ball.nextY < wallStart.y
) {
// Ball might hit the top cap of a horizontal wall
const distance = distance2D(wallStart, {
x: ball.nextX,
y: ball.nextY
});
if (distance < ballSize / 2 + wallW / 2) {
if (debugMode && wi > 4)
console.warn("too close v head", distance, ball);
// Ball hits the left cap of a horizontal wall
const closest = closestItCanBe(wallStart, {
x: ball.nextX,
y: ball.nextY
});
const rolled = rollAroundCap(wallStart, {
x: closest.x,
y: closest.y,
velocityX: ball.velocityX,
velocityY: ball.velocityY
});
Object.assign(ball, rolled);
}
}
if (
ball.nextY - ballSize / 2 <= wallEnd.y + wallW / 2 &&
ball.nextY > wallEnd.y
) {
// Ball might hit the bottom cap of a horizontal wall
const distance = distance2D(wallEnd, {
x: ball.nextX,
y: ball.nextY
});
if (distance < ballSize / 2 + wallW / 2) {
if (debugMode && wi > 4)
console.warn("too close v tail", distance, ball);
// Ball hits the right cap of a horizontal wall
const closest = closestItCanBe(wallEnd, {
x: ball.nextX,
y: ball.nextY
});
const rolled = rollAroundCap(wallEnd, {
x: closest.x,
y: closest.y,
velocityX: ball.velocityX,
velocityY: ball.velocityY
});
Object.assign(ball, rolled);
}
}
if (ball.nextY >= wallStart.y && ball.nextY <= wallEnd.y) {
// The ball got inside the main body of the wall
if (ball.nextX < wall.x) {
// Hit vertical wall from left
ball.nextX = wall.x - wallW / 2 - ballSize / 2;
} else {
// Hit vertical wall from right
ball.nextX = wall.x + wallW / 2 + ballSize / 2;
}
ball.x = ball.nextX;
ball.velocityX = -ball.velocityX / 3;
if (debugMode && wi > 4)
console.error("crossing v line, HIT", ball);
}
}
}
});
// Detect is a ball fell into a hole
if (hardMode) {
holes.forEach((hole, hi) => {
const distance = distance2D(hole, {
x: ball.nextX,
y: ball.nextY
});
if (distance <= holeSize / 2) {
// The ball fell into a hole
holeElements[hi].style.backgroundColor = "red";
throw Error("The ball fell into a hole");
}
});
}
// Adjust ball metadata
ball.x = ball.x + ball.velocityX;
ball.y = ball.y + ball.velocityY;
});
// Move balls to their new position on the UI
balls.forEach(({ x, y }, index) => {
ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
});
}
// Win detection
if (
balls.every(
(ball) => distance2D(ball, { x: 350 / 2, y: 315 / 2 }) < 65 / 2
)
) {
noteElement.innerHTML = `Congrats, you did it!
${!hardMode ? "<p>Press H for hard mode</p>" : ""}
<p>
Follow me
<a href="https://twitter.com/HunorBorbely" , target="_top"
>@HunorBorbely</a
>
</p>`;
noteElement.style.opacity = 1;
gameInProgress = false;
} else {
previousTimestamp = timestamp;
window.requestAnimationFrame(main);
}
} catch (error) {
if (error.message == "The ball fell into a hole") {
noteElement.innerHTML = `A ball fell into a black hole! Press space to reset the game.
<p>
Back to easy? Press E
</p>`;
noteElement.style.opacity = 1;
gameInProgress = false;
} else throw error;
}
}
Also see: Tab Triggers