在最近的项目中,需要通过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'
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…