Pong Wars | 黑白对决
先看效果:
bongwar
项目概述
Pong Wars 是一个基于 HTML5 Canvas 的创意视觉项目,展现了黑与白、昼与夜的永恒对决。两个小球在画布上不断碰撞和反弹,同时将它们经过的区域转变为自己的颜色,形成一场视觉上的领地争夺战。
技术实现
这个项目使用了以下技术:
- HTML5 Canvas: 用于绘制游戏界面和动态效果
- JavaScript: 控制游戏逻辑、碰撞检测和视觉变化
- CSS: 设计响应式布局,确保在不同设备上都有良好的体验
整个项目都在一个 HTML 文件中实现,展示了如何用最简洁的代码创造出引人入胜的视觉体验。
项目特色
1. 黑白对决
项目采用极简的黑白配色方案,营造出简约而强烈的视觉对比。两个小球分别代表黑暗与光明,在画布上不断移动并争夺领地。
2. 动态天体元素(新增)
- 太阳: 位于画布左上方,随着区域被占领情况动态改变亮度和外观
- 月亮: 位于画布右上方,同样会根据区域占领状态变化外观
当太阳区域被黑色占领,太阳会变暗;当月亮区域被白色占领,月亮会变亮。这种动态变化增强了视觉体验,使画面更加生动。
3. 实时得分统计
画面下方实时显示黑白两方各自占领的方格数量,直观展示当前战局。
算法简述
项目中使用了几个关键算法:
- 碰撞检测: 检测小球与边界和方格的碰撞,并计算反弹方向
- 区域占领: 小球经过时将方格颜色更改为自己的颜色
- 区域分析: 通过分析特定区域内方格的颜色分布,决定太阳和月亮的显示效果
使用方法
方法一:
- 复制我下方的index.html文件代码,
- 在桌面新建一个html文件,
- 用浏览器打开即可
方法二:
直接点击此处链接下载并用浏览器打开
可能的扩展方向
这个项目有很多有趣的扩展可能性:
- 添加用户交互: 允许用户通过点击或拖动改变小球的方向或速度
- 增加更多元素: 引入更多的小球或特殊效果
- 自定义主题: 允许用户选择不同的配色方案
- 添加音效: 为碰撞和颜色变化添加音效,增强感官体验
- 实现多人对战: 让多个玩家控制不同的小球进行对战
致谢
本项目原始灵感来自 Koen van Gilst的 Pong Wars 项目,本人在此基础上增加了太阳和月亮元素以及更多视觉效果。
以下是完整代码:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pong wars | Black & White Edition</title>
<meta
name="description"
content="The eternal battle between day and night, good and bad. Written in JavaScript with some HTML & CSS in one index.html. Feel free to reuse the code and create your own version."
/>
<link rel="canonical" href="https://pong-wars.koenvangilst.nl/" />
<link rel="author" href="https://koenvangilst.nl" />
<meta name="theme-color" content="#000000" />
<meta name="creator" content="Koen van Gilst" />
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to bottom, #000000 0%, #ffffff 100%);
}
#container {
display: flex;
align-items: center;
flex-direction: column;
width: min(70vh, 80%);
max-width: 600px;
height: 100%;
}
#pongCanvas {
display: block;
border-radius: 4px;
overflow: hidden;
width: 100%;
margin-top: auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
}
#score {
font-family: monospace;
margin-top: 30px;
font-size: 16px;
padding-left: 20px;
color: #000000;
font-weight: bold;
}
#made {
text-align: center;
line-height: 1.5;
font-family: monospace;
margin-top: auto;
margin-bottom: 20px;
font-size: 10px;
}
#made a {
color: #000000;
font-weight: bold;
}
</style>
</head>
<body>
<div id="container">
<canvas id="pongCanvas" width="800" height="800"></canvas>
<div id="score"></div>
<p id="made">
modified by <a href="https://www.cnblogs.com/Lyu-COOL">Mr.Lyu9277</a>
| source on<a href="https://github.com/vnglst/pong-wars">github</a>
</p>
</div>
</body>
<script>
// Black and white color palette
const colorPalette = {
White: "#FFFFFF",
Black: "#000000",
LightGray: "#EEEEEE",
DarkGray: "#222222",
};
const canvas = document.getElementById("pongCanvas");
const ctx = canvas.getContext("2d");
const scoreElement = document.getElementById("score");
const DAY_COLOR = colorPalette.White;
const DAY_BALL_COLOR = colorPalette.Black;
const NIGHT_COLOR = colorPalette.Black;
const NIGHT_BALL_COLOR = colorPalette.White;
const SQUARE_SIZE = 25;
const numSquaresX = canvas.width / SQUARE_SIZE;
const numSquaresY = canvas.height / SQUARE_SIZE;
let squares = [];
for (let i = 0; i < numSquaresX; i++) {
squares[i] = [];
for (let j = 0; j < numSquaresY; j++) {
squares[i][j] = i < numSquaresX / 2 ? DAY_COLOR : NIGHT_COLOR;
}
}
let x1 = canvas.width / 4;
let y1 = canvas.height / 2;
let dx1 = 12.5;
let dy1 = -12.5;
let x2 = (canvas.width / 4) * 3;
let y2 = canvas.height / 2;
let dx2 = -12.5;
let dy2 = 12.5;
let iteration = 0;
function getDominantColor(centerX, centerY, radius) {
let dayCount = 0;
let nightCount = 0;
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
const squareCenterX = i * SQUARE_SIZE + SQUARE_SIZE / 2;
const squareCenterY = j * SQUARE_SIZE + SQUARE_SIZE / 2;
const distance = Math.sqrt(
Math.pow(squareCenterX - centerX, 2) +
Math.pow(squareCenterY - centerY, 2)
);
if (distance <= radius * 2) {
if (squares[i][j] === DAY_COLOR) {
dayCount++;
} else {
nightCount++;
}
}
}
}
return dayCount > nightCount ? DAY_COLOR : NIGHT_COLOR;
}
function drawSun() {
const sunX = canvas.width / 4;
const sunY = canvas.height / 4;
const sunRadius = canvas.width / 10;
const dominantColor = getDominantColor(sunX, sunY, sunRadius);
const sunRayColor = dominantColor === DAY_COLOR ? colorPalette.DarkGray : colorPalette.Black;
const sunFillColor = dominantColor === DAY_COLOR ? colorPalette.LightGray : colorPalette.DarkGray;
ctx.beginPath();
for (let i = 0; i < 12; i++) {
const angle = i * Math.PI / 6;
const innerRadius = sunRadius * 1.2;
const outerRadius = sunRadius * 1.8;
ctx.moveTo(
sunX + innerRadius * Math.cos(angle),
sunY + innerRadius * Math.sin(angle)
);
ctx.lineTo(
sunX + outerRadius * Math.cos(angle),
sunY + outerRadius * Math.sin(angle)
);
}
ctx.strokeStyle = sunRayColor;
ctx.lineWidth = 4;
ctx.stroke();
ctx.beginPath();
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2, false);
ctx.fillStyle = sunFillColor;
ctx.fill();
ctx.strokeStyle = sunRayColor;
ctx.stroke();
}
function drawMoon() {
const moonX = canvas.width * 3 / 4;
const moonY = canvas.height / 4;
const moonRadius = canvas.width / 10;
const dominantColor = getDominantColor(moonX, moonY, moonRadius);
const moonFillColor =
dominantColor === NIGHT_COLOR ? colorPalette.DarkGray : colorPalette.LightGray;
const moonCraterColor =
dominantColor === NIGHT_COLOR ? colorPalette.Black : colorPalette.White;
const starColor =
dominantColor === NIGHT_COLOR ? colorPalette.White : colorPalette.DarkGray;
ctx.beginPath();
ctx.arc(moonX, moonY, moonRadius, 0, Math.PI * 2, false);
ctx.fillStyle = moonFillColor;
ctx.fill();
ctx.beginPath();
ctx.arc(moonX - moonRadius * 0.3, moonY, moonRadius * 0.9, 0, Math.PI * 2, false);
ctx.fillStyle = moonCraterColor;
ctx.fill();
const stars = [
{ x: moonX + moonRadius * 2, y: moonY - moonRadius, size: 4 },
{ x: moonX + moonRadius, y: moonY + moonRadius * 1.5, size: 3 },
{ x: moonX + moonRadius * 2.2, y: moonY + moonRadius * 0.7, size: 5 },
{ x: moonX - moonRadius * 1.5, y: moonY - moonRadius * 1.3, size: 4 },
{ x: moonX - moonRadius * 2, y: moonY + moonRadius, size: 3 }
];
stars.forEach((star) => {
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * Math.PI * 2) / 5 - Math.PI / 2;
const radius = i % 2 === 0 ? star.size : star.size / 2;
if (i === 0) {
ctx.moveTo(star.x + radius * Math.cos(angle), star.y + radius * Math.sin(angle));
} else {
ctx.lineTo(star.x + radius * Math.cos(angle), star.y + radius * Math.sin(angle));
}
}
ctx.closePath();
ctx.fillStyle = starColor;
ctx.fill();
});
}
function drawBall(x, y, color) {
ctx.beginPath();
ctx.arc(x, y, SQUARE_SIZE / 2, 0, Math.PI * 2, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
function drawSquares() {
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
ctx.fillStyle = squares[i][j];
ctx.fillRect(
i * SQUARE_SIZE,
j * SQUARE_SIZE,
SQUARE_SIZE,
SQUARE_SIZE
);
}
}
}
function updateSquareAndBounce(x, y, dx, dy, color) {
let updatedDx = dx;
let updatedDy = dy;
for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
let checkX = x + Math.cos(angle) * (SQUARE_SIZE / 2);
let checkY = y + Math.sin(angle) * (SQUARE_SIZE / 2);
let i = Math.floor(checkX / SQUARE_SIZE);
let j = Math.floor(checkY / SQUARE_SIZE);
if (i >= 0 && i < numSquaresX && j >= 0 && j < numSquaresY) {
if (squares[i][j] !== color) {
squares[i][j] = color;
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
updatedDx = -updatedDx;
} else {
updatedDy = -updatedDy;
}
}
}
}
return { dx: updatedDx, dy: updatedDy };
}
function updateScoreElement() {
let dayScore = 0;
let nightScore = 0;
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
if (squares[i][j] === DAY_COLOR) {
dayScore++;
} else if (squares[i][j] === NIGHT_COLOR) {
nightScore++;
}
}
}
scoreElement.textContent = `白 ${dayScore} | 黑 ${nightScore}`;
}
function checkBoundaryCollision(x, y, dx, dy) {
if (x + dx > canvas.width - SQUARE_SIZE / 2 || x + dx < SQUARE_SIZE / 2) {
dx = -dx;
}
if (
y + dy > canvas.height - SQUARE_SIZE / 2 ||
y + dy < SQUARE_SIZE / 2
) {
dy = -dy;
}
return { dx: dx, dy: dy };
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquares();
drawSun();
drawMoon();
drawBall(x1, y1, DAY_BALL_COLOR);
let bounce1 = updateSquareAndBounce(x1, y1, dx1, dy1, DAY_COLOR);
dx1 = bounce1.dx;
dy1 = bounce1.dy;
drawBall(x2, y2, NIGHT_BALL_COLOR);
let bounce2 = updateSquareAndBounce(x2, y2, dx2, dy2, NIGHT_COLOR);
dx2 = bounce2.dx;
dy2 = bounce2.dy;
let boundary1 = checkBoundaryCollision(x1, y1, dx1, dy1);
dx1 = boundary1.dx;
dy1 = boundary1.dy;
let boundary2 = checkBoundaryCollision(x2, y2, dx2, dy2);
dx2 = boundary2.dx;
dy2 = boundary2.dy;
x1 += dx1;
y1 += dy1;
x2 += dx2;
y2 += dy2;
iteration++;
if (iteration % 1_000 === 0) console.log("iteration", iteration);
updateScoreElement();
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
</script>
</html>