Konva上手实践(二)

3,047 阅读5分钟

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

舞台缩放

我们之前创建了舞台,现在我们来让他支持缩放,其实我们不需要缩放整个舞台,只需要缩放其中放置内容的部分,就是之前创建的contentLayer图层.

不会在这之前我们先来处理resize事件,因为在窗口尺寸,缩放比例变化时,我们需要重新计算舞台的中心位置,来确保内容区域在正确的位置显示

/**
 * 重绘舞台尺寸
 */
export function resizeStage(stage: Konva.Stage) {
    const container = stage.container()
    const { content: contentLayer } = getLayers()
    const { width, height } = appConfig.editor.content

    // 更新舞台尺寸
    stage.width(container.offsetWidth)
    stage.height(container.offsetHeight)

    // 更新内容图层位置
    contentLayer.x(
        container.offsetWidth / 2 - (width * contentLayer.scaleX()) / 2
    )
    contentLayer.y(
        container.offsetHeight / 2 - (height * contentLayer.scaleY()) / 2
    )

    contentLayer.draw()
}

通过重新计算舞台的widthheight,和内容区域的xy,我们获得了新的内容区域位置.

下来我们可以更新舞台的缩放比例

/**
 * 更新Zoom
 * @param state
 * @param zoom
 */
function onUpdateZoom(state: EditorState, zoom: number) {
    const { scale } = appConfig.editor.content
    const { content: contentLayer } = getLayers()
    contentLayer.scaleX((1 / scale) * zoom)
    contentLayer.scaleY((1 / scale) * zoom)
    resizeStage(state.stage)
}

zoom是我们新的缩放比例,默认为1,scale是我们之前设置的默认缩放比例,我们选取的默认值是3,这样我们通过(1 / scale) * zoom就计算处理新的舞台缩放比例.

下面我们就可以通过UI来控制zoom

const zoomStep = 0.1
const minZoom = 0.5
const maxZoom = 5

function onChangeZoom(vector?: 1 | -1) {
    if (($zoom <= minZoom && vector < 0) || ($zoom >= maxZoom && vector > 0)) {
        return
    }

    const value = vector
        ? parseFloat(($zoom + vector * zoomStep).toFixed(2))
        : 1

    dispatch('updateZoom', value)
}

其中,minZoommaxZoom代表我们限制的最大最小缩放值,zoomStep表示我们的缩放步进值,我们就可以通过onChangeZoom(1)onChangeZoom(-1)来进行放大缩小操作,需要注意的是我们在计算缩放值时记得要通过toFixed取整,因为会存在小数精度问题.

添加组件

添加组件比较简单,直接创建组件添加到内容容器即可

//创建文字组件
const textNode = new Konva.Text({
    ...widget.property,
    text:'text'
})

// 添加组件到图层
contentLayer.add(textNode)

// 创建图片
const image = new Image()
const imageNode = new Konva.Image({
    ...widget.property,
    image: image
})
image.src = imageUrl

// 添加组件到图层
contentLayer.add(imageNode)

不过图片组件如果需要注意图片跨域问题.

组件选择&变形

添加组件后我们希望可以修改组件的大小和位置,那么我们需要添加组件的selectabletransform功能.

组件选择我们需要在组件被选中是创建包裹组件的选择器表示组件属于被选中的状态.同时在单击非选中的区域我们需要取消之前的组件的选择状态,以及如果按住CTRL键,希望可以多个组件被同时选中.

点击非对象区域

  • 清除所有的选择框

点击组件区域

  • 清除所有选择框
  • 如果按下CTRL,则创建包含之前选中组件的联合选择框
  • 如果未按下CTRL,则创建当前组件选择框

我们首先需要一个清除选择框的函数

/**
 * 清理选择器
 */
export function clearSelector(stage: Konva.Stage) {
    const transformer = stage.find<Konva.Transformer>('Transformer')
    transformer.forEach((tr) => tr.destroy())
    stage.draw()
}

以及一个创建选择框的函数

/**
 * 创建选择器
 * @param node
 * @param enabled
 */
export function createSelector(
    backgroundLayer: Konva.Layer,
    nodes: Konva.Node[],
    enabled: boolean = false
) {
    const transformer = new Konva.Transformer({
        keepRatio: true,
        resizeEnabled: enabled,
        rotateEnabled: enabled
    })

    if (nodes.length === 1) {
        const [node] = nodes
        transformer.enabledAnchors(getAnchors(node.getClassName()))
    }

    transformer.nodes(nodes)

    backgroundLayer.add(transformer)
    backgroundLayer.draw()

    // 可编辑状态为选中
    // 多节点状态为选中
    if (enabled || nodes.length > 1) {
        // 设置选择器名称
        transformer.name('selected')
    }

    return transformer
}

getAnchors是我们根据不同组件获取相应的锚点配置,因为我们希望不同组件有不同的缩放配置,比如我们希望图片在缩放时保持宽高比,我们希望文本框在缩放时,单独修改宽和高一类.

准备好了创建和清除选择框的方法,那么下来我们来处理非组件节点选中的情况,我们需要在选中非组件节点时清除所有选择器.

/**
 * 处理非组件节点选中
 */
export function setupStageSelector(stage: Konva.Stage) {
    const { content: contentLayer } = getLayers()

    // 安装选择器
    stage.on('mousedown', (e) => {
        // 点击舞台删除所有选择器
        if (e.target === stage) {
            clearSelector(stage)
            store.dispatch('updateSelected', [])
            return
        }

        // 获取当前可操作点击对象
        const node = contentLayer.findOne((x) =>
            backgroundNodes.includes(e.target.name())
        )

        // 非可选择对象则去除selector
        if (node) {
            clearSelector(stage)
            store.dispatch('updateSelected', [])
            return
        }
    })
}

backgroundNodes是我们之前设置的内容区域的背景图像,虽然它在内容区域,但是我们依然把他当做背景区域组件.

下来我们创建选中组件时的选择框

/**
 * 安装选择器
 * @param layer
 * @param node
 */
export function setupNodeSelector(stage: Konva.Stage, node: Konva.Node) {
    const { background: backgroundLayer } = getLayers()

    node.on('mousedown', (e) => {
        // 获取激活选择器
        const activeTransformer = getActiveSelector(stage)
        // 获取选择器
        const currentTransformer = getNodeSelector(stage, node)

        if (currentTransformer && currentTransformer === activeTransformer) {
            return
        }

        // 获取待激活节点
        const getNodes = () => {
            if (e.evt.ctrlKey && activeTransformer) {
                return [...activeTransformer.nodes(), node]
            } else {
                return [node]
            }
        }

        // 获取待选择节点
        const nodes = getNodes()
        // 清除选择器
        clearSelector(stage)
        // 创建选择器
        createSelector(backgroundLayer, nodes, nodes.length === 1)
        // 更新选中节点
        store.dispatch(
            'updateSelected',
            nodes.map((node) => node.id())
        )
        // 开启resize&rotate
        // transformer.resizeEnabled(true)
        // transformer.rotateEnabled(true)
    })
}

我们在创建舞台是,安装舞台对选择器的处理,在创建组件是,在组件上安装组件选择器的处理,可以看到我们通过getNodes来判断我们是应该创建多组件选择器还是对新组件创建单组件选择器.

至于getActiveSelector是因为我们需要在鼠标滑过组件是也有选择器的提示.

node.on('mouseenter', (e) => {
    if (!getNodeSelector(stage, node)) {
        createSelector(backgroundLayer, [node], false)
    }
})

node.on('mouseout', (e) => {
    const transformer = getNodeSelector(stage, node)
    if (transformer && transformer.name() !== 'selected') {
        transformer.destroy()
    }
})

这样用户在鼠标划入,划出,选择时都有比较友好的选择器提示了.

但是我们不要忘了添加对组件变形的处理.

其实可以看到,在创建选择器时,我们已经添加对变形支持的配置

const transformer = new Konva.Transformer({
    keepRatio: true,
    resizeEnabled: enabled,
    rotateEnabled: enabled
})

resizeEnabled,rotateEnabled分别表示了对缩放和旋转的支持,但是他们修改的是组件的ScaleX,ScaleY属性,我们更希望他们可以修改组件的宽和高.

这个可以通过监听组件的transform事件来实现

node.on('transform', (e) => {
    node.setAttrs({
        width: Math.max(node.width() * node.scaleX()),
        height: Math.max(node.height() * node.scaleY()),
        scaleX: 1,
        scaleY: 1
    })
})

这样我们就保证了,组件的scale始终为1,缩放操作是对组件的宽高的修改.

之后我们会说一说文本组件的编辑支持,以及舞台如何导出图片的实现

源码地址: github

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