Konva上手实践(三)

2,025 阅读2分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

文本编辑

我们之前说过,Konva并没有TextBox组件,而Text组件默认是不支持编辑功能,所以我们使用Konva没有办法添加文本框组件,不过我们可以通过DOM的方式来处理这个问题.

具体的思路,应该就是在双击文本组件的时候,我们在我们组件上创建一个DOM的输入框组件放置在文本组件对应位置的上层,当结束输入时,就销毁输入框组件,我们可以从输入框组件中获取文本值来更新显示的文本组件.

我们认为按下回车和输入框失去焦点两个事件是结束输入状态的标志.

/**
 * 创建编辑区域
 * @param node
 */
function createTextarea(node: Konva.Text) {
    const layer = node.getLayer()
    const stage = layer.getLayer()

    // 获取文本位置
    const areaPosition = {
        x: stage.offsetX() + node.absolutePosition().x,
        y: stage.offsetY() + node.absolutePosition().y
    }
    
    // 创建TextArea
    const textarea = document.createElement('textarea')
    const container = document.getElementById('canvas-container')
    container.appendChild(textarea)

    textarea.value = node.text()
    textarea.style.position = 'absolute'
    textarea.style.top = areaPosition.y + 'px'
    textarea.style.left = areaPosition.x + 'px'
    textarea.style.width = `${node.width() - node.padding() * 2}px`
    textarea.style.height = `${node.height() - node.padding() * 2 + 5}px`
    textarea.style.fontSize = `${node.fontSize()}px`
    textarea.style.border = 'none'
    textarea.style.padding = '0px'
    textarea.style.margin = '0px'
    textarea.style.overflow = 'hidden'
    textarea.style.background = 'none'
    textarea.style.outline = 'none'
    textarea.style.resize = 'none'
    textarea.style.wordBreak = 'break-all'
    textarea.style.fontFamily = node.fontFamily()
    textarea.style.transformOrigin = 'left top'
    textarea.style.textAlign = node.align()
    textarea.style.color = node.fill()
    textarea.style.transform = `rotateZ(${node.rotation()}deg) scale(${layer.scaleX()})`
    textarea.focus()

    /**
     * 移除Textarea
     */
    function removeTextarea() {
        textarea.parentNode.removeChild(textarea)
        node.show()
    }

    textarea.addEventListener('keydown', function (e) {
        if (e.key === 'Enter' && !e.shiftKey) {
            textarea.blur()
        }
    })

    textarea.addEventListener('blur', function (e) {
        node.text(textarea.value)
        node.fire(TextWidgetEvent.input)
        removeTextarea()
    })
}

可以发现我们从文本组件中获取属性,通过配置输入框的style来让他们保持一致的样式,这样可以在用户双击时减少视觉上的变化.

组件的缩放比和角度通过style.transform来进行更新,我们使用blurkeydown事件来确认结束输入的时机.

这样我们在创建文本组件时就可以来添加文本编辑支持.

// 添加编辑功能
textNode.on('dblclick dbltap', () => {
    // 隐藏原节点
    node.hide()

    // 获取文本位置
    createTextarea(node)
})

事件监听

我们为了保证UI上显示和数据的同步,我们通过监听组件属性的修改来同步修改我们保存的组件数据.

组件可以通过on来监听对应的组件事件,我们需要来监听的事件有:

  • transformend
  • dragend

transformend是选择框拖动锚点产生的事件,当监听到transformed事件时,我们需要修改如下属性

  • x
  • y
  • width
  • height
  • scaleX
  • scaleY
  • rotation

dragend是组件拖动事件,他会影响组件的x,y

我们也可以通过node.fire来自定义我们需要的事件,比如文本框输入时需要修改存储的文本值.

node.fire('input')

node.on('input', () => {
    // 更新文本值
})

导出图片

我们可以通过stagetoDataUrl可以导出图片的base64编码,然后我们就可以使用a标签实现下载功能

不过在之前我们需要先通过我们之前设置缩放比来计算输出的比例和尺寸

const { content: contentLayer } = getLayers()
const width =
    (contentLayer.clip().width / appConfig.editor.content.scale) * $zoom
const height =
    (contentLayer.clip().height / appConfig.editor.content.scale) * $zoom

因为我们不希望导出整个舞台,而只是希望导出其中内容区域的部分,所以x,y应该使用contentLayerx,y

const dataURL = $stage.toDataURL({
    width,
    height,
    x: contentLayer.x(),
    y: contentLayer.y(),
    pixelRatio: appConfig.editor.content.scale,
    quality: 1
})

现在我们获得了可以用来下载的dataURL,就可以通过a标签直接进行下载了

const download = (uri, name) => {
    const link = document.createElement('a')
    link.download = name
    link.href = uri
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
}

download(dataURL, `${Date.now()}.png`)

但是在生成过程中也常常会遇见一种问题

Konva error: Unable to get data URL. Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

因为图片跨域会在导出的时候发生报错.

一种解决办法是可以尝试设置跨域标记

img.crossOrigin = "anonymous"

或者也可以尝试直接获取图片的base64编码来设置src来解决

export async function getBase64FromUrl(url) {
    const timespan = Math.random().toString(32).slice(2)
    const data = await fetch(url + `?v=${timespan}`)
    const blob = await data.blob()
    return new Promise((resolve) => {
        const reader = new FileReader()
        reader.readAsDataURL(blob)
        reader.onloadend = () => {
            const base64data = reader.result
            resolve(base64data)
        }
    })
}

此外还有遇到一种情况,就是第一次获取图片是正常,第二次再打开就会报跨域错误.这是因为第一次找服务器请求,服务器返回了跨域头信息,而第二次图片被缓存了,直接走了浏览器缓存后没有了跨域头信息,所以产生了跨域错误.这是需要在图片路径上加一个随机时间戳来防止读取缓存数据就可以了.

img.src = `url?timestamp=${Date.now()}`

源码地址: github

如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛