创建你的第一个Canvas程序,从零开始学习Canvas绑制,包含多个实战案例。 让我们从创建第一个Canvas程序开始,在实践中学习Canvas的基本用法。
首先,在HTML中添加canvas元素:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的第一个Canvas程序</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="300">
您的浏览器不支持Canvas
</canvas>
</body>
</html>
几个注意点:
width和height属性定义画布尺寸(单位:像素)Canvas本身只是一个容器,绑制操作需要通过"上下文"完成:
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
getContext('2d')返回一个2D绑制上下文对象,它提供了所有绑制方法。
class CanvasInitializer {
constructor(options = {}) {
this.options = {
id: 'myCanvas',
width: 400,
height: 300,
backgroundColor: '#ffffff',
...options
}
this.canvas = null
this.ctx = null
this.init()
}
init() {
this.canvas = document.getElementById(this.options.id)
if (!this.canvas) {
this.canvas = document.createElement('canvas')
this.canvas.id = this.options.id
document.body.appendChild(this.canvas)
}
this.canvas.width = this.options.width
this.canvas.height = this.options.height
this.ctx = this.canvas.getContext('2d')
if (this.options.backgroundColor) {
this.clear(this.options.backgroundColor)
}
}
clear(color = this.options.backgroundColor) {
this.ctx.fillStyle = color
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
}
getCanvas() {
return this.canvas
}
getContext() {
return this.ctx
}
resize(width, height) {
this.canvas.width = width
this.canvas.height = height
this.options.width = width
this.options.height = height
}
toDataURL(type = 'image/png') {
return this.canvas.toDataURL(type)
}
download(filename = 'canvas-image.png') {
const link = document.createElement('a')
link.download = filename
link.href = this.toDataURL()
link.click()
}
}
<canvas id="myCanvas" width="400" height="200"></canvas>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const colors = ['#e74c3c', '#2ecc71', '#3498db', '#f1c40f', '#9b59b6']
const rectangles = []
let time = 0
const count = 5
const rectWidth = 60
const rectHeight = 80
const gap = 20
const startX = (canvas.width - (count * rectWidth + (count - 1) * gap)) / 2
for (let i = 0; i < count; i++) {
rectangles.push({
x: startX + i * (rectWidth + gap),
y: canvas.height / 2 - rectHeight / 2,
width: rectWidth,
height: rectHeight,
color: colors[i],
phase: i * 0.5
})
}
function animate() {
time += 0.05
ctx.fillStyle = '#f8f9fa'
ctx.fillRect(0, 0, canvas.width, canvas.height)
rectangles.forEach((rect) => {
const offsetY = Math.sin(time + rect.phase) * 20
ctx.fillStyle = rect.color
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'
ctx.shadowBlur = 10
ctx.shadowOffsetY = 5
ctx.fillRect(rect.x, rect.y + offsetY, rect.width, rect.height)
})
ctx.shadowColor = 'transparent'
requestAnimationFrame(animate)
}
animate()
</script>
| 方法 | 说明 |
|---|---|
fillRect(x, y, w, h) | 绑制填充矩形 |
fillStyle | 设置填充颜色 |
shadowColor/Blur/OffsetY | 设置阴影效果 |
Math.sin() | 正弦函数,用于创建平滑动画 |
requestAnimationFrame() | 浏览器动画帧回调 |
<canvas id="myCanvas" width="400" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const circles = []
let mouseX = canvas.width / 2
let mouseY = canvas.height / 2
for (let i = 0; i < 20; i++) {
circles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 20 + 10,
color: `hsl(${Math.random() * 360}, 70%, 60%)`,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2
})
}
canvas.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect()
mouseX = e.clientX - rect.left
mouseY = e.clientY - rect.top
})
function animate() {
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, canvas.width, canvas.height)
circles.forEach(circle => {
const dx = mouseX - circle.x
const dy = mouseY - circle.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 100) {
circle.vx -= dx * 0.001
circle.vy -= dy * 0.001
}
circle.x += circle.vx
circle.y += circle.vy
if (circle.x < 0 || circle.x > canvas.width) circle.vx *= -1
if (circle.y < 0 || circle.y > canvas.height) circle.vy *= -1
ctx.beginPath()
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2)
ctx.fillStyle = circle.color
ctx.fill()
ctx.beginPath()
ctx.arc(circle.x, circle.y, circle.radius + 5, 0, Math.PI * 2)
ctx.strokeStyle = circle.color.replace('60%', '40%')
ctx.lineWidth = 2
ctx.stroke()
})
requestAnimationFrame(animate)
}
animate()
</script>
| 方法 | 说明 |
|---|---|
arc(x, y, r, start, end) | 绑制圆弧/圆形 |
beginPath() | 开始新路径 |
fill() | 填充当前路径 |
stroke() | 描边当前路径 |
getBoundingClientRect() | 获取元素位置信息 |
<canvas id="myCanvas" width="300" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const faceRadius = 100
let time = 0
function drawFace() {
const bounce = Math.sin(time * 2) * 5
ctx.beginPath()
ctx.arc(centerX, centerY + bounce, faceRadius, 0, Math.PI * 2)
const gradient = ctx.createRadialGradient(
centerX - 30, centerY - 30 + bounce, 0,
centerX, centerY + bounce, faceRadius
)
gradient.addColorStop(0, '#ffe066')
gradient.addColorStop(1, '#f1c40f')
ctx.fillStyle = gradient
ctx.fill()
ctx.strokeStyle = '#d4a800'
ctx.lineWidth = 3
ctx.stroke()
}
function drawEyes() {
const blink = Math.sin(time * 3) > 0.95 ? 0.1 : 1
const eyeOffsetX = 30
const eyeOffsetY = -20
const eyeRadius = 15
;[-1, 1].forEach(side => {
const eyeX = centerX + eyeOffsetX * side
const eyeY = centerY + eyeOffsetY
ctx.beginPath()
ctx.ellipse(eyeX, eyeY, eyeRadius, eyeRadius * blink, 0, 0, Math.PI * 2)
ctx.fillStyle = '#2c3e50'
ctx.fill()
if (blink > 0.5) {
ctx.beginPath()
ctx.arc(eyeX + 5, eyeY - 5, 5, 0, Math.PI * 2)
ctx.fillStyle = '#ffffff'
ctx.fill()
}
})
}
function drawMouth() {
ctx.beginPath()
ctx.arc(centerX, centerY + 20, 50, 0.2 * Math.PI, 0.8 * Math.PI)
ctx.strokeStyle = '#2c3e50'
ctx.lineWidth = 4
ctx.lineCap = 'round'
ctx.stroke()
ctx.beginPath()
ctx.ellipse(centerX, centerY + 40, 30, 20, 0, 0, Math.PI)
ctx.fillStyle = '#c0392b'
ctx.fill()
}
function drawCheeks() {
;[-1, 1].forEach(side => {
const gradient = ctx.createRadialGradient(
centerX + 65 * side, centerY + 15, 0,
centerX + 65 * side, centerY + 15, 20
)
gradient.addColorStop(0, 'rgba(231, 76, 60, 0.5)')
gradient.addColorStop(1, 'rgba(231, 76, 60, 0)')
ctx.beginPath()
ctx.arc(centerX + 65 * side, centerY + 15, 20, 0, Math.PI * 2)
ctx.fillStyle = gradient
ctx.fill()
})
}
function animate() {
time += 0.02
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawFace()
drawCheeks()
drawEyes()
drawMouth()
requestAnimationFrame(animate)
}
animate()
</script>
| 方法 | 说明 |
|---|---|
createRadialGradient() | 创建径向渐变 |
ellipse() | 绑制椭圆 |
lineCap | 线条端点样式 |
clearRect() | 清除矩形区域 |
<canvas id="myCanvas" width="400" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const particles = []
class Particle {
constructor(x, y) {
this.x = x
this.y = y
const angle = Math.random() * Math.PI * 2
const speed = Math.random() * 5 + 2
this.vx = Math.cos(angle) * speed
this.vy = Math.sin(angle) * speed
this.radius = Math.random() * 4 + 2
this.color = `hsl(${Math.random() * 60 + 10}, 100%, 50%)`
this.life = 1
this.decay = Math.random() * 0.02 + 0.01
}
update() {
this.x += this.vx
this.y += this.vy
this.vy += 0.1
this.life -= this.decay
}
draw() {
ctx.globalAlpha = this.life
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius * this.life, 0, Math.PI * 2)
ctx.fillStyle = this.color
ctx.fill()
ctx.globalAlpha = 1
}
}
function createExplosion(x, y) {
for (let i = 0; i < 30; i++) {
particles.push(new Particle(x, y))
}
}
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
createExplosion(x, y)
})
function animate() {
ctx.fillStyle = 'rgba(26, 26, 46, 0.2)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update()
particles[i].draw()
if (particles[i].life <= 0) {
particles.splice(i, 1)
}
}
requestAnimationFrame(animate)
}
animate()
</script>
| 概念 | 说明 |
|---|---|
class Particle | 粒子类,封装粒子属性和行为 |
Math.cos/sin | 计算粒子运动方向 |
globalAlpha | 全局透明度 |
splice() | 从数组中移除死亡粒子 |
<canvas id="myCanvas" width="400" height="200"></canvas>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
let time = 0
function animate() {
time += 0.01
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
const hue1 = (time * 50) % 360
const hue2 = (hue1 + 60) % 360
const hue3 = (hue1 + 120) % 360
gradient.addColorStop(0, `hsl(${hue1}, 70%, 60%)`)
gradient.addColorStop(0.5, `hsl(${hue2}, 70%, 50%)`)
gradient.addColorStop(1, `hsl(${hue3}, 70%, 60%)`)
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < 5; i++) {
const x = (time * 100 + i * 100) % (canvas.width + 100) - 50
const y = canvas.height / 2 + Math.sin(time * 2 + i) * 30
ctx.beginPath()
ctx.arc(x, y, 20, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
ctx.fill()
}
requestAnimationFrame(animate)
}
animate()
</script>
| 方法 | 说明 |
|---|---|
createLinearGradient(x0, y0, x1, y1) | 创建线性渐变 |
addColorStop(offset, color) | 添加颜色停止点 |
hsl() | HSL颜色格式,方便动态计算色相 |
<canvas id="myCanvas" width="400" height="300"></canvas>
<div>
<button id="clearBtn">清除画布</button>
<input type="color" id="brushColor" value="#e74c3c">
<input type="range" id="brushSize" min="1" max="30" value="5">
</div>
<script>
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')
const colorPicker = document.getElementById('brushColor')
const sizePicker = document.getElementById('brushSize')
const clearBtn = document.getElementById('clearBtn')
let isDrawing = false
let lastX = 0
let lastY = 0
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
canvas.addEventListener('mousedown', function(e) {
isDrawing = true
const rect = canvas.getBoundingClientRect()
lastX = e.clientX - rect.left
lastY = e.clientY - rect.top
})
canvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(x, y)
ctx.strokeStyle = colorPicker.value
ctx.lineWidth = sizePicker.value
ctx.stroke()
lastX = x
lastY = y
})
canvas.addEventListener('mouseup', function() {
isDrawing = false
})
canvas.addEventListener('mouseleave', function() {
isDrawing = false
})
clearBtn.addEventListener('click', function() {
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
})
</script>
| 事件 | 说明 |
|---|---|
mousedown | 鼠标按下开始绑制 |
mousemove | 鼠标移动时绑制线条 |
mouseup/mouseleave | 停止绑制 |
touchstart/touchmove/touchend | 触摸事件支持移动端 |
// 错误:用CSS设置尺寸会导致图形变形
canvas.style.width = '400px'
canvas.style.height = '300px'
// 正确:使用属性设置
canvas.width = 400
canvas.height = 300
// 错误:canvas还不存在
const ctx = document.getElementById('myCanvas').getContext('2d')
// 正确:等待DOM加载完成
window.onload = function() {
const ctx = document.getElementById('myCanvas').getContext('2d')
}
// 或者把script放在canvas后面
// 错误:canvas元素没有绑制方法
const canvas = document.getElementById('myCanvas')
canvas.fillRect(0, 0, 100, 100) // 报错!
// 正确:通过上下文绑制
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 100, 100)
在浏览器开发者工具中:
// 将画布内容转为图片
const dataURL = canvas.toDataURL()
console.log(dataURL) // base64格式的图片数据
// 在新窗口打开
window.open(dataURL)
创建Canvas程序的基本步骤:
<canvas>元素getContext('2d')获取绑制上下文接下来,我们将深入学习Canvas的坐标系统和更多绑制方法。