Canvas 进阶应用示例
下面我将介绍更多 Canvas 的进阶用法,包括变换、合成、像素操作以及更复杂的动画效果。
1. 坐标变换
Canvas 提供了几种变换图形位置和方向的方法:
javascript
// 保存当前状态(包括变换矩阵)
ctx.save();
// 平移
ctx.translate(100, 100);
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 50, 50);
// 旋转
ctx.rotate(Math.PI/4); // 45度
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 50, 50);
// 缩放
ctx.scale(1.5, 1.5);
ctx.fillStyle = 'green';
ctx.fillRect(0, 0, 50, 50);
// 恢复之前保存的状态
ctx.restore();
2. 路径与复杂图形
javascript
// 绘制复杂路径
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.quadraticCurveTo(200, 100, 150, 150); // 二次贝塞尔曲线
ctx.lineTo(50, 150);
ctx.bezierCurveTo(0, 100, 0, 100, 50, 50); // 三次贝塞尔曲线
ctx.closePath();
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
3. 图像合成
javascript
// 绘制第一个图形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
// 设置合成模式
ctx.globalCompositeOperation = 'lighter'; // 尝试不同的模式:source-over, multiply, screen等
// 绘制第二个图形(会与第一个图形按指定模式合成)
ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 100, 100);
// 重置合成模式
ctx.globalCompositeOperation = 'source-over';
4. 像素操作
javascript
// 绘制原始图像
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 200, 100);
// 获取像素数据
const imageData = ctx.getImageData(50, 50, 200, 100);
const data = imageData.data;
// 修改像素数据(反色效果)
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i+1] = 255 - data[i+1]; // G
data[i+2] = 255 - data[i+2]; // B
// data[i+3] 是alpha通道,保持不变
}
// 将修改后的像素数据放回canvas
ctx.putImageData(imageData, 50, 50);
5. 高级动画 - 粒子系统
javascript
// 创建粒子数组
const particles = [];
for (let i = 0; i < 100; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 5 + 1,
color: `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.7)`,
speedX: Math.random() * 2 - 1,
speedY: Math.random() * 2 - 1
});
}
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新并绘制所有粒子
particles.forEach(p => {
// 更新位置
p.x += p.speedX;
p.y += p.speedY;
// 边界检测
if (p.x < 0 || p.x > canvas.width) p.speedX = -p.speedX;
if (p.y < 0 || p.y > canvas.height) p.speedY = -p.speedY;
// 绘制粒子
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
});
// 绘制粒子间的连线
drawConnections();
requestAnimationFrame(animateParticles);
}
function drawConnections() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const p1 = particles[i];
const p2 = particles[j];
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = `rgba(150, 150, 150, ${1 - distance/100})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
}
animateParticles();
6. 绘制图表
javascript
// 示例数据
const data = [30, 45, 60, 35, 70, 40, 80];
const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
function drawBarChart() {
const chartWidth = 400;
const chartHeight = 200;
const chartX = 50;
const chartY = 50;
const barWidth = 40;
const gap = 20;
const maxValue = Math.max(...data);
// 绘制坐标轴
ctx.beginPath();
ctx.moveTo(chartX, chartY);
ctx.lineTo(chartX, chartY + chartHeight);
ctx.lineTo(chartX + chartWidth, chartY + chartHeight);
ctx.strokeStyle = 'black';
ctx.stroke();
// 绘制柱状图
data.forEach((value, index) => {
const barHeight = (value / maxValue) * chartHeight;
const x = chartX + index * (barWidth + gap) + gap;
const y = chartY + chartHeight - barHeight;
ctx.fillStyle = `hsl(${index * 50}, 70%, 60%)`;
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制标签
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
ctx.fillText(labels[index], x + barWidth/2 - 10, chartY + chartHeight + 15);
ctx.fillText(value.toString(), x + barWidth/2 - 5, y - 5);
});
}
drawBarChart();
7. 图像滤镜效果
javascript
// 加载图像
const img = new Image();
img.src = 'path/to/image.jpg';
img.onload = function() {
// 绘制原始图像
ctx.drawImage(img, 50, 50, 200, 200);
// 应用灰度滤镜
applyFilter(50, 300, 'grayscale');
// 应用复古滤镜
applyFilter(300, 50, 'sepia');
// 应用反色滤镜
applyFilter(300, 300, 'invert');
};
function applyFilter(x, y, type) {
// 绘制图像到临时位置
ctx.drawImage(img, -1000, -1000, 200, 200);
// 获取像素数据
const imageData = ctx.getImageData(-1000, -1000, 200, 200);
const data = imageData.data;
// 应用滤镜
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
switch(type) {
case 'grayscale':
const avg = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = data[i+1] = data[i+2] = avg;
break;
case 'sepia':
data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
data[i+1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
data[i+2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
break;
case 'invert':
data[i] = 255 - r;
data[i+1] = 255 - g;
data[i+2] = 255 - b;
break;
}
}
// 将处理后的图像放回指定位置
ctx.putImageData(imageData, x, y);
}
8. 性能优化技巧
- 离屏渲染:对于复杂的、重复绘制的图形,可以先在离屏 canvas 上绘制,然后复制到主 canvas。
javascript
// 创建离屏canvas
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
const offscreenCtx = offscreenCanvas.getContext('2d');
// 在离屏canvas上绘制复杂图形
offscreenCtx.fillStyle = 'red';
offscreenCtx.beginPath();
offscreenCtx.arc(50, 50, 40, 0, Math.PI * 2);
offscreenCtx.fill();
// 复制到主canvas(可以多次复制)
ctx.drawImage(offscreenCanvas, 50, 50);
ctx.drawImage(offscreenCanvas, 200, 100);
-
避免频繁的绘制操作:只在必要时重绘整个 canvas。
-
使用 requestAnimationFrame 而不是 setInterval 或 setTimeout 来实现动画。
-
减少绘制区域 :使用
clearRect()
只清除需要更新的区域,而不是整个 canvas。
这些进阶技术可以帮助你创建更复杂、更高效的 Canvas 应用。根据项目需求,你可以组合使用这些技术来实现各种效果。
上一个文字 juejin.cn/post/749159...