文章目录
-
- 概要
- 效果
- 技术细节
- 代码
概要
Canvas上面提供输入:
一、最简单可能是用dom渲染一个input,覆盖在图形上面进行文本编辑,编辑完再把内容更新到图形.这样简单,但是缺点也明显,就是它不是真正绘制在canvas上面,没有层级。体验感较差
Fabric框架的思路大概是这样的。
二、如果要用自己是完全实现:键盘响应、撤消/重做、文本样式/布局、光标/选中区。那也有点难度。
三、还有一种就是利用contentEditable和textarea元素,在这些元素上面进行一些事件监听和文本内容处理。最重要的是保证canvas的字体样式要与元素的字体样式一样,这样才能利用textarea的兴标和选中区体系。不然的话就自己完完全全实现。
我下面实现纯canvas绘制的,就是利用textarea的键盘响应和光标体系,包括选中块。大楖就是保证canvas与元素之间这种些重要属性,做好同步。
下面看一下效果,因为也是花了二三个小时,弄了一个比较简单的,输入选中,光标指定位置生入,性能也是可以,输入响应很快
效果
。
技术细节
技术细节有一个地方,我是这样做的。因为我也没有借鉴别人的,就昨天突然闲,随手写了一下。
就是文本选中高亮:检测到selectionStart和selectionEnd不相等的情况下,就证明是选中区。
选中区的文本颜色和背景要高亮,如果做文本计算的话,那有点麻烦,而且性能也不好/。
我是这样做的。
文本先以默认颜色绘制一遍,然后把选中区作为剪切区域。再清空剪切区域的旧文本。再以高亮的颜色背景文本,再绘制一遍。就行了,这样很简单,不需要做额外文本处理了.
其它的:像光标位置,做到以下几点:
输入时:保持textarea的光标位置与canvas的同步
单点canvas文本框的时候:做一下坐标计算,算出光标位置,然后同步给textarea元素。
选中时候也需要同步给textarea元素,并且textarea也要选中这个区域。这样保证选中区删除文本或插入文本,保持一致
代码
不用管 canvasShapeRender这wh ,这是之前写一个类似figma的渐变调节器,小小的封装了一下,没有别的功能
var renderer = new CanvasShapeRender(container, {
width: 500,
height: 500,
background: '#efefef'
})
renderer.add(new RichInputEditor({
owner: renderer,
x: 100,
y: 100
}))
renderer.requestDraw()
class RichInputEditor2 extends CanvasShape {
constructor(opts = {}) {
super({
type: 'group',
...opts
})
this.minWidth = Math.max(200, this.width)
this.minHeight = Math.max(30, this.height)
this.width = this.minWidth
this.height = this.minHeight
let scope = this;
this.selectionStart = 0
this.selectionEnd = 0;
this.curLine = 0;// 光标所在行
this.curX = 0
// this.curX=0 // 光标x轴位置
this.lineHeight = 20
this._focus = false;
// 光标x轴位置
let getCursorX = () => {
let texts = this.text.texts;
if (this.curLine >= texts.length) {
return 0
}
if (this.selectionStart === this.selectionEnd) {
return this.text.getPositionFromOffsetAndLine(this.selectionStart,this.curLine)
}
return 0
}
let getCursorLine = () => {
let line = this.text.getLineFromPosition(this.selectionStart)
return line
}
let run = false;
let updateCursor = () => {
if (run) {
return
}
run = true;
Promise.resolve().then(() => {
this.selectionStart = this._textarea.selectionStart
this.selectionEnd = this._textarea.selectionEnd
this.width=this._textarea.scrollWidth
this.height=this._textarea.scrollHeight
this.curLine = getCursorLine()
this.curX = getCursorX()
run = false
})
}
let border = this.border = this.addShape({
type: 'rect',
x: 0,
y: 0,
fillStyle: '#fff',
strokeStyle: '#000',
cursor:'text',
beforeUpdate() {
// scope.width=Math.max(minWidth,text.getTextMaxWidth())
this.width = scope.width
this.height = scope.height
},
mousedown(e) {
let [x,y]=this.transformLocalCoord(e.downPoint.x, e.downPoint.y)
this.__selectionStart=null
setTimeout(() => {
scope._textarea.focus()
scope._focus = true
let selectionStart=scope.text.getSelectionFromPosition(x,y)
scope.selectionStart=selectionStart
scope.selectionEnd=selectionStart
scope._textarea.selectionStart=selectionStart
scope._textarea.selectionEnd=selectionStart
this._selectionStart=selectionStart
updateCursor()
this.owner.requestDraw()
})
e.stop()
},
drag(e){
if(this._selectionStart==null){
return
}
let [x,y]=this.transformLocalCoord(e.point.x, e.point.y)
let _selectionEnd=scope.text.getSelectionFromPosition(x,y)
let _selectionStart=this._selectionStart
let selectionStart=Math.min(_selectionStart,_selectionEnd)
let selectionEnd=Math.max(_selectionStart,_selectionEnd)
console.log('scope.selectionStart',selectionStart,selectionEnd)
scope.selectionStart=selectionStart
scope.selectionEnd=selectionEnd
this.owner.requestDraw()
},
mouseup(){
if(scope.selectionStart!==scope.selectionEnd){
scope._textarea.setSelectionRange(scope.selectionStart,scope.selectionEnd)
}
this.owner.requestDraw()
}
})
let text = this.text = this.addShape({
silent:true,
type: "text",
x: 2,
// ignore:true,
fillStyle: '#000',
textBaseline: 'middle',
font: 'normal normal normal normal 14px sans-serif',
beforeUpdate() {
//this.lineHeight=80
this.textOffset=[0,scope.lineHeight * 0.6]
},
})
let selectionArea=new CanvasShapePath2D({
silent:true,
visible:false,
fillStyle:'#0000ff',
beforeUpdate(){
this.visible=scope.selectionStart!==scope.selectionEnd&&scope._focus
let points=text.getSelectAreaFromSelection(scope.selectionStart,scope.selectionEnd)
console.log('selectionArea',this.visible)
this.fromMultiPolygon(points)
}
})
this.add(selectionArea)
let lightText = this.addShape({
type: "text",
x: 2,
silent:true,
visible:false,
clipClearCanvas:true,
fillStyle: '#fff',
textBaseline: 'middle',
drawClip(ctx){
if(this.clipPath){
ctx.beginPath()
ctx.clip(this.clipPath)
ctx.fillStyle=selectionArea.fillStyle
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height)
}
},
beforeUpdate() {
this.font=text.font
//this.lineHeight=80
this.textOffset=[0,scope.lineHeight * 0.6]
this.texts=text.texts
this.setFontProperties(text)
if(selectionArea.visible){
this.clipPath=selectionArea.path2d
this.visible=true;
}else{
this.clipPath=null
lightText.visible=false
}
// this.height=scope.height
},
})
text.setTextContent('fdasfdsaffdsfadasfdafdas\nabcdefg')
let cursor = this.cursor = this.addShape({
type: "line",
x0: 0,
y0: 0,
x0: 0,
y1: 30,
x: 2,
strokeStyle: '#000',
beforeUpdate() {
this.visible=scope.selectionStart===scope.selectionEnd&&scope._focus
this.x = 2 + scope.curX
this.y = scope.curLine * scope.lineHeight
this.y1 = scope.lineHeight
this.lineHeight = scope.lineHeight
},
})
this._textarea = document.createElement('textarea')
this._textarea.style.position = 'absolute'
this._textarea.style.top = '0px'
this._textarea.style.left = '-1000px'
this._textarea.style.boxSizing = 'border-box'
this._textarea.style.width = this.width + 'px'
this._textarea.style.height = this.height + 'px'
this._textarea.style.userSelect='none'
this._textarea.value=text.getTextContent()
text.bindDomTextStyle(this._textarea)
// this._textarea.style.opacity=0
this._textarea.addEventListener('input', (e) => {
let texts = e.target.value.split(/\n/)
this.text.setTextContent(texts)
updateCursor()
this.owner.requestDraw()
})
document.body.appendChild(this._textarea)
}
ownerCreate() {
this.owner.onMousedown = () => {
if (this._focus) {
this._focus = false
this.owner.requestDraw()
}
}
let loop = () => {
if (this._focus) {
// this.syncTextareaToCanvas()
this.cursor.ignore = !this.cursor.ignore
this.owner.requestDraw()
}
setTimeout(loop, 800)
}
loop()
}
}