添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

在最近的项目中,需要通过canvas来实现一个文本编辑器,大部分场景中,其实都不需要通过canvas来实现一个编辑器。只有那种需要利用canvas的绘制功能,实现div/css无法模拟出的文字效果,此时你需要利用canvas来实现文本编辑和渲染。此外,使用canvas实现文本编辑并不是最优的,甚至是不推荐的方案,因为会存在频繁的canvas重绘。本文介绍的是如何通过canvas来实现一个简单的文本编辑器。

  • canvas文本编辑器的需求场景
  • 如何实现一个canvas简单的文本编辑器
  • 编辑器功能优化
  • 源码参考地址:地址为: github.com/fortheallli…

    原文发表在我的github: github.com/fortheallli…

    一、canvas文本编辑器的需求场景

    首先,还是要强调第一点:大部分场景下,你可能不需要通过canvas来实现文本编辑器。只有两种条件下,你需要使用canvas来实现文本编辑功能:

  • canvas画布的图案,包含文字,需要做整体的动画以及转场等特效,需要实时编辑的场景(边编辑边渲染)
  • css无法模拟出的一些特殊文字效果,需要canvas来补充文字渲染特效
  • 举一个使用canvas文本编辑器的例子, fabric.js 是一个简化canvas绘图的工具,提供了强大的矢量图功能,并且可以方便的在canvas上的局部区域绘制一个个不同的图案,这里局部区域称为model,不同的model之间又可以交互等等。

    fabric.js 经常需要对于局部的model,做一些动画效果,如果这个model是一个文本,下面我们简称文本model,用div模拟,我们称div文本编辑。那么需要做两次映射。

    文本model渲染结果——> div文本编辑(模拟渲染结果)——>编辑 ——> 文本modal渲染结果(反重现)

    这么两次映射,如果比较复杂,显然是有一定的转化工作量,这种 工作量的大小并不致命,但是这种转化的文本编辑方式,无法实时的去编辑,必须编辑完成后,才能再canvas中渲染出来。

    除此之外,我们知道大部分的文字渲染效果,通过css都可以完全模拟出来,但是有一些文字的渲染效果是css无法模拟的,比如:

    这种场景下,对于复杂渲染的文字,要实现文本编辑同时还可以边编辑边预览,就必须要使用到canvas来实现文本编辑器。

    我们来简单的看一下 fabric.js 中文本编辑的效果:

    文本编辑效果

    可以访问 fabricjs官网 来查看这个文本编辑的案例,按F12我们可以发现,该文本编辑器并不是通过dom来模拟实现的,是通过canvas来直接实现文本编辑功能的。

    二、如何实现一个简单的文本编辑器

    (1)如何模拟光标

    首先通过canvas实现文本编辑,主要利用的是canvas的fillText用于绘制文字。在处理文本编辑的场景,首先要处理的是光标的问题,本文中的方法,没有模拟出光标的闪烁效果。本文的简易文本编辑器中,通过“|” 来实现光标的功能。

                   我是一只小鸟|
    

        就是一个js的字符串str = "我是一只小鸟|",我们用一个竖线“|” 来模拟光标

        这种简单的设定,只要我们改变|的位置,重新绘制就实现了文本编辑器中类似的光标移动。

        如果对于只有一行的文本,这里我们可以保存光标的位置就是一维的,不过我们场景的文本编辑器都是多行文本的,因此我们需要保存光标的位置也是二维的,决定光标在哪一行,在哪一列。

                 this.focusIndex = [x,y]
    

        保存了光标的位置之后,我们就可以调用fillText方法一行行的绘制文字,如果改行出现了光标,我们就在改行的字符串中插入“|”,最后的绘制结果,就完全模拟了文本编辑器中的光标的实现。

    (2)如何处理鼠标点击切换文本编辑器的光标

        需要实现鼠标点击来切换文本编辑器的光标的功能时,我们需要测量多行文本中,每个文字所在屏幕中的位置,计算位置的关键是如何计算canvas绘制的文字,每一个文字的宽度和高度。

    canvas中文字的宽度:可以通过canvas的measureText来测量文字的宽度

    canvas中文字的高度:在canvas中是没有测量文字高度的方法的,不过canva中的文字跟div/css中渲染的文字,高度的实现方式是相同的,我们可以在div中渲染相同字体的文字,从而测量出其高度,这个高度跟在canvas中渲染出来的文字的高度是一致的。

    下面是通过测量div中文字的高度,来类推canvas中文字的高度的方法:

    var FontMetrics = function(family, size) {
          this._family = family || (family = "Monaco, 'Courier New', Courier, monospace");
          this._size = parseInt(size) || (size = 12);
          // Preparing container
          var line = document.createElement('div'),
              body = document.body;
          line.style.position = 'absolute';
          line.style.whiteSpace = 'nowrap';
          line.style.font = size + 'px ' + family;
          body.appendChild(line);
          // Now we can measure width and height of the letter
          line.innerHTML = 'm'; // It doesn't matter what text goes here
          this._width = line.offsetWidth;
          this._height = line.offsetHeight;
          // Now creating 1px sized item that will be aligned to baseline
          // to calculate baseline shift
          var span = document.createElement('span');
          span.style.display = 'inline-block';
          span.style.overflow = 'hidden';
          span.style.width = '1px';
          span.style.height = '1px';
          line.appendChild(span);
          // Baseline is important for positioning text on canvas
          this._baseline = span.offsetTop + span.offsetHeight;
          document.body.removeChild(line);
        FontMetrics.prototype.getSize = function() {
          return this._size;
    

        由此我们就知道了如何计算每个文字的宽度和高度,从而计算出每个文字的位置。

    (3)坐标转换

        在canvas中绘制文本还有另一个重要的点,就是坐标转换,如何将css坐标转化和canvas的绘图坐标进行转化,需要理解canvas的绘图坐标和canvas的css坐标之间的区别,转化的公式如下

    let ratio  = canvas.width / cancas.style.width
    let updateClientX  = ratio * clientX
    

    (4)处理回车,空格,上下左右等按键

        除了鼠标可以点击切换光标的位置外,还可以通过上下左右键来更新光标的位置:

    if(this.isFocus && e.key === 'ArrowUp'){
            //边界判断
            if(this.focusIndex[0]>0){
    

        根据方位键可以移动光标的位置,特别注意的是需要处理边界条件,比如移动到某一行最后一列,再移动就需要换行等。

    除此之外,还有回车换行Enter和删除BackSpace键的处理这里不一一举例。

    (5)处理文字的键入

        如何往canvas的文本编辑器中键入值,这个问题我们需要引入一个textArea节点,改textArea的节点位置和文本光标的位置保持一致,我们需要设置zIndex,将canvas覆盖在textArea上:

     this.textAreaLocation = () => {
        //找出光标的位置,并令其绝对定位之
        canvas.style.zIndex = 100;
        canvas.style.position = 'absolute'
        that.TextArea.style.position = 'absolute';
        that.TextArea.style.zIndex = -1000;
        that.TextArea.style.opacity = 0;
        let y = this.focusIndex[0]
        let x = this.focusIndex[1]
        let cur = this.localArr[y][x]
        that.TextArea.style.left = cur.x + 'px'
        that.TextArea.style.top = cur.y.start + 'px';
    

    当点击canvas文本编辑区时:

      textArea.focus()
    

    当点击文本编辑区以外的时候,

      textArea.blur()
    

        当输入文字的时候,监听textArea的input事件,从而拿到textArea输入的值,从而渲染在canvas中。这样就能实现英文的输入,但是中文的键入无法支持,如果需要文本编辑器可以输入中文,需要在textArea的input事件的基础上,增加监听textArea的compositionstart事件compositionend事件

    这里的判断逻辑是:

    如果触发了compositionstart事件说明是一个中文键入,在compositionend事件中可以拿到完成中文输入法后输入的完整的值,否则就是一个英文键入,只需要在input中拿到英文键入值。

    完整的代码如下:

      this.TextArea.addEventListener('compositionstart',function(e){
            that.inputStatus = 'CHINESE_TYPING';
        },false);
        this.TextArea.addEventListener('input',function(e){
            if (that.inputStatus === 'CHINESE_TYPING') {
                return;
            //处理英文输入
        },false);
        this.TextArea.addEventListener('compositionend',function(e){
           if(that.inputStatus === 'CHINESE_TYPING'){
                 //处理中文输入
                 e.data .. //中文输入的值 
        },false);
    

    到此 为止,我们基本上可以得到一个完成的简易文本编辑器。具体的效果如下:

    简易文本编辑器源码的地址为:github.com/fortheallli…

    分类:
    前端
    标签: