Canvas API
基本绘图
js
// 获取绘制上下文
const ctx = canvas.getContext('2d')
// 使用上下文对象ctx完成后续绘图
// 获取绘制上下文
const ctx = canvas.getContext('2d')
// 使用上下文对象ctx完成后续绘图
2d 的 canvas 上下文支持以下图形的绘制:
- 直线(矩形 API)
ctx.beginPath
, ctx.moveTo
, ctx.lineTo
, ctx.stroke
, ctx.closePath
- 曲线(椭圆 API)
ctx.arc
- 文字
ctx.font
, ctx.fillText
, ctx.strokeText
- 图片
ctx
绘制流程:
生成路径
绘制
vue
<template>
<canvas ref="canvasRef" width="600" height="400"></canvas>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const canvasRef = ref()
onMounted(() => {
const canvas = canvasRef1.value
const ctx = canvas.getContext('2d')
// 画一个三角形
ctx.beginPath()
ctx.moveTo(100, 50)
ctx.lineTo(300, 100)
ctx.lineTo(123, 222)
ctx.closePath()
ctx.strokeStyle = '#000'
ctx.stroke()
// 画一个圆
ctx.beginPath()
ctx.arc(200, 300, 100, 0, 2 * Math.PI)
ctx.strokeStyle = '#f00'
ctx.stroke()
// 画文字
ctx.font = '50px serif'
ctx.fillText('Hello world', 300, 100)
// 画图片
let image = new Image()
image.src = '/img-example.jpg'
image.onload = () => {
ctx.drawImage(
image,
350,
200,
image.naturalWidth / 4,
image.naturalHeight / 4
)
}
})
</script>
<template>
<canvas ref="canvasRef" width="600" height="400"></canvas>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const canvasRef = ref()
onMounted(() => {
const canvas = canvasRef1.value
const ctx = canvas.getContext('2d')
// 画一个三角形
ctx.beginPath()
ctx.moveTo(100, 50)
ctx.lineTo(300, 100)
ctx.lineTo(123, 222)
ctx.closePath()
ctx.strokeStyle = '#000'
ctx.stroke()
// 画一个圆
ctx.beginPath()
ctx.arc(200, 300, 100, 0, 2 * Math.PI)
ctx.strokeStyle = '#f00'
ctx.stroke()
// 画文字
ctx.font = '50px serif'
ctx.fillText('Hello world', 300, 100)
// 画图片
let image = new Image()
image.src = '/img-example.jpg'
image.onload = () => {
ctx.drawImage(
image,
350,
200,
image.naturalWidth / 4,
image.naturalHeight / 4
)
}
})
</script>
自定义封装方法
源码如下:
vue
<template>
<canvas
ref="cvsRef"
style="width: 700px; height: 800px; background-color: #000"
></canvas>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const cvsRef = ref<any>()
const getRandom = (min, max) => {
return Math.floor(min + Math.random() * (max - min))
}
onMounted(() => {
const canvas = cvsRef.value
const ctx: CanvasRenderingContext2D = canvas.getContext('2d')
canvas.width = 700 * devicePixelRatio
canvas.height = 800 * devicePixelRatio
/**
* 生成一个点
*/
class Point {
x: number
y: number
r: number
xSpeed: number
ySpeed: number
lastDrawTime: number | null
constructor() {
this.r = 2 * devicePixelRatio
this.x = getRandom(0, canvas.width - this.r / 2)
this.y = getRandom(0, canvas.height - this.r / 2)
this.xSpeed = getRandom(-50, 50)
this.ySpeed = getRandom(-50, 50)
this.lastDrawTime = null
}
draw() {
if (this.lastDrawTime) {
// 计算新的坐标
const duration = (Date.now() - this.lastDrawTime) / 1000
const xDis = this.xSpeed * duration
const yDis = this.ySpeed * duration
let x = this.x + xDis
let y = this.y + yDis
if (x > canvas.width - this.r / 2 || x < this.r / 2) {
this.xSpeed = -this.xSpeed
}
if (y > canvas.height - this.r / 2 || y < this.r / 2) {
this.ySpeed = -this.ySpeed
}
this.x = x
this.y = y
}
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 2 * Math.PI, 0)
ctx.fillStyle = `rgb(200,200,200)`
ctx.fill()
this.lastDrawTime = Date.now()
}
}
class Graph {
/**
* 点的集合
*/
points: Point[]
/**
* 连线最大距离
*/
maxDis: number = 150 * devicePixelRatio
constructor(pointNumber = 30) {
this.points = new Array(pointNumber).fill(0).map(() => new Point())
}
drawLine(p1, p2) {
// 计算2点间距离
const d = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
if (d > this.maxDis) return
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
ctx.lineTo(p2.x, p2.y)
// 计算连线清晰度
ctx.strokeStyle = `rgba(200,200,200,${1 - d / this.maxDis})`
ctx.stroke()
}
/**
* 绘制
*/
draw() {
requestAnimationFrame(() => {
this.draw()
})
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < this.points.length; i++) {
let p1 = this.points[i]
p1.draw()
// 进行连线
for (let j = i + 1; j < this.points.length; j++) {
const p2 = this.points[j]
this.drawLine(p1, p2)
}
}
}
}
let graph = new Graph(100)
graph.draw()
})
</script>
<template>
<canvas
ref="cvsRef"
style="width: 700px; height: 800px; background-color: #000"
></canvas>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const cvsRef = ref<any>()
const getRandom = (min, max) => {
return Math.floor(min + Math.random() * (max - min))
}
onMounted(() => {
const canvas = cvsRef.value
const ctx: CanvasRenderingContext2D = canvas.getContext('2d')
canvas.width = 700 * devicePixelRatio
canvas.height = 800 * devicePixelRatio
/**
* 生成一个点
*/
class Point {
x: number
y: number
r: number
xSpeed: number
ySpeed: number
lastDrawTime: number | null
constructor() {
this.r = 2 * devicePixelRatio
this.x = getRandom(0, canvas.width - this.r / 2)
this.y = getRandom(0, canvas.height - this.r / 2)
this.xSpeed = getRandom(-50, 50)
this.ySpeed = getRandom(-50, 50)
this.lastDrawTime = null
}
draw() {
if (this.lastDrawTime) {
// 计算新的坐标
const duration = (Date.now() - this.lastDrawTime) / 1000
const xDis = this.xSpeed * duration
const yDis = this.ySpeed * duration
let x = this.x + xDis
let y = this.y + yDis
if (x > canvas.width - this.r / 2 || x < this.r / 2) {
this.xSpeed = -this.xSpeed
}
if (y > canvas.height - this.r / 2 || y < this.r / 2) {
this.ySpeed = -this.ySpeed
}
this.x = x
this.y = y
}
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 2 * Math.PI, 0)
ctx.fillStyle = `rgb(200,200,200)`
ctx.fill()
this.lastDrawTime = Date.now()
}
}
class Graph {
/**
* 点的集合
*/
points: Point[]
/**
* 连线最大距离
*/
maxDis: number = 150 * devicePixelRatio
constructor(pointNumber = 30) {
this.points = new Array(pointNumber).fill(0).map(() => new Point())
}
drawLine(p1, p2) {
// 计算2点间距离
const d = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
if (d > this.maxDis) return
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
ctx.lineTo(p2.x, p2.y)
// 计算连线清晰度
ctx.strokeStyle = `rgba(200,200,200,${1 - d / this.maxDis})`
ctx.stroke()
}
/**
* 绘制
*/
draw() {
requestAnimationFrame(() => {
this.draw()
})
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < this.points.length; i++) {
let p1 = this.points[i]
p1.draw()
// 进行连线
for (let j = i + 1; j < this.points.length; j++) {
const p2 = this.points[j]
this.drawLine(p1, p2)
}
}
}
}
let graph = new Graph(100)
graph.draw()
})
</script>
在 canvas 中进行用户交互
源码如下:
vue
<template>
<canvas ref="cvsRef" style="width: 700px; height: 800px"></canvas>
</template>
<script setup lang="ts">
import { ElementNode } from '@vue/compiler-core'
import { ref, onMounted } from 'vue'
const cvsRef = ref<any>()
const getRandom = (min, max) => {
return Math.floor(min + Math.random() * (max - min))
}
onMounted(() => {
const canvas = cvsRef.value as HTMLCanvasElement
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = 700 * devicePixelRatio
canvas.height = 800 * devicePixelRatio
const init = () => {
ctx.strokeStyle = '#000'
ctx.strokeRect(0, 0, canvas.width, canvas.height)
}
init()
class Rectangle {
color: string
startX: number
startY: number
endX: number
endY: number
constructor(color, startX, startY) {
this.color = color
this.startX = startX
this.startY = startY
this.endX = startX
this.endY = startY
}
get minX() {
return Math.min(this.startX, this.endX) * devicePixelRatio
}
get maxX() {
return Math.max(this.startX, this.endX) * devicePixelRatio
}
get minY() {
return Math.min(this.startY, this.endY) * devicePixelRatio
}
get maxY() {
return Math.max(this.startY, this.endY) * devicePixelRatio
}
draw() {
ctx.beginPath()
ctx.moveTo(this.minX, this.minY)
ctx.lineTo(this.maxX, this.minY)
ctx.lineTo(this.maxX, this.maxY)
ctx.lineTo(this.minX, this.maxY)
ctx.lineTo(this.minX, this.minY)
ctx.lineCap = 'square'
ctx.fillStyle = this.color
ctx.fill()
ctx.strokeStyle = '#fff'
ctx.lineWidth = 3
ctx.stroke()
}
isInside(x, y) {
x *= devicePixelRatio
y *= devicePixelRatio
return (
x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
)
}
}
const shapes: Rectangle[] = []
const draw = () => {
requestAnimationFrame(draw)
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (const shape of shapes) {
shape.draw()
}
}
const getShape = (x, y) => {
for (let i = shapes.length - 1; i >= 0; i--) {
const shape = shapes[i]
if (shape.isInside(x, y)) {
return shape
}
}
return null
}
canvas.onmousedown = e => {
const rect = canvas.getBoundingClientRect()
const clickX = e.clientX - rect.left
const clickY = e.clientY - rect.top
const existShape = getShape(clickX, clickY)
if (existShape) {
const { startX, startY, endX, endY } = existShape
// 拖动
window.onmousemove = e => {
const disX = e.clientX - rect.left - clickX
const disY = e.clientY - rect.top - clickY
existShape.startX = startX + disX
console.log('disX', disX, startX, existShape.startX)
existShape.endX = endX + disX
existShape.startY = startY + disY
existShape.endY = endY + disY
}
} else {
// 新建
const shape = new Rectangle('#f00', clickX, clickY)
shapes.push(shape)
window.onmousemove = e => {
shape.endX = e.clientX - rect.left
shape.endY = e.clientY - rect.top
draw()
}
}
window.onmouseup = e => {
window.onmousemove = null
window.onmouseup = null
}
}
})
</script>
<template>
<canvas ref="cvsRef" style="width: 700px; height: 800px"></canvas>
</template>
<script setup lang="ts">
import { ElementNode } from '@vue/compiler-core'
import { ref, onMounted } from 'vue'
const cvsRef = ref<any>()
const getRandom = (min, max) => {
return Math.floor(min + Math.random() * (max - min))
}
onMounted(() => {
const canvas = cvsRef.value as HTMLCanvasElement
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = 700 * devicePixelRatio
canvas.height = 800 * devicePixelRatio
const init = () => {
ctx.strokeStyle = '#000'
ctx.strokeRect(0, 0, canvas.width, canvas.height)
}
init()
class Rectangle {
color: string
startX: number
startY: number
endX: number
endY: number
constructor(color, startX, startY) {
this.color = color
this.startX = startX
this.startY = startY
this.endX = startX
this.endY = startY
}
get minX() {
return Math.min(this.startX, this.endX) * devicePixelRatio
}
get maxX() {
return Math.max(this.startX, this.endX) * devicePixelRatio
}
get minY() {
return Math.min(this.startY, this.endY) * devicePixelRatio
}
get maxY() {
return Math.max(this.startY, this.endY) * devicePixelRatio
}
draw() {
ctx.beginPath()
ctx.moveTo(this.minX, this.minY)
ctx.lineTo(this.maxX, this.minY)
ctx.lineTo(this.maxX, this.maxY)
ctx.lineTo(this.minX, this.maxY)
ctx.lineTo(this.minX, this.minY)
ctx.lineCap = 'square'
ctx.fillStyle = this.color
ctx.fill()
ctx.strokeStyle = '#fff'
ctx.lineWidth = 3
ctx.stroke()
}
isInside(x, y) {
x *= devicePixelRatio
y *= devicePixelRatio
return (
x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
)
}
}
const shapes: Rectangle[] = []
const draw = () => {
requestAnimationFrame(draw)
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (const shape of shapes) {
shape.draw()
}
}
const getShape = (x, y) => {
for (let i = shapes.length - 1; i >= 0; i--) {
const shape = shapes[i]
if (shape.isInside(x, y)) {
return shape
}
}
return null
}
canvas.onmousedown = e => {
const rect = canvas.getBoundingClientRect()
const clickX = e.clientX - rect.left
const clickY = e.clientY - rect.top
const existShape = getShape(clickX, clickY)
if (existShape) {
const { startX, startY, endX, endY } = existShape
// 拖动
window.onmousemove = e => {
const disX = e.clientX - rect.left - clickX
const disY = e.clientY - rect.top - clickY
existShape.startX = startX + disX
console.log('disX', disX, startX, existShape.startX)
existShape.endX = endX + disX
existShape.startY = startY + disY
existShape.endY = endY + disY
}
} else {
// 新建
const shape = new Rectangle('#f00', clickX, clickY)
shapes.push(shape)
window.onmousemove = e => {
shape.endX = e.clientX - rect.left
shape.endY = e.clientY - rect.top
draw()
}
}
window.onmouseup = e => {
window.onmousemove = null
window.onmouseup = null
}
}
})
</script>