如何優雅的經過cavans實現一個簡單的文本編輯器


    在最近的項目中,須要經過canvas來實現一個文本編輯器,大部分場景中,其實都不須要經過canvas來實現一個編輯器。只有那種須要利用canvas的繪製功能,實現div/css沒法模擬出的文字效果,此時你須要利用canvas來實現文本編輯和渲染。此外,使用canvas實現文本編輯並非最優的,甚至是不推薦的方案,由於會存在頻繁的canvas重繪。本文介紹的是如何經過canvas來實現一個簡單的文本編輯器。css

  • canvas文本編輯器的需求場景
  • 如何實現一個canvas簡單的文本編輯器
  • 編輯器功能優化

源碼參考地址:地址爲:github.com/fortheallli…git

原文發表在個人github: github.com/fortheallli…github


1、canvas文本編輯器的需求場景

    首先,仍是要強調第一點:大部分場景下,你可能不須要經過canvas來實現文本編輯器。只有兩種條件下,你須要使用canvas來實現文本編輯功能:canvas

  • canvas畫布的圖案,包含文字,須要作總體的動畫以及轉場等特效,須要實時編輯的場景(邊編輯邊渲染)
  • css沒法模擬出的一些特殊文字效果,須要canvas來補充文字渲染特效

    舉一個使用canvas文本編輯器的例子,fabric.js是一個簡化canvas繪圖的工具,提供了強大的矢量圖功能,而且能夠方便的在canvas上的局部區域繪製一個個不一樣的圖案,這裏局部區域稱爲model,不一樣的model之間又能夠交互等等。app

    在fabric.js常常須要對於局部的model,作一些動畫效果,若是這個model是一個文本,下面咱們簡稱文本model,用div模擬,咱們稱div文本編輯。那麼須要作兩次映射。dom

文本model渲染結果——> div文本編輯(模擬渲染結果)——>編輯 ——> 文本modal渲染結果(反重現)編輯器

    這麼兩次映射,若是比較複雜,顯然是有必定的轉化工做量,這種 工做量的大小並不致命,可是這種轉化的文本編輯方式,沒法實時的去編輯,必須編輯完成後,才能再canvas中渲染出來。工具

    除此以外,咱們知道大部分的文字渲染效果,經過css均可以徹底模擬出來,可是有一些文字的渲染效果是css沒法模擬的,好比:字體


文字1

文字2

這種場景下,對於複雜渲染的文字,要實現文本編輯同時還能夠邊編輯邊預覽,就必需要使用到canvas來實現文本編輯器。優化

咱們來簡單的看一下fabric.js中文本編輯的效果:


文本編輯效果

    能夠訪問fabricjs官網來查看這個文本編輯的案例,按F12咱們能夠發現,該文本編輯器並非經過dom來模擬實現的,是經過canvas來直接實現文本編輯功能的。

2、如何實現一個簡單的文本編輯器

(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

簡易文本編輯器源碼的地址爲:github.com/fortheallli…

相關文章
相關標籤/搜索