作为一名前端开发者,第一次接触Canvas时那种既兴奋又困惑的感觉我至今记忆犹新。Canvas本质上就是浏览器提供的一块可以自由绘制的画布,它不像SVG那样基于矢量图形,而是直接操作像素,这既是它的优势也是它的局限。
很多初学者容易混淆Canvas和SVG,其实它们是完全不同的两种技术路线。SVG是基于XML的矢量图形描述语言,而Canvas则是通过JavaScript API直接操作像素的位图绘制技术。简单来说:
使用Canvas绘图的基本流程可以概括为以下几步:
javascript复制// 基本示例
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 设置样式
ctx.fillStyle = 'blue';
ctx.strokeStyle = 'red';
ctx.lineWidth = 5;
// 绘制矩形
ctx.fillRect(10, 10, 100, 100);
ctx.strokeRect(10, 10, 100, 100);
Canvas的坐标系原点(0,0)位于画布的左上角,x轴向右延伸,y轴向下延伸。这与我们常见的数学坐标系不同,需要特别注意。在绘制圆环时,我们通常需要先计算圆心位置:
javascript复制const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
Canvas的arc方法使用的是弧度而非角度,这是很多新手容易出错的地方。弧度与角度的转换公式如下:
code复制弧度 = 角度 × (π/180)
为了方便使用,我们可以封装一个转换函数:
javascript复制function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
Canvas没有直接绘制圆环的API,我们需要通过"挖洞"的方式实现:
javascript复制function drawRing(ctx, x, y, outerRadius, innerRadius, color) {
ctx.beginPath();
// 外圆 - 顺时针
ctx.arc(x, y, outerRadius, 0, Math.PI * 2, false);
// 内圆 - 逆时针
ctx.arc(x, y, innerRadius, 0, Math.PI * 2, true);
ctx.fillStyle = color;
ctx.fill();
}
让我们实现一个基础的进度环,它可以显示0-100%的进度:
javascript复制function drawProgressRing(ctx, x, y, radius, thickness, progress, bgColor, fgColor) {
const endAngle = -Math.PI/2 + (Math.PI * 2 * progress);
// 绘制背景环
ctx.beginPath();
ctx.arc(x, y, radius, -Math.PI/2, Math.PI*1.5);
ctx.lineWidth = thickness;
ctx.strokeStyle = bgColor;
ctx.stroke();
// 绘制进度环
ctx.beginPath();
ctx.arc(x, y, radius, -Math.PI/2, endAngle);
ctx.lineWidth = thickness;
ctx.strokeStyle = fgColor;
ctx.stroke();
}
使用requestAnimationFrame实现平滑的动画效果:
javascript复制function animateProgress(progress, duration) {
let startTime = null;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const currentProgress = Math.min(elapsed / duration, 1) * progress;
drawProgressRing(ctx, centerX, centerY, 100, 20, currentProgress, '#eee', '#00c896');
if (currentProgress < progress) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
在高DPI设备上,Canvas默认会显得模糊,这是因为物理像素与CSS像素不匹配导致的。解决方案是缩放Canvas:
javascript复制function setupCanvas(canvas) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
// 设置实际像素尺寸
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// 缩放上下文以匹配CSS尺寸
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return ctx;
}
javascript复制// 离屏渲染示例
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
// 在离屏Canvas上绘制静态内容
function renderStaticContent() {
offscreenCtx.fillStyle = '#f5f5f5';
offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
// 绘制其他静态元素...
}
// 在主Canvas上绘制动态内容
function renderDynamicContent() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreenCanvas, 0, 0);
// 绘制动态元素...
}
实现一个包含多个同心圆环的仪表盘,每个环代表不同的指标:
javascript复制const rings = [
{ radius: 120, thickness: 20, color: '#FF6B6B', progress: 0.7 },
{ radius: 90, thickness: 20, color: '#4ECDC4', progress: 0.5 },
{ radius: 60, thickness: 20, color: '#FFE66D', progress: 0.3 }
];
为每个圆环设置不同的动画时间和延迟,创造更自然的视觉效果:
javascript复制function animateDashboard() {
rings.forEach((ring, index) => {
setTimeout(() => {
animateRing(ring, 1500 + index * 300);
}, index * 200);
});
}
function animateRing(ring, duration) {
let startTime = null;
const targetProgress = ring.progress;
ring.progress = 0;
function step(currentTime) {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
ring.progress = Math.min(elapsed / duration, 1) * targetProgress;
if (ring.progress < targetProgress) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
Canvas的路径是累积的,如果不正确管理会导致奇怪的效果:
javascript复制// 错误示例 - 忘记beginPath
ctx.fillStyle = 'red';
ctx.arc(100, 100, 50, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'blue';
ctx.arc(200, 100, 50, 0, Math.PI*2);
ctx.fill(); // 两个圆都会变成蓝色!
// 正确做法
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(100, 100, 50, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = 'blue';
ctx.arc(200, 100, 50, 0, Math.PI*2);
ctx.fill();
javascript复制// 高清屏下的文字绘制
ctx.font = `${14 * dpr}px Arial`;
ctx.fillText('Hello World', 10 * dpr, 20 * dpr);
javascript复制// 使用transform优化旋转动画
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(angle);
ctx.translate(-centerX, -centerY);
// 绘制需要旋转的元素...
ctx.restore();
angle += 0.01;
requestAnimationFrame(render);
}
由于Canvas是一个整体,要实现元素级别的交互需要手动计算:
javascript复制canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否点击了圆环
rings.forEach(ring => {
const distance = Math.sqrt(
Math.pow(x - centerX, 2) +
Math.pow(y - centerY, 2)
);
if (distance > ring.radius - ring.thickness/2 &&
distance < ring.radius + ring.thickness/2) {
console.log(`点击了${ring.color}圆环`);
}
});
});
通过mousemove事件和重绘实现悬停效果:
javascript复制let hoveredRing = null;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let newHovered = null;
rings.forEach(ring => {
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
if (distance > ring.radius - ring.thickness/2 &&
distance < ring.radius + ring.thickness/2) {
newHovered = ring;
}
});
if (hoveredRing !== newHovered) {
hoveredRing = newHovered;
renderDashboard();
}
});
function renderDashboard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
rings.forEach(ring => {
const isHovered = ring === hoveredRing;
const thickness = isHovered ? ring.thickness * 1.2 : ring.thickness;
const color = isHovered ? lightenColor(ring.color, 20) : ring.color;
drawProgressRing(ctx, centerX, centerY, ring.radius, thickness, ring.progress, '#eee', color);
});
}
function lightenColor(color, percent) {
// 颜色变亮逻辑...
}
掌握了Canvas圆环绘制技术后,可以在实际项目中实现多种效果:
javascript复制// 自定义滑块控件示例
class CircularSlider {
constructor(canvas, options) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.value = options.value || 0.5;
this.min = options.min || 0;
this.max = options.max || 1;
this.radius = options.radius || 100;
this.thickness = options.thickness || 20;
this.color = options.color || '#00c896';
this.setupEvents();
this.render();
}
setupEvents() {
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
// 其他事件...
}
render() {
// 绘制滑块...
}
handleMouseDown(e) {
// 处理交互...
}
// 其他方法...
}
在实际项目中,我经常使用Canvas圆环来展示系统健康状态、任务进度等关键指标。相比传统的条形进度条,圆环进度更加醒目且节省空间,特别适合在仪表盘类应用中使用。