实现简单纯Canvas文本输入框

16 篇文章 0 订阅
4 篇文章 0 订阅

 

 

概要

Canvas上面提供输入:

一、最简单可能是用dom渲染一个input,覆盖在图形上面进行文本编辑,编辑完再把内容更新到图形.这样简单,但是缺点也明显,就是它不是真正绘制在canvas上面,没有层级。体验感较差

 

二、如果要用自己是完全实现:键盘响应、撤消/重做、文本样式/布局、光标/选中区。那也有点难度。

三、还有一种就是利用contentEditable和textarea元素,在这些元素上面进行一些事件监听和文本内容处理。最重要的是保证canvas的字体样式要与元素的字体样式一样,这样才能利用textarea的兴标和选中区体系。不然的话就自己完完全全实现。

我下面实现纯canvas绘制的,就是利用textarea的键盘响应和光标体系,包括选中块。大楖就是保证canvas与元素之间这种些重要属性,做好同步。

下面看一下效果,因为也是花了二三个小时,弄了一个比较简单的,输入选中,光标指定位置生入,性能也是可以,输入响应很快

效果

 

ffca799a24df177025ca019bf24f34d7.gif

 

技术细节

技术细节有一个地方,我是这样做的。因为我也没有借鉴别人的,就昨天突然闲,随手写了一下。

就是文本选中高亮:检测到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()
            }
 
        }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值