基于canvas实现图片和图形的等比例放大和缩小,圆角矩形,hover图形显示名字

引言

最近遇到了一个需求,canvas画布渲染图片和图形,并且支持同比例缩放[拖拽下一篇支持],完成此需求后想好好总结一下。可能有太多,看到某些文章写了很多看不到最后效果是否满足自己需求的困扰,所以我把预览图先放在开始,以便观看~

看本文章前,可以先看看看这篇信息之前最好先去看一下canvasapicanvas 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}),不然无法阻止默认的滚动事件。

大家可以赋值offsetXoffsetY始终为0看下,画布就会以左上角(0,0)进行缩放,记录offsetXoffsetY就是让图像以当前鼠标的位置就行缩放.

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之外,

其他的属性是相对于视图窗口 的左上角 来计算的。根据clientXclientY和矩形的位置差值来计算出当前坐标的位置

🌰 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计算的宽度lineslength和行高和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名字
})

以上如有问题或疏漏,欢迎指正,谢谢。

相关推荐
m0_748230947 分钟前
Redis 通用命令
前端·redis·bootstrap
YaHuiLiang39 分钟前
一切的根本都是前端“娱乐圈化”
前端·javascript·代码规范
ObjectX前端实验室2 小时前
个人网站开发记录-引流公众号 & 谷歌分析 & 谷歌广告 & GTM
前端·程序员·开源
CL_IN2 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天3 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
qianmoQ4 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
尚学教辅学习资料4 小时前
基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考
vue.js·spring boot·uni-app·旅游
椰果uu4 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑4 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄4 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器