重写fabric.Itext 解决 fabric 文字在 fill 为 transparent 情况下装饰线不显示问题

279 阅读4分钟

前言

因为目前有时间了,所以在整理一下自己这几年写过的一些东西的相关文档,准备把一些东西改一下发出来,有的内容可能并不复杂,甚至有点浅显,但是也是对自己这几年的一些复盘和总结了

原创文章,全文唯一
如果内容有帮助请不要吝啬您的点赞收藏哦
有转载需求的请跟我确认

问题背景

由于fabric 文字的装饰线是跟填充一起绘制的,所以导致我们在设置填充为透明的时候装饰线会消失 但是这实际上是违背了我们的期望

image.png

所以我们需要解决在fill情况下装饰线不显示的问题

针对这个问题我能想到的有三种方案

方法一:使用自定义的装饰线样式

Fabric.js允许通过修改文本的样式属性(styles)来实现更复杂的文本渲染效果,包括自定义的装饰线样式。不过,这需要自己绘制这些装饰线,而不是依赖Fabric.js内置的装饰线功能。 步骤如下:

  1. 创建文本对象:首先,创建一个fabric.IText对象来表示你的文本。
  2. 定义样式:在文本的styles属性中定义每个字符或字符组的样式。对于需要添加装饰线的部分,你可以通过添加额外的路径(fabric.Path对象)来模拟装饰线的效果。
  3. 添加到画布:将文本对象添加到Fabric.js的画布上。

这种方法比较复杂,需要对Fabric.js的绘图API有深入的了解。

方法二:使用叠加的文本对象

另一种方法是在文本下方或上方叠加一个额外的文本对象,该对象只包含装饰线部分,并使用不同的颜色。 步骤如下:

  1. 创建主文本对象:首先,创建一个fabric.IText对象来表示你的主文本,并设置其fill属性为想要的主文本颜色。
  2. 创建装饰线文本对象:创建一个额外的fabric.IText对象来表示装饰线。这个对象应该只包含与主文本相同位置的空格字符(或者下划线字符,如果需要下划线效果),并设置其fill属性为想要的装饰线颜色。
  3. 调整位置和层级:将装饰线文本对象放置在主文本对象的相同位置,并确保其在主文本对象的下方(如果需要中划线效果)或上方(如果需要下划线效果)。可以通过调整left、top属性以及canvas.sendToBack()或canvas.bringToFront()方法来实现这一点。
  4. 添加到画布:将两个文本对象都添加到Fabric.js的画布上。

方法三:使用SVG滤镜或混合模式

在某些情况下,可能还可以使用SVG的滤镜或混合模式来改变装饰线的颜色。 然而,这种方法需要更多的图形处理知识,并且可能不是所有浏览器都支持。

解决办法

经过调查,因为我们需要导出 svg 以及 拿到相关的path数据来进行一些业务上的处理
所以我们认为最靠谱的办法还是直接重写 fabric 的 itext 类
可以直接在上面拓展我们的处理

如何重写 fabric.Itext

我们可以直接新建一个 class 继承自 fabric.IText,然后在fabric上注册该方法

const CustomIText = fabric.util.createClass(fabric.IText, {
  type: 'i-text',
  initialize(text, options) {
  },
})

CustomIText.fromObject = (object, callback) => {
  return fabric.Object._fromObject('CustomIText', object, callback, 'text')
}

// @ts-ignore wait-doing
fabric.CustomIText = CustomIText

定义重写方法 _renderTextDecoration

对于这个方法,我们主要是要自己去绘制文字的中划线和下划线 所以在这个需求中主要的难点在于:

  1. 计算线条的起点和终点
  2. 考虑多文本情况下的问题,因为需要计算字间的间隔来计算画线起点和终点
  3. 考虑多行情况下的问题,需要计算行的高端以及行间距来计算出下一行的中划线和下划线的高度

计算行宽

  _getLineWidth(ctx, _, line) {
    ctx.save()
    this._setTextStyles(ctx)
    const width = ctx.measureText(line.join('')).width
    ctx.restore()
    return width
  },

计算每行的宽高

    const textLines = this._textLines
    const lineHeight = this.fontSize * this.lineHeight
    const topOffset = -this.height / 2
    textLines.forEach((line, i) => {
      const leftOffset = this._getLeftOffset() + this._getLineLeftOffset(i)
      const width = this._getLineWidth(ctx, i, line)
      const lineTop = topOffset + lineHeight * i + i * this._fontSizeMult
      const lineBottom = lineTop + lineHeight
    })

完整代码实现

 _renderTextDecoration(ctx, method, color) {
    if (!this[method]) {
      return
    }

    const textLines = this._textLines
    const lineHeight = this.fontSize * this.lineHeight
    const topOffset = -this.height / 2

    ctx.strokeStyle = color || this.fill
    ctx.lineWidth = this.fontSize / 15

    textLines.forEach((line, i) => {
      const leftOffset = this._getLeftOffset() + this._getLineLeftOffset(i)
      const width = this._getLineWidth(ctx, i, line)
      const lineTop = topOffset + lineHeight * i + i * this._fontSizeMult
      const lineBottom = lineTop + lineHeight

      ctx.beginPath()
      switch (method) {
        case 'underline':
          ctx.moveTo(leftOffset, lineBottom)
          ctx.lineTo(leftOffset + width, lineBottom)
          break
        case 'linethrough':
          ctx.moveTo(leftOffset, lineTop + lineHeight / 2)
          ctx.lineTo(leftOffset + width, lineTop + lineHeight / 2)
          break
      }
      ctx.stroke()
    })
  }

重写 render 方法

  _render: function (ctx) {
    this.callSuper('_render', ctx)

    if (this.underline) {
      this._renderTextDecoration(ctx, 'underline', this.underlineColor)
    }

    if (this.linethrough) {
      this._renderTextDecoration(ctx, 'linethrough', this.linethroughColor)
    }
  },

完整代码实现

import { fabric } from 'fabric'

const CustomIText = fabric.util.createClass(fabric.IText, {
  type: 'i-text',
  initialize(text, options) {
    this.callSuper('initialize', text, options)
    options = options || {}
    this.underlineColor = options.underlineColor || 'black'
    this.linethroughColor = options.linethroughColor || 'black'
    this.overlineColor = options.overlineColor || 'black'
  },
  _render: function (ctx) {
    this.callSuper('_render', ctx)

    if (this.underline) {
      this._renderTextDecoration(ctx, 'underline', this.underlineColor)
    }

    if (this.linethrough) {
      this._renderTextDecoration(ctx, 'linethrough', this.linethroughColor)
    }
  },

  _renderTextDecoration(ctx, method, color) {
    if (!this[method]) {
      return
    }

    const textLines = this._textLines
    const lineHeight = this.fontSize * this.lineHeight
    const topOffset = -this.height / 2

    ctx.strokeStyle = color || this.fill
    ctx.lineWidth = this.fontSize / 15

    textLines.forEach((line, i) => {
      const leftOffset = this._getLeftOffset() + this._getLineLeftOffset(i)
      const width = this._getLineWidth(ctx, i, line)
      const lineTop = topOffset + lineHeight * i + i * this._fontSizeMult
      const lineBottom = lineTop + lineHeight

      ctx.beginPath()
      switch (method) {
        case 'underline':
          ctx.moveTo(leftOffset, lineBottom)
          ctx.lineTo(leftOffset + width, lineBottom)
          break
        case 'linethrough':
          ctx.moveTo(leftOffset, lineTop + lineHeight / 2)
          ctx.lineTo(leftOffset + width, lineTop + lineHeight / 2)
          break
      }
      ctx.stroke()
    })
  },

  _getLineWidth(ctx, _, line) {
    ctx.save()
    this._setTextStyles(ctx)
    const width = ctx.measureText(line.join('')).width
    ctx.restore()
    return width
  }
})

CustomIText.fromObject = (object, callback) => {
  return fabric.Object._fromObject('CustomIText', object, callback, 'text')
}

// @ts-ignore wait-doing
fabric.CustomIText = CustomIText
export default CustomIText