Canvas是HTML5提供的绘图元素,可以通过JavaScript在网页上绘制图形、动画和图像。本章将介绍Canvas的基本概念和2D绑图技术。
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持Canvas
</canvas>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
console.log(canvas.width);
console.log(canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
const canvasState = {
save: '保存当前状态(变换矩阵、裁剪区域、样式属性)',
restore: '恢复到上次保存的状态',
stateStack: '状态以栈的形式存储,可以多次save/restore',
example: `
ctx.fillStyle = 'red';
ctx.save();
ctx.fillStyle = 'blue';
ctx.translate(100, 100);
ctx.save();
ctx.fillStyle = 'green';
ctx.translate(50, 50);
ctx.restore();
console.log(ctx.fillStyle);
ctx.restore();
console.log(ctx.fillStyle);
`
};
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.strokeRect(130, 10, 100, 50);
ctx.clearRect(20, 20, 80, 30);
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(150, 150);
ctx.closePath();
ctx.strokeStyle = 'green';
ctx.lineWidth = 3;
ctx.stroke();
ctx.fillStyle = 'rgba(0, 255, 0, 0.3)';
ctx.fill();
ctx.beginPath();
ctx.moveTo(200, 50);
ctx.lineTo(300, 150);
ctx.lineTo(200, 150);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = 'orange';
ctx.fill();
ctx.beginPath();
ctx.arc(200, 100, 50, 0, Math.PI);
ctx.strokeStyle = 'purple';
ctx.lineWidth = 3;
ctx.stroke();
ctx.beginPath();
ctx.arc(300, 100, 50, 0, Math.PI, true);
ctx.stroke();
ctx.beginPath();
ctx.arc(100, 200, 50, 0, Math.PI * 2);
ctx.arc(100, 200, 30, 0, Math.PI * 2);
ctx.fillStyle = 'cyan';
ctx.fill('evenodd');
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.quadraticCurveTo(100, 100, 200, 200);
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250, 200);
ctx.bezierCurveTo(280, 100, 350, 300, 400, 200);
ctx.strokeStyle = 'blue';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(50, 300);
ctx.lineTo(100, 300);
ctx.arcTo(150, 300, 150, 350, 50);
ctx.lineTo(150, 400);
ctx.stroke();
ctx.fillStyle = 'red';
ctx.fillStyle = '#ff0000';
ctx.fillStyle = '#f00';
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillStyle = 'hsl(0, 100%, 50%)';
ctx.fillStyle = 'hsla(0, 100%, 50%, 0.5)';
ctx.strokeStyle = 'blue';
ctx.lineWidth = 5;
ctx.lineCap = 'butt';
ctx.lineCap = 'round';
ctx.lineCap = 'square';
ctx.lineJoin = 'miter';
ctx.lineJoin = 'round';
ctx.lineJoin = 'bevel';
ctx.miterLimit = 10;
ctx.setLineDash([10, 5]);
ctx.lineDashOffset = 0;
const linearGradient = ctx.createLinearGradient(0, 0, 200, 0);
linearGradient.addColorStop(0, 'red');
linearGradient.addColorStop(0.5, 'yellow');
linearGradient.addColorStop(1, 'blue');
ctx.fillStyle = linearGradient;
ctx.fillRect(10, 10, 200, 100);
const radialGradient = ctx.createRadialGradient(100, 300, 10, 100, 300, 100);
radialGradient.addColorStop(0, 'white');
radialGradient.addColorStop(1, 'black');
ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(100, 300, 100, 0, Math.PI * 2);
ctx.fill();
const conicGradient = ctx.createConicGradient(0, 300, 100);
conicGradient.addColorStop(0, 'red');
conicGradient.addColorStop(0.25, 'yellow');
conicGradient.addColorStop(0.5, 'green');
conicGradient.addColorStop(0.75, 'blue');
conicGradient.addColorStop(1, 'red');
ctx.fillStyle = conicGradient;
ctx.beginPath();
ctx.arc(300, 100, 80, 0, Math.PI * 2);
ctx.fill();
const img = new Image();
img.onload = function() {
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
img.src = 'pattern.png';
const repeatModes = {
repeat: '水平和垂直重复',
'repeat-x': '仅水平重复',
'repeat-y': '仅垂直重复',
'no-repeat': '不重复'
};
ctx.font = '48px Arial';
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'black';
ctx.fillText('东巴文 Canvas教程', 50, 50);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 1;
ctx.strokeText('描边文本', 50, 120);
const text = '测量文本宽度';
const metrics = ctx.measureText(text);
console.log(`文本宽度: ${metrics.width}px`);
ctx.font = '24px Arial';
const textWidth = ctx.measureText('居中文本').width;
const x = (canvas.width - textWidth) / 2;
ctx.fillText('居中文本', x, 200);
ctx.font = 'italic bold 36px Georgia, serif';
ctx.textAlign = 'left';
ctx.textAlign = 'center';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.textBaseline = 'middle';
ctx.textBaseline = 'bottom';
ctx.textBaseline = 'alphabetic';
ctx.textBaseline = 'hanging';
ctx.direction = 'ltr';
ctx.direction = 'rtl';
ctx.direction = 'inherit';
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 0, 0, 200, 150);
ctx.drawImage(img,
0, 0, 100, 100,
250, 0, 200, 200
);
};
img.src = 'image.jpg';
const video = document.querySelector('video');
function drawFrame() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
requestAnimationFrame(drawFrame);
}
video.play();
drawFrame();
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg;
data[i + 1] = avg;
data[i + 2] = avg;
}
ctx.putImageData(imageData, img.width + 10, 0);
};
img.src = 'photo.jpg';
function invertColors(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
return imageData;
}
function adjustBrightness(imageData, factor) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, data[i] * factor);
data[i + 1] = Math.min(255, data[i + 1] * factor);
data[i + 2] = Math.min(255, data[i + 2] * factor);
}
return imageData;
}
function createImageData(width, height) {
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.random() * 255;
data[i + 1] = Math.random() * 255;
data[i + 2] = Math.random() * 255;
data[i + 3] = 255;
}
return imageData;
}
ctx.translate(100, 100);
ctx.fillRect(0, 0, 50, 50);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 50, 50);
ctx.scale(2, 0.5);
ctx.fillRect(0, 0, 50, 50);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.transform(1, 0, 0, 1, 100, 100);
ctx.transform(1, 0.5, 0, 1, 0, 0);
const matrix = {
description: '变换矩阵 [a, b, c, d, e, f]',
format: `
| a c e |
| b d f |
| 0 0 1 |
`,
parameters: {
a: '水平缩放',
b: '垂直倾斜',
c: '水平倾斜',
d: '垂直缩放',
e: '水平位移',
f: '垂直位移'
}
};
function drawRotatedRect(x, y, width, height, angle) {
ctx.save();
ctx.translate(x + width / 2, y + height / 2);
ctx.rotate(angle);
ctx.translate(-width / 2, -height / 2);
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
function drawSkewedRect(x, y, width, height, skewX, skewY) {
ctx.save();
ctx.setTransform(1, skewY, skewX, 1, x, y);
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
let animationId;
let x = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'blue';
ctx.fillRect(x, 100, 50, 50);
x += 2;
if (x > canvas.width) {
x = -50;
}
animationId = requestAnimationFrame(animate);
}
animate();
function stopAnimation() {
cancelAnimationFrame(animationId);
}
class Animation {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.objects = [];
this.isRunning = false;
this.lastTime = 0;
}
add(object) {
this.objects.push(object);
}
remove(object) {
const index = this.objects.indexOf(object);
if (index > -1) {
this.objects.splice(index, 1);
}
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
this.loop();
}
stop() {
this.isRunning = false;
}
loop() {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
this.update(deltaTime);
this.render();
requestAnimationFrame(() => this.loop());
}
update(deltaTime) {
this.objects.forEach(obj => {
if (obj.update) {
obj.update(deltaTime);
}
});
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.objects.forEach(obj => {
if (obj.render) {
obj.render(this.ctx);
}
});
}
}
class Ball {
constructor(x, y, radius, color, vx, vy) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.vx = vx;
this.vy = vy;
this.gravity = 200;
}
update(deltaTime) {
this.vy += this.gravity * deltaTime;
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
}
render(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
const animation = new Animation(canvas);
animation.add(new Ball(100, 100, 20, 'red', 100, 0));
animation.start();
let isDrawing = false;
let lastX = 0;
let lastY = 0;
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
[lastX, lastY] = [e.offsetX, e.offsetY];
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
[lastX, lastY] = [e.offsetX, e.offsetY];
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
});
canvas.addEventListener('mouseleave', () => {
isDrawing = false;
});
function getMousePos(e) {
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const pos = getTouchPos(touch);
isDrawing = true;
[lastX, lastY] = [pos.x, pos.y];
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!isDrawing) return;
const touch = e.touches[0];
const pos = getTouchPos(touch);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
[lastX, lastY] = [pos.x, pos.y];
});
canvas.addEventListener('touchend', () => {
isDrawing = false;
});
function getTouchPos(touch) {
const rect = canvas.getBoundingClientRect();
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
};
}
function pointInCircle(px, py, cx, cy, radius) {
const dx = px - cx;
const dy = py - cy;
return dx * dx + dy * dy <= radius * radius;
}
function pointInRect(px, py, rx, ry, rw, rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
function circleCollision(c1, c2) {
const dx = c2.x - c1.x;
const dy = c2.y - c1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < c1.radius + c2.radius;
}
function rectCollision(r1, r2) {
return r1.x < r2.x + r2.width &&
r1.x + r1.width > r2.x &&
r1.y < r2.y + r2.height &&
r1.y + r1.height > r2.y;
}
const objects = [
{ type: 'circle', x: 100, y: 100, radius: 30, color: 'red' },
{ type: 'rect', x: 200, y: 150, width: 60, height: 40, color: 'blue' }
];
canvas.addEventListener('click', (e) => {
const pos = getMousePos(e);
for (const obj of objects) {
let hit = false;
if (obj.type === 'circle') {
hit = pointInCircle(pos.x, pos.y, obj.x, obj.y, obj.radius);
} else if (obj.type === 'rect') {
hit = pointInRect(pos.x, pos.y, obj.x, obj.y, obj.width, obj.height);
}
if (hit) {
console.log('点击了对象:', obj);
}
}
});
function saveAsPNG() {
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'canvas-image.png';
link.href = dataURL;
link.click();
}
function saveAsJPEG(quality = 0.9) {
const dataURL = canvas.toDataURL('image/jpeg', quality);
const link = document.createElement('a');
link.download = 'canvas-image.jpg';
link.href = dataURL;
link.click();
}
async function saveAsBlob() {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/png');
});
}
const blob = await saveAsBlob();
const formData = new FormData();
formData.append('image', blob, 'canvas.png');
await fetch('/upload', { method: 'POST', body: formData });
🎨 Canvas性能优化
- 减少状态切换:批量绘制相同样式的图形
- 离屏Canvas:复杂图形先在离屏Canvas绘制
- 避免频繁清除:只清除需要更新的区域
- 使用requestAnimationFrame:保证动画流畅
- 合理使用save/restore:减少不必要的状态保存
📐 坐标系说明
Canvas坐标系原点在左上角:
- X轴向右为正
- Y轴向下为正
- 角度以弧度为单位,顺时针方向
下一章将探讨 WebGL基础,学习如何在浏览器中进行3D图形渲染。