This is the fifth day of my participation in the August Wen Challenge.More challenges in August

  • Konva Hands-on Practice (I)
  • Konva Hands-on Practice (II)
  • Konva Hands-on Practice (III)

Scaling of the stage

Now that we’ve created the stage, let’s scale it. We don’t need to scale the whole stage, we just need to scale the part of the stage where we put the content, which is the contentLayer we created earlier.

We’ll handle the resize event first, because as the window size and scaling changes, we need to recalculate the center of the stage to make sure the content area is displayed in the right place

/** * redraw the stage size */
export function resizeStage(stage: Konva.Stage) {
    const container = stage.container()
    const { content: contentLayer } = getLayers()
    const { width, height } = appConfig.editor.content

    // Update the stage size
    stage.width(container.offsetWidth)
    stage.height(container.offsetHeight)

    // Update the content layer position
    contentLayer.x(
        container.offsetWidth / 2 - (width * contentLayer.scaleX()) / 2
    )
    contentLayer.y(
        container.offsetHeight / 2 - (height * contentLayer.scaleY()) / 2
    )

    contentLayer.draw()
}
Copy the code

By recalculating the width and height of the stage, and x and y of the content area, we get the new content area position.

Next we can update the scale of the stage

/** * Update 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)
}
Copy the code

Zoom is our new scale, the default is 1,scale is the default scale we set before, we choose the default value is 3, so we can calculate and process the new stage scale through (1 / scale) * zoom.

Now we can use the UI to control 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)
}

Copy the code

Among them,minZoom and maxZoom represent the maximum and minimum zoom value we limit,zoomStep represents our zoomStep value, we can use onChangeZoom(1) or onChangeZoom(-1) to zoom in and out, it should be noted that we should remember to pass when calculating the zoom value Round by toFixed, because there will be fractional accuracy problems.

Add the component

Adding components is simple. You can simply create components and add them to the content container

// Create the text component
const textNode = newKonva.Text({ ... widget.property,text:'text'
})

// Add the component to the layer
contentLayer.add(textNode)

// Create the image
const image = new Image()
const imageNode = newKonva.Image({ ... widget.property,image: image
})
image.src = imageUrl

// Add the component to the layer
contentLayer.add(imageNode)
Copy the code

However, if the picture component needs to pay attention to the picture cross domain problem.

Component selection & deformation

After adding a component, we want to change the size and location of the component, so we need to add the component’s selectable and Transform functions.

Component selection We need to create a wrapper component selector to indicate that the component belongs to the selected state when the component is selected. At the same time, we need to unselect the components before clicking the unselected area, and if we hold down the CTRL key, we want multiple components to be selected at the same time.

Click on the non-object area

  • Clear all selection boxes

Click on the Component area

  • Clear all selection boxes
  • If CTRL is pressed, a joint selection box containing previously selected components is created
  • If CTRL is not pressed, the current component selection box is created

We first need a function that clears the selection box

/** * clean up the selector */
export function clearSelector(stage: Konva.Stage) {
    const transformer = stage.find<Konva.Transformer>('Transformer')
    transformer.forEach((tr) = > tr.destroy())
    stage.draw()
}
Copy the code

And a function to create a selection box

/** * Create selector *@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()

    // The editable state is selected
    // The multi-node status is selected
    if (enabled || nodes.length > 1) {
        // Set the selector name
        transformer.name('selected')}return transformer
}
Copy the code

GetAnchors is where we get the anchor configuration for each component, because we want different components to have different scaling configurations, like we want images to maintain aspect ratios when they’re scaled, and we want text fields to change width and height separately when they’re scaled.

Now that we have the methods to create and clear selection boxes, let’s deal with the non-component node selection. We need to clear all selectors when non-component nodes are selected.

/** * Processing non-component nodes check */
export function setupStageSelector(stage: Konva.Stage) {
    const { content: contentLayer } = getLayers()

    // Install the selector
    stage.on('mousedown'.(e) = > {
        // Click stage to delete all selectors
        if (e.target === stage) {
            clearSelector(stage)
            store.dispatch('updateSelected'[]),return
        }

        // Get the current clickable object
        const node = contentLayer.findOne((x) = >
            backgroundNodes.includes(e.target.name())
        )

        // Remove selector for non-selectable objects
        if (node) {
            clearSelector(stage)
            store.dispatch('updateSelected'[]),return}})}Copy the code

BackgroundNodes is the background image of the content area we set earlier. Although it is in the content area, we still treat it as the background area component.

Next we create a selection box for the selected component

/** * Install selector *@param layer
 * @param node* /
export function setupNodeSelector(stage: Konva.Stage, node: Konva.Node) {
    const { background: backgroundLayer } = getLayers()

    node.on('mousedown'.(e) = > {
        // Get the activation selector
        const activeTransformer = getActiveSelector(stage)
        // Get the selector
        const currentTransformer = getNodeSelector(stage, node)

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

        // Obtain the node to be activated
        const getNodes = () = > {
            if (e.evt.ctrlKey && activeTransformer) {
                return [...activeTransformer.nodes(), node]
            } else {
                return [node]
            }
        }

        // Get the node to be selected
        const nodes = getNodes()
        // Clear the selector
        clearSelector(stage)
        // Create a selector
        createSelector(backgroundLayer, nodes, nodes.length === 1)
        // Update the selected node
        store.dispatch(
            'updateSelected',
            nodes.map((node) = > node.id())
        )
        / / open resize&rotate
        // transformer.resizeEnabled(true)
        // transformer.rotateEnabled(true)})}Copy the code

When we’re creating a stage, we’re installing a stage selector on a component, and when we’re creating a component, we’re installing a component selector on a component, and you can see that we use getNodes to determine whether we should create a multi-component selector or a single component selector for a new component.

The reason for getActiveSelector is that we need to mouse over components that also have selector prompts.

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()
    }
})
Copy the code

In this way the user in the mouse, underline, selection have a more friendly selector prompt.

But let’s not forget to add handling for component deformations.

In fact, you can see that when we created the selector, we already added the configuration for deformation support

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

ResizeEnabled, rotateEnabled respectively expressed support for scaling and rotation, but they are modified components ScaleX, ScaleY properties, we hope that they can modify the width and height of the component.

This can be done by listening for the component’s Transform event

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

This ensures that the scale of the component is always 1 and that scaling is a modification of the width and height of the component.

Then we’ll talk about editing support for the text component, and how does the stage export the image implementation

Source address: Github

If you feel this article is helpful to your words might as well 🍉 attention + like + favorites + comments + forward 🍉 support yo ~~😛