blog icon indicating copy to clipboard operation
blog copied to clipboard

如何优雅的通过canvas实现一个简单的文本编辑器

Open forthealllight opened this issue 4 years ago • 9 comments

如何优雅的通过canvas实现一个简单的文本编辑器


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

  • canvas文本编辑器的需求场景
  • 如何实现一个canvas简单的文本编辑器
  • 编辑器功能优化

源码参考地址:地址为:https://github.com/forthealllight/canvasTextEditor


一、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无法模拟的,比如:


文字1

文字2

这种场景下,对于复杂渲染的文字,要实现文本编辑同时还可以边编辑边预览,就必须要使用到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);
    
    

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

Untitled3

简易文本编辑器源码的地址为:https://github.com/forthealllight/canvasTextEditor

forthealllight avatar Jun 23 '20 12:06 forthealllight

太厉害了

libin1991 avatar Jul 07 '20 14:07 libin1991

目前就只有 Google Docs 敢商用 canvas 富文本编辑器,难度实在太大。

SolidZORO avatar Sep 27 '20 03:09 SolidZORO

目前就只有 Google Docs 敢商用 canvas 富文本编辑器,难度实在太大。

确实难度很大。我这个也只是简单的demo

forthealllight avatar Sep 28 '20 07:09 forthealllight

腾讯文档好像也是

Pythonofsdc avatar Jun 11 '21 11:06 Pythonofsdc

飞书的在线文档也是canvas

yangguansen avatar Aug 16 '21 02:08 yangguansen

飞书不是canvas

hhm1999 avatar Aug 30 '22 06:08 hhm1999

自动回复邮件。您好,我已收到您的来信,会尽快给您回复。

yangnaiyue avatar Aug 30 '22 06:08 yangnaiyue

在手机上无法弹出软键盘。

oneofzero avatar Nov 09 '23 08:11 oneofzero

自动回复邮件。您好,我已收到您的来信,会尽快给您回复。

yangnaiyue avatar Nov 09 '23 08:11 yangnaiyue