Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
forthealllight opened this issue Jun 23, 2020 · 9 comments
Open

Comments

@forthealllight
Copy link
Owner

forthealllight commented Jun 23, 2020

如何优雅的通过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

@libin1991
Copy link

太厉害了

@forthealllight forthealllight changed the title 如何优雅的通过cavans实现一个简单的文本编辑器 如何优雅的通过canvas实现一个简单的文本编辑器 Aug 9, 2020
@SolidZORO
Copy link

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

@forthealllight
Copy link
Owner Author

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

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

@Pythonofsdc
Copy link

腾讯文档好像也是

@yangguansen
Copy link

飞书的在线文档也是canvas

@hhm1999
Copy link

hhm1999 commented Aug 30, 2022

飞书不是canvas

@yangnaiyue
Copy link

yangnaiyue commented Aug 30, 2022 via email

@oneofzero
Copy link

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

@yangnaiyue
Copy link

yangnaiyue commented Nov 9, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants