04-Canvas与SVG
📋 学习目标
掌握Canvas 2D绘图API
理解SVG矢量图形
学习Canvas与SVG的应用场景
实现图表和交互式图形
🎨 Canvas基础
Canvas元素
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持Canvas
</canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
</script>
要点:
width和height属性设置画布尺寸(默认300x150)
不要用CSS设置尺寸,会导致缩放失真
使用getContext(‘2d’)获取2D渲染上下文
绘制基本形状
矩形
const ctx = canvas.getContext('2d');
// 填充矩形
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(10, 10, 100, 100);
// 描边矩形
ctx.strokeStyle = '#339af0';
ctx.lineWidth = 2;
ctx.strokeRect(120, 10, 100, 100);
// 清除矩形
ctx.clearRect(50, 50, 20, 20);
路径绘制
// 三角形
ctx.beginPath();
ctx.moveTo(250, 10);
ctx.lineTo(300, 110);
ctx.lineTo(200, 110);
ctx.closePath();
ctx.fillStyle = '#51cf66';
ctx.fill();
ctx.strokeStyle = '#2f9e44';
ctx.stroke();
// 圆形
ctx.beginPath();
ctx.arc(400, 60, 50, 0, Math.PI * 2);
ctx.fillStyle = '#ffd43b';
ctx.fill();
// 圆弧
ctx.beginPath();
ctx.arc(500, 60, 50, 0, Math.PI, false);
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 3;
ctx.stroke();
// 贝塞尔曲线
ctx.beginPath();
ctx.moveTo(600, 10);
ctx.quadraticCurveTo(650, 10, 650, 60); // 二次贝塞尔
ctx.bezierCurveTo(650, 110, 600, 110, 600, 60); // 三次贝塞尔
ctx.stroke();
样式和颜色
颜色设置
// 填充颜色
ctx.fillStyle = '#ff6b6b';
ctx.fillStyle = 'rgb(255, 107, 107)';
ctx.fillStyle = 'rgba(255, 107, 107, 0.5)';
// 描边颜色
ctx.strokeStyle = '#339af0';
// 渐变
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, '#ff6b6b');
gradient.addColorStop(1, '#339af0');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
// 径向渐变
const radialGradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 50);
radialGradient.addColorStop(0, '#fff');
radialGradient.addColorStop(1, '#000');
ctx.fillStyle = radialGradient;
ctx.fillRect(50, 50, 100, 100);
// 图案
const img = new Image();
img.src = 'pattern.png';
img.onload = () => {
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 200, 200);
};
线条样式
ctx.lineWidth = 5;
ctx.lineCap = 'round'; // butt, round, square
ctx.lineJoin = 'round'; // miter, round, bevel
ctx.setLineDash([5, 10]); // 虚线
ctx.lineDashOffset = 0;
文本绘制
// 填充文本
ctx.font = '30px Arial';
ctx.fillStyle = '#000';
ctx.fillText('Hello Canvas', 10, 50);
// 描边文本
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.strokeText('Hello Canvas', 10, 100);
// 文本对齐
ctx.textAlign = 'left'; // left, right, center, start, end
ctx.textBaseline = 'top'; // top, middle, bottom, alphabetic, hanging
// 测量文本
const metrics = ctx.measureText('Hello');
console.log(metrics.width); // 文本宽度
图像处理
const img = new Image();
img.src = 'photo.jpg';
img.onload = () => {
// 绘制图像
ctx.drawImage(img, 0, 0);
// 缩放
ctx.drawImage(img, 0, 0, 200, 150);
// 裁剪和绘制
ctx.drawImage(
img,
50, 50, 100, 100, // 源图像裁剪区域
0, 0, 200, 200 // 目标画布区域
);
};
// 像素操作
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data; // [r, g, b, a, r, g, b, a, ...]
// 灰度化
for (let i = 0; i < pixels.length; i += 4) {
const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
pixels[i] = avg; // R
pixels[i + 1] = avg; // G
pixels[i + 2] = avg; // B
}
ctx.putImageData(imageData, 0, 0);
变换
基本变换
// 平移
ctx.translate(100, 100);
// 旋转(弧度)
ctx.rotate(Math.PI / 4);
// 缩放
ctx.scale(2, 2);
// 重置变换
ctx.setTransform(1, 0, 0, 1, 0, 0);
// 矩阵变换
ctx.transform(1, 0, 0, 1, 0, 0);
保存和恢复状态
ctx.fillStyle = '#ff6b6b';
ctx.save(); // 保存当前状态
ctx.fillStyle = '#339af0';
ctx.fillRect(0, 0, 100, 100);
ctx.restore(); // 恢复之前的状态
ctx.fillRect(120, 0, 100, 100); // 使用红色
动画
基本动画循环
let x = 0;
function animate() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(x, 100, 50, 50);
// 更新位置
x += 2;
if (x > canvas.width) x = -50;
// 下一帧
requestAnimationFrame(animate);
}
animate();
小球弹跳动画
const ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 20,
color: '#ff6b6b'
};
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
}
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
// 更新位置
ball.x += ball.vx;
ball.y += ball.vy;
// 边界检测
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.vx = -ball.vx;
}
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.vy = -ball.vy;
}
requestAnimationFrame(update);
}
update();
📐 SVG基础
SVG元素
<!-- 内联SVG -->
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="80" fill="#ff6b6b"/>
</svg>
<!-- 外部SVG文件 -->
<img src="image.svg" alt="SVG图片">
<object data="image.svg" type="image/svg+xml"></object>
<embed src="image.svg" type="image/svg+xml">
<!-- 背景图片 -->
<div style="background-image: url('image.svg')"></div>
基本形状
矩形
<svg width="300" height="200">
<!-- 基本矩形 -->
<rect x="10" y="10" width="100" height="80" fill="#ff6b6b"/>
<!-- 圆角矩形 -->
<rect x="120" y="10" width="100" height="80" rx="10" ry="10" fill="#339af0"/>
<!-- 描边矩形 -->
<rect x="230" y="10" width="60" height="80"
fill="none" stroke="#51cf66" stroke-width="3"/>
</svg>
圆形和椭圆
<svg width="400" height="200">
<!-- 圆形 -->
<circle cx="100" cy="100" r="80" fill="#ffd43b"/>
<!-- 椭圆 -->
<ellipse cx="250" cy="100" rx="100" ry="60" fill="#ff6b6b"/>
</svg>
线条和多边形
<svg width="400" height="300">
<!-- 直线 -->
<line x1="10" y1="10" x2="200" y2="100"
stroke="#339af0" stroke-width="2"/>
<!-- 折线 -->
<polyline points="10,150 50,100 90,130 130,90 170,110"
fill="none" stroke="#ff6b6b" stroke-width="2"/>
<!-- 多边形 -->
<polygon points="250,50 300,150 200,150"
fill="#51cf66" stroke="#2f9e44" stroke-width="2"/>
</svg>
路径
<svg width="400" height="300">
<!-- 基本路径 -->
<path d="M 10 10 L 100 10 L 100 100 Z" fill="#ff6b6b"/>
<!-- 曲线路径 -->
<path d="M 150 10 Q 200 50 150 100"
fill="none" stroke="#339af0" stroke-width="2"/>
<!-- 贝塞尔曲线 -->
<path d="M 250 10 C 250 50, 350 50, 350 100"
fill="none" stroke="#51cf66" stroke-width="2"/>
<!-- 圆弧 -->
<path d="M 50 150 A 50 50 0 0 1 150 150"
fill="none" stroke="#ffd43b" stroke-width="2"/>
</svg>
路径命令:
M/m: 移动到 (moveto)
L/l: 直线到 (lineto)
H/h: 水平线 (horizontal)
V/v: 垂直线 (vertical)
C/c: 三次贝塞尔曲线
Q/q: 二次贝塞尔曲线
A/a: 圆弧
Z/z: 闭合路径
SVG样式
填充和描边
<svg width="400" height="200">
<!-- 填充 -->
<circle cx="100" cy="100" r="50" fill="#ff6b6b"/>
<!-- 描边 -->
<circle cx="250" cy="100" r="50"
fill="none"
stroke="#339af0"
stroke-width="5"
stroke-dasharray="5,5"
stroke-linecap="round"/>
</svg>
渐变
<svg width="400" height="200">
<defs>
<!-- 线性渐变 -->
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#339af0;stop-opacity:1"/>
</linearGradient>
<!-- 径向渐变 -->
<radialGradient id="grad2">
<stop offset="0%" style="stop-color:#fff;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#ff6b6b;stop-opacity:1"/>
</radialGradient>
</defs>
<rect x="10" y="10" width="180" height="180" fill="url(#grad1)"/>
<circle cx="300" cy="100" r="80" fill="url(#grad2)"/>
</svg>
滤镜
<svg width="400" height="200">
<defs>
<!-- 模糊滤镜 -->
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="5"/>
</filter>
<!-- 阴影 -->
<filter id="shadow">
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.5"/>
</filter>
</defs>
<rect x="10" y="10" width="100" height="100"
fill="#ff6b6b" filter="url(#blur)"/>
<circle cx="250" cy="60" r="50"
fill="#339af0" filter="url(#shadow)"/>
</svg>
SVG动画
CSS动画
<svg width="200" height="200">
<circle cx="100" cy="100" r="50" fill="#ff6b6b">
<animate attributeName="r"
from="50" to="80"
dur="1s"
repeatCount="indefinite"
direction="alternate"/>
</circle>
</svg>
<style>
@keyframes pulse {
0%, 100% { r: 50px; }
50% { r: 80px; }
}
circle {
animation: pulse 2s infinite;
}
</style>
SMIL动画
<svg width="400" height="200">
<!-- 移动动画 -->
<circle cx="50" cy="100" r="20" fill="#ff6b6b">
<animate attributeName="cx"
from="50" to="350"
dur="3s"
repeatCount="indefinite"/>
</circle>
<!-- 路径动画 -->
<path id="motionPath"
d="M 50 50 Q 200 150 350 50"
fill="none" stroke="#ddd"/>
<circle r="10" fill="#339af0">
<animateMotion dur="3s" repeatCount="indefinite">
<mpath href="#motionPath"/>
</animateMotion>
</circle>
</svg>
🎯 实战案例
Canvas图表示例
// 柱状图
function drawBarChart(data) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const maxValue = Math.max(...data.map(d => d.value));
const barWidth = width / data.length - 20;
const scale = (height - 40) / maxValue;
ctx.clearRect(0, 0, width, height);
data.forEach((item, index) => {
const barHeight = item.value * scale;
const x = index * (barWidth + 20) + 10;
const y = height - barHeight - 20;
// 绘制柱子
ctx.fillStyle = '#339af0';
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制标签
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + barWidth / 2, height - 5);
// 绘制数值
ctx.fillText(item.value, x + barWidth / 2, y - 5);
});
}
// 使用
const chartData = [
{ label: '一月', value: 120 },
{ label: '二月', value: 80 },
{ label: '三月', value: 150 },
{ label: '四月', value: 100 }
];
drawBarChart(chartData);
SVG交互图标
<svg width="200" height="200" id="interactive-icon">
<defs>
<style>
.icon-part {
transition: all 0.3s ease;
cursor: pointer;
}
.icon-part:hover {
fill: #ff6b6b;
transform: scale(1.1);
}
</style>
</defs>
<g class="icon-part">
<circle cx="100" cy="100" r="40" fill="#339af0"/>
<path d="M 80 100 L 120 100 M 100 80 L 100 120"
stroke="#fff" stroke-width="4"/>
</g>
</svg>
<script>
document.getElementById('interactive-icon').addEventListener('click', () => {
alert('图标被点击!');
});
</script>
⚖️ Canvas vs SVG对比
Canvas优势
像素级操作
高性能动画
适合大量对象
游戏开发
图像处理
SVG优势
矢量图形,无损缩放
可通过CSS和JavaScript操作
可访问性更好
文件体积小
适合图表和图标
选择建议
Canvas适用于:
- 实时游戏渲染
- 复杂动画效果
- 像素级图像处理
- 大量对象(>1000)
SVG适用于:
- 图标和Logo
- 数据可视化
- 需要交互的图形
- 响应式图形
📚 实践练习
练习1:Canvas时钟
实现一个实时时钟,包含:
表盘、刻度、指针
时分秒实时更新
平滑动画效果
练习2:SVG图表
创建一个SVG饼图,要求:
数据驱动
鼠标悬停提示
动画过渡效果
练习3:Canvas画板
实现一个简单的画板应用:
自由绘制
颜色选择
线条粗细
橡皮擦和清空