使用react-konva制作在线制图应用(3)——在线字体文件的动态渲染

1,614 阅读3分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

相关阅读

  1. 使用konva制作在线photoshop(1)——元素拖拽、变形与导出
  2. 使用react-konva制作在线photoshop(2)——字体的文本与样式的修改

字体进阶

原生 konva 与 react-konva 中的不同点(坑)

在上一篇文章中双击修改文本的示例中,发现使用 state 驱动数据变化从而引起 Text 和 Transformer 组件引起变化,会使某些功能失效,比如,我们在键入文本改变了 Text 长度的时候,Transformer 组件并没有随其动态改变宽高。

这里的解决方法是,使用 state 去驱动 ref,调用 ref.current 上的 konva 方法.

如果使用 state 控制一个元素的显示与隐藏,如下:

useEffect(() => {
    if (trRef) {
        const tr = trRef.current
        if (isSelected && showTransformer) {
            tr.show()
            tr.forceUpdate()
        } else {
            tr.hide()
        }
    }
}, [isSelected, showTransformer, trRef])

效果如下:

总之,在 react-konva 中控制元素的显隐,一定不能用 state 直接绑到视图层!!

字体修改

和上面修改样式一样,我们只要改变fontFamily属性就可以修改Text的字体了。(当然,前提是你的项目中已经有这种字体)

// 画布组件
  useEffect(() => {
    if (
      selectedItemChange &&
      Object.keys(selectedItemChange).length &&
      selectedId
    ) {
      const index = infos.findIndex((i) => i.id === selectedId);
      const selecteditem = infos[index];
      const properties = {
        ...selecteditem,
        ...selectedItemChange,
      };
      const newInfos = [...infos];
      newInfos.splice(index, 1, properties);
      setInfo(newInfos);
    }
  }, [selectedItemChange]); // 更改选中元素的属性

  // ...
  <Stage width={width} height={height} ref={stageRef}>
        {infos.map((i: Iinfo, idx: number) => (
          <Layer key={i.id}>
          // ...
          </Layer>)}
  </Stage>

使用

  const fonts = [
    { name: 'fantasy', fontFamily: 'Fantasy' },
    { name: 'sans-serif', fontFamily: 'sans-serif' },
  ];

  // ....
 <YHSelect placeholder="请选择" onChange={changeFont}>
        {fonts.map((f) => (
          <Option value={f.fontFamily} key={f.name}>
            {f.name}
          </Option>
        ))}
      </YHSelect>
      <KonvaCanvas
        // ...
        selectedItemChange={changed}
      />

效果如下: demo

我的写法是,使用selectedItemChange字段来改变已选中元素的样式,你也可以设计更严谨的方式,这里不再详细说了。这一小节的主要问题是处理在线字体,也就是处理不存在本地/项目中的字体,是如何应用于画布上的。

在线字体渲染

这里的方案思考了很久,原本方案是直接让后端去生成含有所有字体的 css 文件,但觉得很不好,当用户从前端网页配置好一个新的字体后,这时服务端就要去写那个 css 文件;如果删掉一个字体,还要从 css 文件中去删么,这个思路显然不是最优解,后端不用生成这个 css 文件啊,只需要提供字体的地址和名称就可以了。

参考konva 官网文档中发现,可以用fontfaceobserver来处理字体文件的加载。当我们从后端中获取到字体数据后,就可以自己生成 css 文件了,把他插入到<head>中,然后再去调用这个 font。

demo

那么这部分是怎么做的呢,我们先看下后端的数据

const fonts = [
    { name: 'fantasy', fontFamily: 'Fantasy' }, // 本地
    { name: 'sans-serif', fontFamily: 'sans-serif' }, // 本地
    {
        name: 'frutiger',
        fontFamily: 'frutiger',
        url: 'http://lib.mytac.cn/frutiger.ttf', // 远程,需要下载到本地
    },
    {
        name: 'Blackletter',
        fontFamily: 'Blackletter',
        url: 'http://lib.mytac.cn/Blackletter.TTF', // 远程,需要下载到本地
    },
]

远程的字体文件都有url字段,当我们选中某种在线字体后,需要构造一段<style>代码,里面包含定义字体的 font-face 相关部分的代码,我们用 id 来区分不同的字体样式,这样就不会插入相同的<style>元素了。(注意:当应用元素时,第一次加载字体是异步的,这里需要加下 loading,下面的代码没有补充)

const addFontFaceToCss = (fontFamily: string, url: string) => {
    const id = 'fontStyle' + '_' + fontFamily
    const styleElement = document.getElementById(id)

    if (styleElement) {
        setChanged({ fontFamily: fontFamily })
    } else {
        const el = document.createElement('style')
        el.id = id
        const str = `  
    @font-face {
      font-family: ${fontFamily};
      src: url(${url});}
    ` // 构造代码
        el.innerText = str
        document.head.append(el)
        const font = new FontFaceObserver(fontFamily)
        // 这里需要加一些loading
        font.load()
            .then(function () {
                setChanged({ fontFamily: fontFamily })
                console.log('Output Sans has loaded.')
            })
            .catch(function (err) {
                console.log(err)
                console.log('Output Sans failed to load.')
            })
    }
}