基于fabric自定义svg光标

1,676 阅读4分钟

如何自定义光标?

fabric的光标实际上是和浏览器默认一致的,也就是说,浏览器有啥就有啥,没有啥就没有啥
但是我们很多时候默认的浏览器光标并不能满足需求
例如,我们做个魔术橡皮的功能,光标要求是个棒棒

image.png

这种情况我们就只能想办法自定义光标了
但是搜了一遍都找不到有相关的办法,所以只能自己实现了

如何绘制自定义的光标

因为我们是在fabric上去进行自定义光标,我们这里有两个绘制自定义光标的方案:

  1. 在canvas上进行绘制,如果这样的话我们需要先导入svg,然后将其作为一个单独图层进行处理,在鼠标事件进行变化时进行计算
  2. 在dom上进行绘制,好处在于不需要将处理耦合进画布中,并且经过实测性能表现会更好一些

如果在dom上绘制光标

定义dom

如果我们需要绘制光标,那么我们需要注意以下几点:

  1. 肯定需要先定义一个div作为光标挂载的位置
  2. 并且这个dom的定位必须为绝对定位以方便计算
  3. 光标的原本的pointer事件必须禁止
  4. 初始的光标显示默认定义为隐藏
  5. 并且因为是光标,我们必须要注意到这个dom的层级,需要设置为最高

所以具体的实现处理如下:

    const cursorDiv = document.createElement('div')
    cursorDiv.style.position = 'absolute'
    cursorDiv.style.pointerEvents = 'none'
    cursorDiv.style.display = 'none'
    cursorDiv.style.zIndex = '9999'

将自定义的 SVG 加载到 div 元素中

这里需要将我们需要的自定义样式加载到div里面,我这里是为了方便直接将svg写成一个字符串

    cursorDiv.innerHTML = `
    <svg 
        t="1718954739568" 
        class="icon" 
        viewBox="0 0 1024 1024" 
        version="1.1" 
        xmlns="http://www.w3.org/2000/svg" 
        p-id="4872" 
        width="24" 
        height="24"
    >
        <path 
            d="M648.490667 424.277333a110.933333 110.933333 0 0 1-33.706667-66.133333l-17.194667-131.541333-116.522666 63.402666a110.933333 110.933333 0 0 1-73.344 11.605334L277.333333 277.333333l24.32 130.389334a110.933333 110.933333 0 0 1-11.648 73.386666l-63.402666 116.48 131.541333 17.194667a110.933333 110.933333 0 0 1 66.133333 33.706667l91.221334 96.298666 57.002666-119.765333a110.933333 110.933333 0 0 1 52.48-52.522667l119.808-57.002666-96.298666-91.221334z m1.066666 237.397334l-94.421333 198.4a25.6 25.6 0 0 1-41.685333 6.613333l-151.125334-159.530667a25.6 25.6 0 0 0-15.274666-7.765333l-217.856-28.501333a25.6 25.6 0 0 1-19.2-37.589334l105.045333-193.024a25.6 25.6 0 0 0 2.688-16.896L177.493333 207.36a25.6 25.6 0 0 1 29.866667-29.866667l215.978667 40.234667a25.6 25.6 0 0 0 16.938666-2.688l192.981334-104.96a25.6 25.6 0 0 1 37.632 19.114667l28.501333 217.898666a25.6 25.6 0 0 0 7.765333 15.232l159.530667 151.125334a25.6 25.6 0 0 1-6.613333 41.685333l-198.4 94.421333a25.6 25.6 0 0 0-12.117334 12.117334z m34.005334 82.218666l60.330666-60.330666 180.992 180.992-60.330666 60.373333-180.992-181.034667z" 
            p-id="4873"
        >
        </path>
    </svg>`

然后我们需要将div的元素样式设置为光标的样式

 cursorDiv.style.cursor =
      'url("data:image/svg+xml;base64,' + btoa(cursorDiv.innerHTML) + '"), auto'
 cursorDiv.id = 'cursorDiv'

将这个div添加到body中去

 document.body.appendChild(cursorDiv)

如何限制自定义光标只在某个区域内生效?

因为我们这里对光标的要求只需要在图片中的移动为棒棒,在图片之外还是正常的光标 所以我们就必须在进行光标的绘制和处理的时候进行判断当前的光标移动是否在图片内

  judgeCursorIsInImg(pointer) {
    // 获取Fabric.js画布上的所有对象
    const objects = this.owlCanvas.getObjects()
    objects.forEach((obj) => {
      if (obj.type === 'image') {
        // @ts-ignore
        const isHovering = obj.containsPoint({ x: pointer.x, y: pointer.y })
        if (isHovering) {
          this.customCursor.style.display = 'block'
          this.canvas.defaultCursor = 'none'
          this.canvas.upperCanvasEl.style.cursor = 'none'
        } else {
          this.customCursor.style.display = 'none'
          this.canvas.defaultCursor = 'default'
          this.canvas.upperCanvasEl.style.cursor = 'default'
        }
      } else {
        this.customCursor.style.display = 'none'
      }
    })
  }

其中的

this.canvas.defaultCursor = 'none'
this.canvas.upperCanvasEl.style.cursor = 'none'

是为了去除fabric原本默认的鼠标样式

如何计算当前光标在canvas画布上的真实位置

众所周知,在 fabric 的 canvas 画布上鼠标移动事件获得的坐标和我们实际在视窗中绘制的坐标是不一样的,所以我们需要对当前canvas画布在视窗中所在位置的偏移量进行一个计算才能让我们的自定义坐标刚好在原本的坐标位置上

this.canvasOffsetX = canvasRect.left + window.scrollX - 12
this.canvasOffsetY = canvasRect.top + window.scrollY - 12

而在鼠标事件触发需要更新坐标位置时我们需要计算的方式也是一样

  updateCustomCursor(x, y) {
    if (this.customCursor) {
      this.customCursor.style.left = x + this.canvasOffsetX + 'px'
      this.customCursor.style.top = y + this.canvasOffsetY + 'px'
    }
  }

移除移入及销毁时光标的处理

在鼠标移入移出我们需要自定义光标区域的时候我们需要对当前的光标进行一个判断和处理

onMouseOver(event: fabric.IEvent) {
    const pointer = this.canvas.getPointer(event.e)
    this.judgeCursorIsInImg(pointer)
  }

  onMouseOut(event: fabric.IEvent) {
    const pointer = this.canvas.getPointer(event.e)
    this.judgeCursorIsInImg(pointer)
  }

记得在销毁这个示例的时候要移除掉这个dom

destroyed() {
  document.getElementById('cursorDiv')?.remove()
  this.owlCanvas.defaultCursor = 'default'
  this.owlCanvas.upperCanvasEl.style.cursor = 'default'
}

整体实现

下面是这个类的完整实现


class CustomCursor {
  private customCursor
  private canvasOffsetX: number = 0
  private canvasOffsetY: number = 0

  override initialize() {
    // 获取 Canvas 元素的相对于 document 的偏移量
    const canvasRect = this.owlCanvas.getElement().getBoundingClientRect()
    this.canvasOffsetX = canvasRect.left + window.scrollX - 12
    this.canvasOffsetY = canvasRect.top + window.scrollY - 12
    this.createCustomCursor()
  }

  createCustomCursor() {
    const cursorDiv = document.createElement('div')
    cursorDiv.style.position = 'absolute'
    cursorDiv.style.pointerEvents = 'none'
    cursorDiv.style.display = 'none'
    cursorDiv.style.zIndex = '9999'
    // 将自定义的 SVG 加载到 div 元素中
    cursorDiv.innerHTML = `
    <svg 
        t="1718954739568" 
        class="icon" 
        viewBox="0 0 1024 1024" 
        version="1.1" 
        xmlns="http://www.w3.org/2000/svg" 
        p-id="4872" 
        width="24" 
        height="24"
    >
        <path 
            d="M648.490667 424.277333a110.933333 110.933333 0 0 1-33.706667-66.133333l-17.194667-131.541333-116.522666 63.402666a110.933333 110.933333 0 0 1-73.344 11.605334L277.333333 277.333333l24.32 130.389334a110.933333 110.933333 0 0 1-11.648 73.386666l-63.402666 116.48 131.541333 17.194667a110.933333 110.933333 0 0 1 66.133333 33.706667l91.221334 96.298666 57.002666-119.765333a110.933333 110.933333 0 0 1 52.48-52.522667l119.808-57.002666-96.298666-91.221334z m1.066666 237.397334l-94.421333 198.4a25.6 25.6 0 0 1-41.685333 6.613333l-151.125334-159.530667a25.6 25.6 0 0 0-15.274666-7.765333l-217.856-28.501333a25.6 25.6 0 0 1-19.2-37.589334l105.045333-193.024a25.6 25.6 0 0 0 2.688-16.896L177.493333 207.36a25.6 25.6 0 0 1 29.866667-29.866667l215.978667 40.234667a25.6 25.6 0 0 0 16.938666-2.688l192.981334-104.96a25.6 25.6 0 0 1 37.632 19.114667l28.501333 217.898666a25.6 25.6 0 0 0 7.765333 15.232l159.530667 151.125334a25.6 25.6 0 0 1-6.613333 41.685333l-198.4 94.421333a25.6 25.6 0 0 0-12.117334 12.117334z m34.005334 82.218666l60.330666-60.330666 180.992 180.992-60.330666 60.373333-180.992-181.034667z" 
            p-id="4873"
        >
        </path>
    </svg>`

    // 设置 div 元素的样式为光标样式
    cursorDiv.style.cursor =
      'url("data:image/svg+xml;base64,' + btoa(cursorDiv.innerHTML) + '"), auto'
    cursorDiv.id = 'cursorDiv'

    document.body.appendChild(cursorDiv)
    this.customCursor = document.getElementById('cursorDiv')
  }

  judgeCursorIsInImg(pointer) {
    // 获取Fabric.js画布上的所有对象
    const objects = this.owlCanvas.getObjects()
    objects.forEach((obj) => {
      if (obj.type === 'image') {
        // @ts-ignore
        const isHovering = obj.containsPoint({ x: pointer.x, y: pointer.y })
        if (isHovering) {
          this.customCursor.style.display = 'block'
          this.canvas.defaultCursor = 'none'
          this.canvas.upperCanvasEl.style.cursor = 'none'
        } else {
          this.customCursor.style.display = 'none'
          this.canvas.defaultCursor = 'default'
          this.canvas.upperCanvasEl.style.cursor = 'default'
        }
      } else {
        this.customCursor.style.display = 'none'
      }
    })
  }

  override onMousemove(event: fabric.IEvent) {
    if (!this.customCursor) return
    const pointer = this.owlCanvas.getPointer(event.e)
    this.judgeCursorIsInImg(pointer)

    this.updateCustomCursor(pointer.x, pointer.y)
  }
  override onMouseup() {
    // this.customCursor && (this.customCursor.style.display = 'none')
  }
  onMouseOver(event: fabric.IEvent) {
    const pointer = this.canvas.getPointer(event.e)
    this.judgeCursorIsInImg(pointer)
  }

  onMouseOut(event: fabric.IEvent) {
    const pointer = this.canvas.getPointer(event.e)
    this.judgeCursorIsInImg(pointer)
  }

  updateCustomCursor(x, y) {
    if (this.customCursor) {
      this.customCursor.style.left = x + this.canvasOffsetX + 'px'
      this.customCursor.style.top = y + this.canvasOffsetY + 'px'
    }
  }

  override onMousedown(event: fabric.IEvent) {
  }

  destroyed() {
    document.getElementById('cursorDiv')?.remove()
    this.owlCanvas.defaultCursor = 'default'
    this.owlCanvas.upperCanvasEl.style.cursor = 'default'
  }
}
export default CustomCursor

实现效果

自定义光标.gif