引言

最近遇到了一个需求,canvas画布渲染图片和图形,并且支持同比例缩放[拖拽下一篇支持],完成此需求后想好好总结一下。可能有太多,看到某些文章写了很多看不到最后效果是否满足自己需求的困扰,所以我把预览图先放在开始,以便观看~
看本文章前,可以先看看看这篇信息之前最好先去看一下canvas
的 api
,canvas API 传送门
开始编写
创建canvas画布
ini
<div class=container>
<canvas id="chart-wrap" />
</div>
首先绘制图片
这里写一个名叫 chart
的类,在构造器 constructor
里初始化画布,进行图片的渲染
js
class chart {
//初始化构造器
constructor (params) {
this.maxWidth = params.maxWidth
this.maxHeight = params.maxHeight
//创建画布(maxWidth,maxHeight 画布的长宽最大值)
this.El = params.el
this.ctx = this.El.getContext('2d')
this.El.width = params.maxWidth
this.El.height = params.maxHeight
//绘制图片的长宽(canvas如果和图片同宽高,可取一个)
this.width = params.width
this.height = params.height
this.offsetX = 0 // X轴偏移值
this.offsetY = 0 // Y轴偏移值
}
drawImage(data) {
this.image = new Image()
this.image.src = data.url
// 图像在Canvas中的位置 居中
this.offsetX = (this.maxWidth - this.width) / 2
this.offsetY = (this.maxHeight - this.height) / 2
// 绘制图像
this.ctx.drawImage(this.image, this.offsetX, this.offsetY, this.width, this.height)
}
}
// 构建图表对象
const chartObj = new chart({
el: document.getElementById('chart-wrap') ,
width: 300,
height: 300,
maxWidth: 1000,
maxHeight: 1000
});
this.chartObj.drawImage({ url: 'xxxxx' })
预览图(灰色部分为canvas画布)

绘制多种图形
js
class chart {
//初始化构造器
constructor(params) {
this.maxWidth = params.maxWidth
this.maxHeight = params.maxHeight
//创建画布(maxWidth,maxHeight 画布的长宽最大值)
this.El = params.el
this.ctx = this.El.getContext('2d')
this.El.width = params.maxWidth
this.El.height = params.maxHeight
//绘制图片的长宽(canvas如果和图片同宽高,可取一个)
this.width = params.width
this.height = params.height
this.offsetX = 0 // X轴偏移值
this.offsetY = 0 // Y轴偏移值
}
// __________ 绘制矩形 __________
drawRect(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.fill()
this.ctx.stroke()
}
// ____________ 绘制多边形________
drawPolygon(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.stroke()
this.ctx.fill()
}
// ___________ 添加一个判断类型绘制的方法 _____________
draw(item) {
switch (item.type) {
case 'rectangle':
this.drawRect(item)
break
case 'polygon':
this.drawPolygon(item)
break
}
}
// 添加形状
push(data) {
this.draw(data); // ____________ 修改调用绘制方法 ____________
}
// ___________ 坐标图像缩放 _____________
getTransForm(item = {}, originSizeWidth, originSizeHeight, imgSizeWidth, imgSizeHeight ) {
if(item.type === 'polygon') {
let polygon = []
for (let i = 0; i < item.length - 1; i = i + 2) {
// 图片本身宽高 / 绘制出来的宽高 等于坐标的比例转换
const cur = [item[i] / originSizeWidth * imgSizeWidth, item[i + 1] / originSizeHeight * imgSizeHeight]
polygon.push(cur)
}
return polygon
}
if(item.type === 'rectangle') {
const x1 = [item[0] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x2 = [item[2] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x3 = [item[2] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
const x4 = [item[0] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
return [x1, x2, x3, x4]
}
}
drawImage(data) {
this.image = new Image()
this.image.src = data.url
// 图像在Canvas中的位置 居中
this.offsetX = (this.maxWidth - this.width) / 2
this.offsetY = (this.maxHeight - this.height) / 2
// 绘制图像
this.ctx.drawImage(this.image, this.offsetX, this.offsetY, this.width, this.height)
// _________________ 绘制图形 ________________
data.shape.forEach(i) => {
//坐标缩放转换
const _data = getTransForm(i, data.width, data.height, this.width, this.height)
this.push({ ...i, width: data.width, height: data.height, points: _data })
}
}
}
// 构建图表对象
const chartObj = new chart({
el: document.getElementById('chart-wrap'),
width: 300,
height: 300,
maxWidth: 1000,
maxHeight: 1000
});
this.chartObj.drawImage({
url: 'xxxxx',
type: '',
width: 1240, //图片本身宽高
height: 1340, //图片本身宽高
//一个图片可能绘制多个图形
shape: [
{
type: 'polygon',
points: [632, 1922, 128, 1924, 14, 1961, 610, 1982, 1972, 632...]
}
...
]
name: '我是图片' //后面显示hover名字
})
对比前面这里添加了一个绘制矩形(drawRect
)、绘制多边形(drawPolygon
)的方法 和 数据,并且添加了判断渲染类型的函数(draw
)。
预览

增加缩放
canvas
缩放 提供了两个类型方法可以实现:
一个是在当前缩放基础上缩放: scale()
缩放当前绘图至更大或更小,transform()
替换绘图的当前转换矩阵
意思就是原本画布大小是 1,第一次放大 2倍,就变成2,第二次放大2倍就变成4
一个是在基础画布上缩放: setTransform()
将当前转换重置为单位矩阵。然后运行transform()
意思就是原本画布大小是 1,第一次放大 2倍,就变成2,第二次放大2倍还是2,因为重置回原来的1后再放大的
矩阵变化不只有缩放,但是可以其他参数不变只更改缩放值
这里我使用 setTransform()
缩放画布
第一步 :
保存好当前缩放值,就在constructor
加以下参数,以及在 push()
方法下保存数据、render()
重绘图像和图像数据
js
constructor (params) {
// 因为canvas是基于状态绘制的,也就是设置了缩放值
// 再绘制的元素才会根据缩放倍数绘制,因此需要把每个绘制的对象保存起来。
this.data = []
this.scale = 1 // 默认缩放值是 1
this.offsetX = 0 // 画布X轴偏移值
this.offsetY = 0 // 画布Y轴偏移值
}
// 添加形状
push(data) {
// push 方法中添加保存数据操作
this.data.push(data)
this.draw(data)
}
// 渲染整个图形画布
render() {
this.ctx.clearRect(0, 0, this.maxWidth, this.maxHeight)
this.El.width = this.maxWidth
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
//重绘图像
this.ctx.drawImage(
this.image,
this.offsetX, // x轴偏移
this.offsetY, // y轴偏移
scaledWidth,
scaledHeight
)
//重绘图形
this.data.forEach((item) => {
this.draw(item)
})
}
第二步:
缩放时鼠标滚轮控制,加上监听滚轮事件,在鼠标移入画布中时才添加,不在画布中就不需要监听滚轮事件
js
constructor() { // 添加滚轮判断事件
this.addScaleFunc();
}
// 判断时机注册移除MouseWhell事件
addScaleFunc () {
this.El.addEventListener('mouseenter', this.addMouseWhell)
this.El.addEventListener('mouseleave', this.removeMouseWhell)
}
// 添加 mousewhell 事件
addMouseWhell = () => {
document.addEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
// 移除 mousewhell 事件
removeMouseWhell = () => {
document.removeEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
第三步:
具体实现
js
class chart {
//初始化构造器
constructor (params) {
this.maxWidth = params.maxWidth
this.maxHeight = params.maxHeight
//创建画布(maxWidth,maxHeight 画布的长宽最大值)
this.El = params.el
this.ctx = this.El.getContext('2d')
this.El.width = params.maxWidth
this.El.height = params.maxHeight
//绘制图片的长宽(canvas如果和图片同宽高,可取一个)
this.width = params.width
this.height = params.height
this.offsetX = 0 // X轴偏移值
this.offsetY = 0 // Y轴偏移值
}
drawImage(data) {
this.image = new Image()
this.image.src = data.url
// 图像在Canvas中的位置 居中
this.offsetX = (this.maxWidth - this.width) / 2
this.offsetY = (this.maxHeight - this.height) / 2
// 绘制图像
this.ctx.drawImage(this.image, this.offsetX, this.offsetY, this.width, this.height)
}
}
// 构建图表对象
const chartObj = new chart({
el: document.getElementById('chart-wrap') ,
width: 300,
height: 300,
maxWidth: 1000,
maxHeight: 1000
});
this.chartObj.drawImage({ url: 'xxxxx' })
整体代码:
js
class chart {
//初始化构造器
constructor(params) {
this.maxWidth = params.maxWidth
this.maxHeight = params.maxHeight
//创建画布(maxWidth,maxHeight 画布的长宽最大值)
this.El = params.el
this.ctx = this.El.getContext('2d')
this.El.width = params.maxWidth
this.El.height = params.maxHeight
//绘制图片的长宽(canvas如果和图片同宽高,可取一个)
this.width = params.width
this.height = params.height
// -------缩放需要的变量------
this.maxScale = 10 // 最大缩放值
this.minScale = 0.2 // 最小缩放值
this.step = 0.07 // 缩放率
this.offsetX = 0 // 画布X轴偏移值
this.offsetY = 0 // 画布Y轴偏移值
this.addScaleFunc()
}
// -----判断时机注册移除MouseWhell事件-------
addScaleFunc () {
this.El.addEventListener('mouseenter', this.addMouseWhell)
this.El.addEventListener('mouseleave', this.removeMouseWhell)
}
// -----MouseWhell事件-------
addMouseWhell = () => {
document.addEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
// -----移除MouseWhell事件-------
removeMouseWhell = () => {
document.removeEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
drawRect(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.fill()
this.ctx.stroke()
}
drawPolygon(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.stroke()
this.ctx.fill()
}
// -------渲染整个图形画布-------
render() {
this.ctx.clearRect(0, 0, this.maxWidth, this.maxHeight)
this.El.width = this.maxWidth
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
//重绘图像
this.ctx.drawImage(
this.image,
this.offsetX, // x轴偏移
this.offsetY, // y轴偏移
scaledWidth,
scaledHeight
)
//重绘图形
this.data.forEach((item) => {
this.draw(item)
})
}
scrollFunc = (e) => {
e.preventDefault()
if (e.wheelDelta) {
const x = e.offsetX - this.offsetX
const y = e.offsetY - this.offsetY
const offsetX = (x / this.scale) * this.step
const offsetY = (y / this.scale) * this.step
if (e.wheelDelta > 0) {
this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY
this.scale += this.step
} else {
this.offsetX += this.scale <= this.minScale ? 0 : offsetX
this.offsetY += this.scale <= this.minScale ? 0 : offsetY
this.scale -= this.step
}
this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))
this.render()
}
};
draw(item) {
this.ctx.setTransform( // ---------添加绘制图形缩放方法-----
this.scale,
0,
0,
this.scale,
this.offsetX,
this.offsetY
)
switch (item.type) {
case 'rectangle':
this.drawRect(item)
break
case 'polygon':
this.drawPolygon(item)
break
}
}
// 添加形状
push(data) {
this.data.push(data) // -------添加保存数据操作-----
this.draw(data);
}
getTransForm(item = {}, originSizeWidth, originSizeHeight, imgSizeWidth, imgSizeHeight ) {
if(item.type === 'polygon') {
let polygon = []
for (let i = 0; i < item.length - 1; i = i + 2) {
// 图片本身宽高 / 绘制出来的宽高 等于坐标的比例转换
const cur = [item[i] / originSizeWidth * imgSizeWidth, item[i + 1] / originSizeHeight * imgSizeHeight]
polygon.push(cur)
}
return polygon
}
if(item.type === 'rectangle') {
const x1 = [item[0] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x2 = [item[2] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x3 = [item[2] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
const x4 = [item[0] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
return [x1, x2, x3, x4]
}
}
drawImage(data) {
this.image = new Image()
this.image.src = data.url
// 图像在Canvas中的位置 居中
this.offsetX = (this.maxWidth - this.width) / 2
this.offsetY = (this.maxHeight - this.height) / 2
// 绘制图像
this.ctx.drawImage(this.image, this.offsetX, this.offsetY, this.width, this.height)
// _________________ 绘制图形 ________________
data.shape.forEach(i) => {
//坐标缩放转换
const _data = getTransForm(i, data.width, data.height, this.width, this.height)
this.push({ ...i, width: data.width, height: data.height, points: _data })
}
}
}
// 构建图表对象
const chartObj = new chart({
el: document.getElementById('chart-wrap'),
width: 300,
height: 300,
maxWidth: 1000,
maxHeight: 1000
});
this.chartObj.drawImage({
url: 'xxxxx',
type: '',
width: 1240, //图片本身宽高
height: 1340, //图片本身宽高
//一个图片可能绘制多个图形
shape: [
{
type: 'polygon',
points: [632, 1922, 128, 1924, 14, 1961, 610, 1982, 1972, 632...]
}
...
]
name: '我是图片' //后面显示hover名字
})
第一步和第二步比较好理解,看下第三步
js
scrollFunc = (e) => { /
// 阻止默认事件 (缩放时外部容器禁止滚动)
e.preventDefault();
if(e.wheelDelta){
e.wheelDelta > 0 ? this.scale += this.step : this.scale -= this.step
this.render()
}
}
只需要上述几行就实现了缩放。判断 e.wheelDelta
是向上滚动还是向下,从而增加或减少 this.scale
的大小,最后调用 render()
重新绘制当前画布。
e.preventDefault()
就不多解释了,大家都知道是解决默认行为的。但是有一点要解释一下 在调用 scrollFunc()
这个函数的事件监听器的第三个参数 {passive: false}
是必须加的(默认就是 {passive: true}
),不然无法阻止默认的滚动事件。
大家可以赋值offsetX
和offsetY
始终为0看下,画布就会以左上角(0,0)进行缩放,记录offsetX
和offsetY
就是让图像以当前鼠标的位置就行缩放.
js
scrollFunc = (e) => {
e.preventDefault()
if (e.wheelDelta) {
const x = e.offsetX - this.offsetX
const y = e.offsetY - this.offsetY
const offsetX = (x / this.scale) * this.step
const offsetY = (y / this.scale) * this.step
if (e.wheelDelta > 0) {
this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY
this.scale += this.step
} else {
this.offsetX += this.scale <= this.minScale ? 0 : offsetX
this.offsetY += this.scale <= this.minScale ? 0 : offsetY
this.scale -= this.step
}
this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))
this.render()
}
};
x,y
是鼠标距离画布原始原点的距离,offsetX,offsetY
是本次缩放的偏移量,然后判断放大或者缩小从而增减整体画布的偏移量。 本次偏移量计算方式:鼠标距原始点距离(x,y)
除以 缩放值 this.scale
再乘以 缩放率 this.step
。
解释:因为是使用setTransform()
,所以每次放大或者缩小都是在原始画布大小的基础上缩放,所以需要除以缩放值,找到在原始缩放基础上鼠标距离原始点的距离。
解释:如果使用scale()
,就不需要除以缩放值,直接当前缩放值乘以缩放率就能等于现在实际缩放值
绘制图形的名字
第一步:
鼠标hover时,加上mousemove监听事件,在鼠标移入画布中时才添加,不在画布中就不需要监听事件
js
// 判断时机注册移除MouseWhell事件
addScaleFunc () {
this.El.addEventListener('mouseenter', this.addMouseWhell)
this.El.addEventListener('mouseleave', this.removeMouseWhell)
this.El.addEventListener('mouseenter', this.move) //鼠标移入
this.El.addEventListener('mouseleave', this.removeMove) //鼠标移除
}
move = () => {
this.El.addEventListener('mousemove', this.handleMousemove)
}
removeMove =() => {
this.El.removeEventListener('mousemove', this.handleMousemove)
}
// 重置画布
clearCanvas () {
this.ctx.clearRect(0, 0, this.maxWidth, this.maxHeight)
}
handleMousemove = (event) => {
// 获取当前鼠标位置
const mouseX = event.clientX - this.El.getBoundingClientRect().left
const mouseY = event.clientY - this.El.getBoundingClientRect().top
this.clearCanvas()
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
this.El.width = this.maxWidth
// 重新绘制图像
this.ctx.drawImage(
this.image,
this.offsetX,
this.offsetY,
scaledWidth,
scaledHeight
)
// 循环所有图形
for (const polygon of this.data) {
this.draw(polygon)
//判断当前鼠标是否在绘制图形内
if (this.ctx.isPointInPath(mouseX, mouseY)) {
if (!polygon.name) return
// 显示名字逻辑
}
}
}

getBoundingClientRect()
返回的是矩形的集合。
表示了当前盒子在浏览器中的位置以及自身占据的空间的大小,除了width和height之外,
其他的属性是相对于视图窗口 的左上角 来计算的。根据clientX
和clientY
和矩形的位置差值来计算出当前坐标的位置
🌰 demo地址
第二步:
绘制文字可以直接用fillText方式绘制,图上的效果我加了个圆角矩形作为文字的背景,觉得这种名字更立体突出一些
先来绘制圆角矩形,绘制的过程也要考虑画布的放大和缩小,矩形也会随之放大缩小
js
handleMousemove = (event) => {
const mouseX = event.clientX - this.El.getBoundingClientRect().left
const mouseY = event.clientY - this.El.getBoundingClientRect().top
this.clearCanvas()
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
this.El.width = this.maxWidth
this.ctx.drawImage(
this.image,
this.offsetX,
this.offsetY,
scaledWidth,
scaledHeight
)
for (const polygon of this.data) {
this.draw(polygon)
if (this.ctx.isPointInPath(mouseX, mouseY)) {
if (!polygon.name) return
const text = polygon.name // 图形名字
const padding = 10
const maxWidth = 100 //矩形最大宽度
const lineHeight = 20
const borderRadius = 4
const backgroundColor = '#fff'
const font = '14px Arial'
this.drawRoundedRect(text, mouseX + 10, mouseY, maxWidth, lineHeight, borderRadius, backgroundColor, padding)
}
}
drawRoundedRect = (text, x, y, maxWidth, lineHeight, radius, color, padding) => {
this.ctx.font = '14px Arial'
const words = text.split('\n') //文字遇见 \n换行显示
let line = ''
let testWidth = 0
let offsetY = y + 12 / 2
this.lines = []
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ''
// 获取文字的宽度
const metrics = this.ctx.measureText(testLine)
testWidth = metrics.width
// 循环当前名字,超出最大宽度换行显示,遇见\n换行显示
if (testWidth > maxWidth && i > 0) {
this.ctx.fillText(line, x, offsetY)
line = words[i] + ''
offsetY += lineHeight
} else {
line = testLine
}
this.lines.push(line) //文字保存
}
//获取缩放后鼠标的值
const scaleX = (x - this.offsetX) / this.scale
const scaleY = (y - this.offsetY) / this.scale
//获取绘制圆角矩形的高度 + padding,判断是否换行情况
const textHeight = this.lines.length === 1
? this.lines.length * padding : this.lines.length * (lineHeight - 2)
const rectHeight = textHeight + 2 * padding
//获取绘制圆角矩形的宽度 + padding,判断是否换行情况
const width = testWidth > maxWidth ? maxWidth : testWidth
const rectWidth = width + 2 * padding
this.ctx.fillStyle = color
this.ctx.beginPath()
this.ctx.moveTo(scaleX + radius, scaleY)
this.ctx.arcTo(scaleX + rectWidth, scaleY, scaleX + rectWidth, scaleY + rectHeight, radius)
this.ctx.arcTo(scaleX + rectWidth, scaleY + rectHeight, scaleX, scaleY + rectHeight, radius)
this.ctx.arcTo(scaleX, scaleY + rectHeight, scaleX, scaleY, radius)
this.ctx.arcTo(scaleX, scaleY, scaleX + rectWidth, scaleY, radius)
this.ctx.closePath()
this.ctx.fill()
}
看这段可以先看下圆角矩形的绘制方式,设置矩形的宽高,和显示的位置随着当前鼠标位置。 文字可能是一行也可能是多行,计算高度要根据measureText
计算的宽度lines
得length
和行高和padding不要忘记,这样我们就绘制出来一个有10padding
得圆角矩形。
下面我们来绘制文字就大功告成了!
js
if (this.ctx.isPointInPath(mouseX, mouseY)) {
if (!polygon.name) return
const text = polygon.name
const padding = 10
const maxWidth = 100
const lineHeight = 20
const borderRadius = 4
const backgroundColor = '#fff'
const font = '14px Arial'
this.drawRoundedRect(text, mouseX + 10, mouseY, maxWidth, lineHeight, borderRadius, backgroundColor, padding)
// 绘制文字
this.drawTextWithLineBreak(mouseX + 10, mouseY + 12 / 2, lineHeight, font, padding)
}
drawTextWithLineBreak = (x, y, lineHeight, font, padding) => {
// 白色矩形框黑色的字
this.ctx.font = font
this.ctx.fillStyle = 'black'
this.ctx.textBaseline = 'middle'
//渲染文字
for (let i = 0; i < this.lines.length; i++) {
this.ctx.fillText(
this.lines[i],
(x - this.offsetX) / this.scale + padding,
((y - this.offsetY) / this.scale + i * lineHeight + padding)
)
}
}
fillText(text, x, y [, maxWidth])
x和y得计算也要考虑到缩放值,减去偏移值和✖️缩放比例得到缩放后的鼠标位置,y的计算要考虑到换行显示,要加上lineHeight的值
预览

最后
到这里这个需求就完成了,欢迎讨论和指正

完整代码:
js
class chart {
//初始化构造器
constructor(params) {
this.maxWidth = params.maxWidth
this.maxHeight = params.maxHeight
//创建画布(maxWidth,maxHeight 画布的长宽最大值)
this.El = params.el
this.ctx = this.El.getContext('2d')
this.El.width = params.maxWidth
this.El.height = params.maxHeight
//绘制图片的长宽(canvas如果和图片同宽高,可取一个)
this.width = params.width
this.height = params.height
// -------缩放需要的变量------
this.maxScale = 10 // 最大缩放值
this.minScale = 0.2 // 最小缩放值
this.step = 0.07 // 缩放率
this.offsetX = 0 // 画布X轴偏移值
this.offsetY = 0 // 画布Y轴偏移值
this.addScaleFunc()
}
// 判断时机注册移除MouseWhell事件
addScaleFunc () {
this.El.addEventListener('mouseenter', this.addMouseWhell)
this.El.addEventListener('mouseleave', this.removeMouseWhell)
this.El.addEventListener('mouseenter', this.move)
this.El.addEventListener('mouseleave', this.removeMove)
}
// MouseWhell事件
addMouseWhell = () => {
document.addEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
// 移除MouseWhell事件
removeMouseWhell = () => {
document.removeEventListener('mousewheel', this.scrollFunc, {
passive: false
})
};
// 添加mousemove显示名字
move = () => {
this.El.addEventListener('mousemove', this.handleMousemove)
}
removeMove =() => {
this.El.removeEventListener('mousemove', this.handleMousemove)
}
// 绘制矩形
drawRect(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.fill()
this.ctx.stroke()
}
// 绘制多边形
drawPolygon(canvasSystem) {
const { points } = canvasSystem
this.ctx.beginPath()
this.ctx.moveTo(points[0][0], points[0][1])
for (let i = 1; i <= points.length - 1; i++) {
this.ctx.lineTo(points[i][0], points[i][1])
}
this.ctx.closePath()
this.ctx.strokeStyle = canvasSystem.color || 'red'
this.ctx.fillStyle = hexToRgba(canvasSystem.color, 0.3)
this.ctx.stroke()
this.ctx.fill()
}
// 渲染整个图形画布
render() {
this.ctx.clearRect(0, 0, this.maxWidth, this.maxHeight)
this.El.width = this.maxWidth
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
//重绘图像
this.ctx.drawImage(
this.image,
this.offsetX, // x轴偏移
this.offsetY, // y轴偏移
scaledWidth,
scaledHeight
)
//重绘图形
this.data.forEach((item) => {
this.draw(item)
})
}
scrollFunc = (e) => {
e.preventDefault()
if (e.wheelDelta) {
const x = e.offsetX - this.offsetX
const y = e.offsetY - this.offsetY
const offsetX = (x / this.scale) * this.step
const offsetY = (y / this.scale) * this.step
if (e.wheelDelta > 0) {
this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY
this.scale += this.step
} else {
this.offsetX += this.scale <= this.minScale ? 0 : offsetX
this.offsetY += this.scale <= this.minScale ? 0 : offsetY
this.scale -= this.step
}
this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))
this.render()
}
};
draw(item) {
this.ctx.setTransform(
this.scale,
0,
0,
this.scale,
this.offsetX,
this.offsetY
)
switch (item.type) {
case 'rectangle':
this.drawRect(item)
break
case 'polygon':
this.drawPolygon(item)
break
}
}
// 添加形状
push(data) {
this.data.push(data)
this.draw(data);
}
// 坐标等比例转换
getTransForm(item = {}, originSizeWidth, originSizeHeight, imgSizeWidth, imgSizeHeight ) {
if(item.type === 'polygon') {
let polygon = []
for (let i = 0; i < item.length - 1; i = i + 2) {
// 图片本身宽高 / 绘制出来的宽高 等于坐标的比例转换
const cur = [item[i] / originSizeWidth * imgSizeWidth, item[i + 1] / originSizeHeight * imgSizeHeight]
polygon.push(cur)
}
return polygon
}
if(item.type === 'rectangle') {
const x1 = [item[0] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x2 = [item[2] / originSizeWidth * imgSizeWidth, item[1] / originSizeHeight * imgSizeHeight]
const x3 = [item[2] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
const x4 = [item[0] / originSizeWidth * imgSizeWidth, item[3] / originSizeHeight * imgSizeHeight]
return [x1, x2, x3, x4]
}
}
// 绘制图像
drawImage(data) {
this.image = new Image()
this.image.src = data.url
// 图像在Canvas中的位置 居中
this.offsetX = (this.maxWidth - this.width) / 2
this.offsetY = (this.maxHeight - this.height) / 2
// 绘制图像
this.ctx.drawImage(this.image, this.offsetX, this.offsetY, this.width, this.height)
// _________________ 绘制图形 ________________
data.shape.forEach(i) => {
//坐标缩放转换
const _data = getTransForm(i, data.width, data.height, this.width, this.height)
this.push({ ...i, width: data.width, height: data.height, points: _data })
}
}
// 获取鼠标位置
handleMousemove = (event) => {
const mouseX = event.clientX - this.El.getBoundingClientRect().left
const mouseY = event.clientY - this.El.getBoundingClientRect().top
this.clearCanvas()
const scaledWidth = this.width * this.scale
const scaledHeight = this.height * this.scale
this.El.width = this.maxWidth
this.ctx.drawImage(
this.image,
this.offsetX,
this.offsetY,
scaledWidth,
scaledHeight
)
for (const polygon of this.data) {
this.draw(polygon)
if (this.ctx.isPointInPath(mouseX, mouseY)) {
if (!polygon.name) return
const text = polygon.name
const padding = 10
const maxWidth = 100
const lineHeight = 20
const borderRadius = 4
const backgroundColor = '#fff'
const font = '14px Arial'
this.drawRoundedRect(text, mouseX + 10, mouseY, maxWidth, lineHeight, borderRadius, backgroundColor, padding)
this.drawTextWithLineBreak(mouseX + 10, mouseY + 12 / 2, lineHeight, font, padding)
}
}
}
// 绘制圆角矩形背景图
drawRoundedRect = (text, x, y, maxWidth, lineHeight, radius, color, padding) => {
this.ctx.font = '14px Arial'
const words = text.split('\n')
let line = ''
let testWidth = 0
let offsetY = y + 12 / 2
this.lines = []
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ''
const metrics = this.ctx.measureText(testLine)
testWidth = metrics.width
if (testWidth > maxWidth && i > 0) {
this.ctx.fillText(line, x, offsetY)
line = words[i] + ''
offsetY += lineHeight
} else {
line = testLine
}
this.lines.push(line)
}
const scaleX = (x - this.offsetX) / this.scale
const scaleY = (y - this.offsetY) / this.scale
const textHeight = this.lines.length === 1
? this.lines.length * padding : this.lines.length * (lineHeight - 2)
const rectHeight = textHeight + 2 * padding
const width = testWidth > maxWidth ? maxWidth : testWidth
const rectWidth = width + 2 * padding
this.ctx.fillStyle = color
this.ctx.beginPath()
this.ctx.moveTo(scaleX + radius, scaleY)
this.ctx.arcTo(scaleX + rectWidth, scaleY, scaleX + rectWidth, scaleY + rectHeight, radius)
this.ctx.arcTo(scaleX + rectWidth, scaleY + rectHeight, scaleX, scaleY + rectHeight, radius)
this.ctx.arcTo(scaleX, scaleY + rectHeight, scaleX, scaleY, radius)
this.ctx.arcTo(scaleX, scaleY, scaleX + rectWidth, scaleY, radius)
this.ctx.closePath()
this.ctx.fill()
}
// 绘制文字
drawTextWithLineBreak = (x, y, lineHeight, font, padding) => {
this.ctx.font = font
this.ctx.fillStyle = 'black'
this.ctx.textBaseline = 'middle'
for (let i = 0; i < this.lines.length; i++) {
this.ctx.fillText(
this.lines[i],
(x - this.offsetX) / this.scale + padding,
((y - this.offsetY) / this.scale + i * lineHeight + padding)
)
}
}
// 构建图表对象
const chartObj = new chart({
el: document.getElementById('chart-wrap'),
width: 300,
height: 300,
maxWidth: 1000,
maxHeight: 1000
});
this.chartObj.drawImage({
url: 'xxxxx',
type: '',
width: 1240, //图片本身宽高
height: 1340, //图片本身宽高
//一个图片可能绘制多个图形
shape: [
{
type: 'polygon',
points: [632, 1922, 128, 1924, 14, 1961, 610, 1982, 1972, 632...]
}
...
]
name: '我是图片' //后面显示hover名字
})
以上如有问题或疏漏,欢迎指正,谢谢。