Canvas 高级应用与实战项目
下面我将介绍 Canvas 更高级的应用场景,包括游戏开发、数据可视化、图像处理等实战项目,并提供完整的代码示例。
1. 简单游戏开发 - 打砖块
javascript
// 游戏状态
const game = {
ball: { x: 250, y: 150, radius: 10, dx: 4, dy: -4 },
paddle: { x: 200, width: 100, height: 15 },
bricks: [],
brickRowCount: 5,
brickColumnCount: 8,
brickWidth: 60,
brickHeight: 20,
brickPadding: 10,
brickOffsetTop: 60,
brickOffsetLeft: 30,
score: 0,
lives: 3
};
// 初始化砖块
function initBricks() {
for (let c = 0; c < game.brickColumnCount; c++) {
game.bricks[c] = [];
for (let r = 0; r < game.brickRowCount; r++) {
game.bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
}
// 绘制游戏元素
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制球
ctx.beginPath();
ctx.arc(game.ball.x, game.ball.y, game.ball.radius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
// 绘制挡板
ctx.beginPath();
ctx.rect(game.paddle.x, canvas.height-game.paddle.height,
game.paddle.width, game.paddle.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
// 绘制砖块
for (let c = 0; c < game.brickColumnCount; c++) {
for (let r = 0; r < game.brickRowCount; r++) {
if (game.bricks[c][r].status === 1) {
const brickX = c*(game.brickWidth+game.brickPadding)+game.brickOffsetLeft;
const brickY = r*(game.brickHeight+game.brickPadding)+game.brickOffsetTop;
game.bricks[c][r].x = brickX;
game.bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, game.brickWidth, game.brickHeight);
ctx.fillStyle = `hsl(${r*60}, 100%, 50%)`;
ctx.fill();
ctx.closePath();
}
}
}
// 绘制分数
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Score: "+game.score, 8, 20);
// 绘制生命值
ctx.fillText("Lives: "+game.lives, canvas.width-65, 20);
}
// 碰撞检测
function collisionDetection() {
for (let c = 0; c < game.brickColumnCount; c++) {
for (let r = 0; r < game.brickRowCount; r++) {
const b = game.bricks[c][r];
if (b.status === 1) {
if (game.ball.x > b.x && game.ball.x < b.x+game.brickWidth &&
game.ball.y > b.y && game.ball.y < b.y+game.brickHeight) {
game.ball.dy = -game.ball.dy;
b.status = 0;
game.score++;
if (game.score === game.brickRowCount*game.brickColumnCount) {
alert("YOU WIN, CONGRATULATIONS!");
document.location.reload();
}
}
}
}
}
}
// 游戏逻辑更新
function update() {
// 球移动
game.ball.x += game.ball.dx;
game.ball.y += game.ball.dy;
// 边界碰撞检测
if (game.ball.x + game.ball.dx > canvas.width-game.ball.radius ||
game.ball.x + game.ball.dx < game.ball.radius) {
game.ball.dx = -game.ball.dx;
}
if (game.ball.y + game.ball.dy < game.ball.radius) {
game.ball.dy = -game.ball.dy;
} else if (game.ball.y + game.ball.dy > canvas.height-game.ball.radius) {
if (game.ball.x > game.paddle.x && game.ball.x < game.paddle.x + game.paddle.width) {
game.ball.dy = -game.ball.dy;
} else {
game.lives--;
if (!game.lives) {
alert("GAME OVER");
document.location.reload();
} else {
game.ball.x = 250;
game.ball.y = 150;
game.ball.dx = 4;
game.ball.dy = -4;
game.paddle.x = 200;
}
}
}
// 挡板控制
if (rightPressed && game.paddle.x < canvas.width-game.paddle.width) {
game.paddle.x += 7;
} else if (leftPressed && game.paddle.x > 0) {
game.paddle.x -= 7;
}
collisionDetection();
}
// 键盘控制
let rightPressed = false;
let leftPressed = false;
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
// 鼠标控制
canvas.addEventListener("mousemove", mouseMoveHandler);
function mouseMoveHandler(e) {
const relativeX = e.clientX - canvas.offsetLeft;
if (relativeX > 0 && relativeX < canvas.width) {
game.paddle.x = relativeX - game.paddle.width/2;
}
}
// 游戏循环
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
initBricks();
gameLoop();
2. 数据可视化 - 动态折线图
javascript
// 模拟实时数据
const data = {
labels: Array(60).fill(0).map((_, i) => i),
values: Array(60).fill(0),
maxPoints: 60,
currentIndex: 0
};
// 更新数据
function updateData() {
// 模拟数据变化
const newValue = Math.random() * 100 + 50;
data.values[data.currentIndex] = newValue;
data.currentIndex = (data.currentIndex + 1) % data.maxPoints;
}
// 绘制折线图
function drawLineChart() {
const padding = 40;
const chartWidth = canvas.width - 2*padding;
const chartHeight = canvas.height - 2*padding;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制坐标轴
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, canvas.height - padding);
ctx.lineTo(canvas.width - padding, canvas.height - padding);
ctx.strokeStyle = '#666';
ctx.stroke();
// 绘制网格线
ctx.strokeStyle = '#eee';
for (let i = 0; i <= 10; i++) {
// 水平网格线
ctx.beginPath();
const y = padding + (chartHeight - i*chartHeight/10);
ctx.moveTo(padding, y);
ctx.lineTo(canvas.width - padding, y);
ctx.stroke();
// Y轴标签
ctx.fillStyle = '#666';
ctx.font = '10px Arial';
ctx.fillText(Math.round(i*150/10), 5, y + 5);
}
// 绘制折线
ctx.beginPath();
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 2;
const visibleData = [];
for (let i = 0; i < data.maxPoints; i++) {
const index = (data.currentIndex + i) % data.maxPoints;
visibleData.push(data.values[index]);
}
const maxValue = Math.max(...visibleData);
const minValue = Math.min(...visibleData);
const valueRange = maxValue - minValue;
visibleData.forEach((value, i) => {
const x = padding + (i * chartWidth / (data.maxPoints - 1));
const y = canvas.height - padding - ((value - minValue) / valueRange) * chartHeight;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI*2);
ctx.fillStyle = '#3498db';
ctx.fill();
});
ctx.stroke();
// 绘制标题
ctx.fillStyle = '#333';
ctx.font = '16px Arial';
ctx.fillText('实时数据折线图', canvas.width/2 - 60, 30);
// 绘制当前值
const currentValue = data.values[(data.currentIndex - 1 + data.maxPoints) % data.maxPoints];
ctx.fillStyle = '#e74c3c';
ctx.font = '14px Arial';
ctx.fillText(`当前值: ${currentValue.toFixed(2)}`, canvas.width - 150, 30);
}
// 动画循环
function animate() {
updateData();
drawLineChart();
requestAnimationFrame(animate);
}
animate();
3. 图像处理 - 图片编辑器
html
<div>
<input type="file" id="imageUpload" accept="image/*">
<button id="grayscale">灰度化</button>
<button id="brighten">亮度+</button>
<button id="darken">亮度-</button>
<button id="reset">重置</button>
</div>
<canvas id="imageCanvas" width="600" height="400"></canvas>
javascript
const imageCanvas = document.getElementById('imageCanvas');
const ctx = imageCanvas.getContext('2d');
const imageUpload = document.getElementById('imageUpload');
let originalImageData = null;
// 上传图片
imageUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
// 调整canvas大小以适应图片
const ratio = Math.min(
imageCanvas.width / img.width,
imageCanvas.height / img.height
);
const newWidth = img.width * ratio;
const newHeight = img.height * ratio;
// 绘制图片
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(img,
(imageCanvas.width - newWidth)/2,
(imageCanvas.height - newHeight)/2,
newWidth, newHeight);
// 保存原始图像数据
originalImageData = ctx.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
// 应用滤镜
function applyFilter(filterFn) {
if (!originalImageData) return;
const imageData = ctx.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
filterFn(data, i);
}
ctx.putImageData(imageData, 0, 0);
}
// 滤镜函数
const filters = {
grayscale: (data, i) => {
const avg = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
data[i] = data[i+1] = data[i+2] = avg;
},
brighten: (data, i) => {
data[i] = Math.min(255, data[i] * 1.2);
data[i+1] = Math.min(255, data[i+1] * 1.2);
data[i+2] = Math.min(255, data[i+2] * 1.2);
},
darken: (data, i) => {
data[i] = Math.max(0, data[i] * 0.8);
data[i+1] = Math.max(0, data[i+1] * 0.8);
data[i+2] = Math.max(0, data[i+2] * 0.8);
}
};
// 按钮事件
document.getElementById('grayscale').addEventListener('click', () => {
applyFilter(filters.grayscale);
});
document.getElementById('brighten').addEventListener('click', () => {
applyFilter(filters.brighten);
});
document.getElementById('darken').addEventListener('click', () => {
applyFilter(filters.darken);
});
document.getElementById('reset').addEventListener('click', () => {
if (originalImageData) {
ctx.putImageData(originalImageData, 0, 0);
}
});
4. 物理引擎 - 弹跳球模拟
javascript
// 物理参数
const physics = {
gravity: 0.2,
friction: 0.99,
bounce: 0.8
};
// 球体
const balls = [];
for (let i = 0; i < 20; i++) {
balls.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height/2,
radius: Math.random() * 20 + 10,
color: `hsl(${Math.random() * 360}, 100%, 50%)`,
dx: Math.random() * 6 - 3,
dy: Math.random() * 6 - 3
});
}
// 绘制所有球
function drawBalls() {
balls.forEach(ball => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
});
}
// 更新物理状态
function updatePhysics() {
balls.forEach(ball => {
// 应用重力
ball.dy += physics.gravity;
// 应用摩擦力
ball.dx *= physics.friction;
ball.dy *= physics.friction;
// 更新位置
ball.x += ball.dx;
ball.y += ball.dy;
// 边界碰撞检测
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
ball.dx = -ball.dx * physics.bounce;
} else if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.dx = -ball.dx * physics.bounce;
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius;
ball.dy = -ball.dy * physics.bounce;
} else if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.dy = -ball.dy * physics.bounce;
}
});
}
// 球体间碰撞检测
function checkCollisions() {
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[i];
const ball2 = balls[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
// 碰撞响应
const angle = Math.atan2(dy, dx);
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// 旋转坐标系
const vx1 = ball1.dx * cos + ball1.dy * sin;
const vy1 = ball1.dy * cos - ball1.dx * sin;
const vx2 = ball2.dx * cos + ball2.dy * sin;
const vy2 = ball2.dy * cos - ball2.dx * sin;
// 碰撞后速度
const v1Final = (vx1 * (ball1.radius - ball2.radius) + 2 * ball2.radius * vx2) /
(ball1.radius + ball2.radius);
const v2Final = (vx2 * (ball2.radius - ball1.radius) + 2 * ball1.radius * vx1) /
(ball1.radius + ball2.radius);
// 更新速度
ball1.dx = v1Final * cos - vy1 * sin;
ball1.dy = vy1 * cos + v1Final * sin;
ball2.dx = v2Final * cos - vy2 * sin;
ball2.dy = vy2 * cos + v2Final * sin;
// 防止球体重叠
const overlap = ball1.radius + ball2.radius - distance;
const moveX = overlap * cos * 0.5;
const moveY = overlap * sin * 0.5;
ball1.x -= moveX;
ball1.y -= moveY;
ball2.x += moveX;
ball2.y += moveY;
}
}
}
}
// 动画循环
function animatePhysics() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
updatePhysics();
checkCollisions();
drawBalls();
requestAnimationFrame(animatePhysics);
}
animatePhysics();
5. 创意绘图 - 交互式涂鸦板
html
<div>
<input type="color" id="colorPicker" value="#000000">
<input type="range" id="brushSize" min="1" max="50" value="5">
<button id="clearBtn">清除</button>
<button id="saveBtn">保存</button>
</div>
<canvas id="drawingCanvas" width="800" height="500"></canvas>
javascript
const drawingCanvas = document.getElementById('drawingCanvas');
const ctx = drawingCanvas.getContext('2d');
const colorPicker = document.getElementById('colorPicker');
const brushSize = document.getElementById('brushSize');
const clearBtn = document.getElementById('clearBtn');
const saveBtn = document.getElementById('saveBtn');
let isDrawing = false;
let lastX = 0;
let lastY = 0;
// 设置画布背景为白色
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, drawingCanvas.width, drawingCanvas.height);
ctx.fillStyle = colorPicker.value;
// 绘图函数
function draw(e) {
if (!isDrawing) return;
ctx.strokeStyle = colorPicker.value;
ctx.lineWidth = brushSize.value;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
[lastX, lastY] = [e.offsetX, e.offsetY];
}
// 鼠标事件
drawingCanvas.addEventListener('mousedown', (e) => {
isDrawing = true;
[lastX, lastY] = [e.offsetX, e.offsetY];
});
drawingCanvas.addEventListener('mousemove', draw);
drawingCanvas.addEventListener('mouseup', () => isDrawing = false);
drawingCanvas.addEventListener('mouseout', () => isDrawing = false);
// 触摸支持
drawingCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = drawingCanvas.getBoundingClientRect();
isDrawing = true;
[lastX, lastY] = [touch.clientX - rect.left, touch.clientY - rect.top];
});
drawingCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = drawingCanvas.getBoundingClientRect();
draw({
offsetX: touch.clientX - rect.left,
offsetY: touch.clientY - rect.top
});
});
drawingCanvas.addEventListener('touchend', () => isDrawing = false);
// 清除画布
clearBtn.addEventListener('click', () => {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, drawingCanvas.width, drawingCanvas.height);
});
// 保存图像
saveBtn.addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'drawing.png';
link.href = drawingCanvas.toDataURL('image/png');
link.click();
});
// 高级功能 - 橡皮擦
const eraserBtn = document.createElement('button');
eraserBtn.textContent = '橡皮擦';
document.querySelector('div').appendChild(eraserBtn);
eraserBtn.addEventListener('click', () => {
colorPicker.value = '#ffffff';
});
// 高级功能 - 渐变笔刷
const gradientBtn = document.createElement('button');
gradientBtn.textContent = '渐变笔刷';
document.querySelector('div').appendChild(gradientBtn);
let useGradient = false;
gradientBtn.addEventListener('click', () => {
useGradient = !useGradient;
gradientBtn.style.backgroundColor = useGradient ? '#4CAF50' : '';
});
// 修改绘图函数以支持渐变
drawingCanvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
if (useGradient) {
const gradient = ctx.createLinearGradient(lastX, lastY, e.offsetX, e.offsetY);
gradient.addColorStop(0, `hsl(${Math.random() * 360}, 100%, 50%)`);
gradient.addColorStop(1, `hsl(${Math.random() * 360}, 100%, 50%)`);
ctx.strokeStyle = gradient;
} else {
ctx.strokeStyle = colorPicker.value;
}
ctx.lineWidth = brushSize.value;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
[lastX, lastY] = [e.offsetX, e.offsetY];
});
这些实战项目展示了 Canvas 的强大功能,从简单的游戏开发到复杂的数据可视化和图像处理。你可以基于这些示例进行扩展,创建更复杂的应用。