在最近的项目中,须要经过canvas来实现一个文本编辑器,大部分场景中,其实都不须要经过canvas来实现一个编辑器。只有那种须要利用canvas的绘制功能,实现div/css没法模拟出的文字效果,此时你须要利用canvas来实现文本编辑和渲染。此外,使用canvas实现文本编辑并非最优的,甚至是不推荐的方案,由于会存在频繁的canvas重绘。本文介绍的是如何经过canvas来实现一个简单的文本编辑器。css
- canvas文本编辑器的需求场景
- 如何实现一个canvas简单的文本编辑器
- 编辑器功能优化
源码参考地址:地址为:github.com/fortheallli…git
原文发表在个人github: github.com/fortheallli…github
首先,仍是要强调第一点:大部分场景下,你可能不须要经过canvas来实现文本编辑器。只有两种条件下,你须要使用canvas来实现文本编辑功能:canvas
举一个使用canvas文本编辑器的例子,fabric.js是一个简化canvas绘图的工具,提供了强大的矢量图功能,而且能够方便的在canvas上的局部区域绘制一个个不一样的图案,这里局部区域称为model,不一样的model之间又能够交互等等。app
在fabric.js常常须要对于局部的model,作一些动画效果,若是这个model是一个文本,下面咱们简称文本model,用div模拟,咱们称div文本编辑。那么须要作两次映射。dom
文本model渲染结果——> div文本编辑(模拟渲染结果)——>编辑 ——> 文本modal渲染结果(反重现)编辑器
这么两次映射,若是比较复杂,显然是有必定的转化工做量,这种 工做量的大小并不致命,可是这种转化的文本编辑方式,没法实时的去编辑,必须编辑完成后,才能再canvas中渲染出来。工具
除此以外,咱们知道大部分的文字渲染效果,经过css均可以彻底模拟出来,可是有一些文字的渲染效果是css没法模拟的,好比:字体
这种场景下,对于复杂渲染的文字,要实现文本编辑同时还能够边编辑边预览,就必需要使用到canvas来实现文本编辑器。优化
咱们来简单的看一下fabric.js中文本编辑的效果:
能够访问fabricjs官网来查看这个文本编辑的案例,按F12咱们能够发现,该文本编辑器并非经过dom来模拟实现的,是经过canvas来直接实现文本编辑功能的。
首先经过canvas实现文本编辑,主要利用的是canvas的fillText用于绘制文字。在处理文本编辑的场景,首先要处理的是光标的问题,本文中的方法,没有模拟出光标的闪烁效果。本文的简易文本编辑器中,经过“|” 来实现光标的功能。
好比:
我是一只小鸟|
复制代码
就是一个js的字符串str = "我是一只小鸟|",咱们用一个竖线“|” 来模拟光标
这种简单的设定,只要咱们改变|的位置,从新绘制就实现了文本编辑器中相似的光标移动。
若是对于只有一行的文本,这里咱们能够保存光标的位置就是一维的,不过咱们场景的文本编辑器都是多行文本的,所以咱们须要保存光标的位置也是二维的,决定光标在哪一行,在哪一列。
this.focusIndex = [x,y]
复制代码
保存了光标的位置以后,咱们就能够调用fillText方法一行行的绘制文字,若是改行出现了光标,咱们就在改行的字符串中插入“|”,最后的绘制结果,就彻底模拟了文本编辑器中的光标的实现。
须要实现鼠标点击来切换文本编辑器的光标的功能时,咱们须要测量多行文本中,每一个文字所在屏幕中的位置,计算位置的关键是如何计算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;
};
复制代码
由此咱们就知道了如何计算每一个文字的宽度和高度,从而计算出每一个文字的位置。
在canvas中绘制文本还有另外一个重要的点,就是坐标转换,如何将css坐标转化和canvas的绘图坐标进行转化,须要理解canvas的绘图坐标和canvas的css坐标之间的区别,转化的公式以下
let ratio = canvas.width / cancas.style.width
let updateClientX = ratio * clientX
复制代码
除了鼠标能够点击切换光标的位置外,还能够经过上下左右键来更新光标的位置:
if(this.isFocus && e.key === 'ArrowUp'){
//边界判断
if(this.focusIndex[0]>0){
}
}
}
复制代码
根据方位键能够移动光标的位置,特别注意的是须要处理边界条件,好比移动到某一行最后一列,再移动就须要换行等。
除此以外,还有回车换行Enter和删除BackSpace键的处理这里不一一举例。
如何往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…