This article is a supplement to the analysis of some technical essentials of visual drag-and-drop component library. The last article mainly explained the following function points:

  1. The editor
  2. Custom components
  3. Drag and drop
  4. Delete components and adjust layers
  5. Zoom in on
  6. Undo, redo
  7. Component Property Settings
  8. The adsorption
  9. Preview and save the code
  10. The binding event
  11. The binding of animation
  12. Import the PSD
  13. Mobile phone model

Now, this article will add four more feature points, which are:

  • Drag the rotation
  • Copy, paste and cut
  • Data interaction
  • release

As in the previous article, I’ve updated the code for the new feature to Github:

  • Github project address
  • The online preview

Friendly reminder: it is recommended to read the source code together for better results (this DEMO uses Vue technology stack).

14. Drag rotation

At the time of writing an article, the original DEMO already supports rotation. But this rotation feature leaves a lot to be desired:

  1. Drag rotation is not supported.
  2. Incorrect zoom in and out after rotation.
  3. The auto-snap after rotation is not correct.
  4. The cursor of the eight retractable points after rotation is incorrect.

In this section, we will address each of these questions one by one.

Drag the rotation

Drag rotation requires the use of the math.atan2 () function.

Math.atan2() returns the plane Angle (radian value) between the line segment from the origin (0,0) to the point (x,y) and the positive X-axis, i.e. Math.atan2(y,x). The y and x in math.atan2 (y,x) are both distances from the dot (0,0).

Set the startX,startY coordinates when the user holds down the mouse and curX,curY coordinates when the mouse moves. The rotation Angle can be calculated by (startX,startY) and (curX,curY).

So how do we get the rotation Angle from (startX,startY) to (curX,curY)?

Step 1: Set the coordinate of mouse click to (startX,startY) :

const startY = e.clientY
const startX = e.clientX
Copy the code

The second step is to calculate the center point of the component:

// Get the component center location
const rect = this.$el.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
Copy the code

Step 3: Hold down the mouse and set the coordinate as (curX,curY) :

const curX = moveEvent.clientX
const curY = moveEvent.clientY
Copy the code

The fourth step is to calculate the corresponding angles of (startX,startY) and (curX,curY), and then subtract them to get the rotation Angle. Also, note that the math.atan2 () method returns a radian, so you need to convert radians to angles. So the complete code is:

// The Angle before rotation
const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)
// The Angle after rotation
const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)
// Get the rotation Angle. StartRotate is the initial Angle
pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore
Copy the code

Zoom in on

There is a BUG with zooming in and out of rotated components.

As you can see from the figure above, the shift occurs as you zoom in and out. And the direction of the flex and the direction of the drag is not right. This BUG is caused by the fact that the zoom in/out function was not designed with rotation in mind. So no matter how much you rotate, the zoom in and out is still the same as when you’re not rotating.

Here’s a concrete example:

As you can see from the figure above, when there is no rotation, hold down the vertex and drag up, just use y2 – y1 to get the drag distance s. In this case, add s to the original height of the component to get the new height, and update the top and left properties of the component.

Now rotate 180 degrees, and if we drag the vertex down, we expect the component to increase in height. But the calculation is the same as it would be without rotation, so the result is the opposite of what you would expect: the height of the component will be smaller. (If you don’t understand this, think of the diagram without rotation and drag it down.)

How to solve this problem? I found a solution to this from github’s snapping- Demo, which links zooming in and out to the Angle of rotation.

The solution

Here is a rectangle that has been rotated by a certain Angle. Let’s say that we now stretch it by dragging the upper left point.

Now we will analyze step by step how to get the correct size and displacement of the stretched component.

The first step is to calculate the center point of the component with the mouse down, using the coordinate (the top left property of the component remains the same no matter how many degrees of rotation) and the size of the component:

const center = {
    x: style.left + style.width / 2.y: style.top + style.height / 2,}Copy the code

The second step is to use the current click coordinate and the center point of the component to calculate the coordinate of the symmetric point of the current click coordinate:

// Get the canvas displacement information
const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()

// Now click on the coordinate
const curPoint = {
    x: e.clientX - editorRectInfo.left,
    y: e.clientY - editorRectInfo.top,
}

// Get the coordinates of the symmetric points
const symmetricPoint = {
    x: center.x - (curPoint.x - center.x),
    y: center.y - (curPoint.y - center.y),
}
Copy the code

Step 3: When pressing the upper left corner of the component for stretching, calculate the new center point of the component through the real-time coordinates and symmetry points of the current mouse:

const curPositon = {
    x: moveEvent.clientX - editorRectInfo.left,
    y: moveEvent.clientY - editorRectInfo.top,
}

const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)

// Find the midpoint coordinates between the two points
function getCenterPoint(p1, p2) {
    return {
        x: p1.x + ((p2.x - p1.x) / 2),
        y: p1.y + ((p2.y - p1.y) / 2),}}Copy the code

Since the component is rotated, you can’t calculate the component directly even if you know the xy distance it moves during stretching. Otherwise, bugs will appear, shifting or zooming in or out in the wrong direction. Therefore, we need to calculate the component without rotating it.

The fourth step, according to the known rotation Angle, the new component center point and the current mouse real-time coordinate, the newTopLeftPoint of the current mouse real-time coordinate currentPosition when the mouse is not rotated can be calculated. At the same time, newBottomRightPoint, the coordinate of the symmetric point sPoint of the component without rotation, can be calculated according to the known rotation Angle, new component center point and symmetry point.

The corresponding calculation formula is as follows:

/** * computes the coordinates of the points rotated according to the center *@param   {Object}  Point Point coordinates before rotation *@param   {Object}  Center Rotation center *@param   {Number}  Rotate Indicates the Angle of rotation@return  {Object}         After rotating the coordinates of the * * / https://www.zhihu.com/question/67425734/answer/252724399 rotation matrix formula
export function calculateRotatedPointCoordinate(point, center, rotate) {
    / rotating formula: * * * * points a center of rotation (x, y) * c (x, y) * after rotating point n (x, y) * rotation Angle theta tan?? * nx = cosine theta * (cx) ax - - sine theta cx * * (ay - cy) + ny = sine theta * (ax - cx) + cos theta * (ay - cy) + cy * /

    return {
        x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,
        y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,
    }
}
Copy the code

The formula above involves the rotation matrix of linear algebra, which is too difficult for someone who has not been to college. Fortunately, I found the reasoning process of this formula from a reply on Zhihu. Here is the original answer:

From these values, you get the new displacement of the component, top left, and the new component size. The complete code is as follows:

function calculateLeftTop(style, curPositon, pointInfo) {
    const { symmetricPoint } = pointInfo
    const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
    const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
    const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
  
    const newWidth = newBottomRightPoint.x - newTopLeftPoint.x
    const newHeight = newBottomRightPoint.y - newTopLeftPoint.y
    if (newWidth > 0 && newHeight > 0) {
        style.width = Math.round(newWidth)
        style.height = Math.round(newHeight)
        style.left = Math.round(newTopLeftPoint.x)
        style.top = Math.round(newTopLeftPoint.y)
    }
}
Copy the code

Now look at the zoom in and out after rotation:

Automatic adsorption

The autosnap is calculated based on the top left width height of the component’s four properties, which remain unchanged after the component is rotated. Therefore, no matter how many degrees the component is rotated, the adsorption time is still calculated as the time when the component is not rotated. This creates a problem, even though the component’s top left Width height property doesn’t actually change. But it has changed in appearance. Here are two identical components: one not rotated and one rotated by 45 degrees.

You can see that the height attribute of the button after rotation is not the same as the height we see from the appearance, so in this case we have the BUG of incorrect adsorption.

The solution

How to solve this problem? We need to take the size and displacement of the component after rotation to do adsorption comparison. That is, don’t compare the actual properties of the component, but the size and displacement we see.

As can be seen from the figure above, the projected length of the rotated component on the X-axis is the sum of the lengths of the two red lines. The lengths of these two red lines can be calculated using sines and cosines, the left red line using sines and the right red line using cosines:

const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
Copy the code

The same goes for height:

const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
Copy the code

With the new width and height, you can obtain the new top left property of the rotated component based on the original top left property of the component. The complete code is attached below:

translateComponentStyle(style){ style = { ... style }if(style.rotate ! =0) {
        const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
        const diffX = (style.width - newWidth) / 2
        style.left += diffX
        style.right = style.left + newWidth

        const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
        const diffY = (newHeight - style.height) / 2
        style.top -= diffY
        style.bottom = style.top + newHeight

        style.width = newWidth
        style.height = newHeight
    } else {
        style.bottom = style.top + style.height
        style.right = style.left + style.width
    }

    return style
}
Copy the code

After the repair, the adsorption can also display normally.

The cursor

The cursor and draggable direction are not correct because the eight-point cursor is fixed and does not change with the Angle.

The solution

Since 360/8 = 45, you can assign a range of 45 degrees for each direction, and each range corresponds to a cursor. An initial Angle is also set for each direction, which is the Angle of each direction of the component when it is not rotated.

pointList: ['lt'.'t'.'rt'.'r'.'rb'.'b'.'lb'.'l'].// Eight directions
initialAngle: { // The initial Angle for each point
    lt: 0.t: 45.rt: 90.r: 135.rb: 180.b: 225.lb: 270.l: 315,},angleToCursor: [ // Each range Angle corresponds to the cursor
    { start: 338.end: 23.cursor: 'nw' },
    { start: 23.end: 68.cursor: 'n' },
    { start: 68.end: 113.cursor: 'ne' },
    { start: 113.end: 158.cursor: 'e' },
    { start: 158.end: 203.cursor: 'se' },
    { start: 203.end: 248.cursor: 's' },
    { start: 248.end: 293.cursor: 'sw' },
    { start: 293.end: 338.cursor: 'w'},].cursors: {},
Copy the code

The calculation is also simple:

  1. Suppose that the component has now been rotated by an Angle a.
  2. Go through the eight directions, and take the initial Angle in each direction + a to get the current Angle B.
  3. traverseangleToCursorArray, see what range b is in, and return the corresponding cursor.

After the above three steps, you can calculate the correct cursor direction after the component rotation. The specific code is as follows:

getCursor() {
    const { angleToCursor, initialAngle, pointList, curComponent } = this
    const rotate = (curComponent.style.rotate + 360) % 360 // In case the Angle is negative, so + 360
    const result = {}
    let lastMatchIndex = -1 // The index from the previous hit Angle starts to match the next one, reducing the time complexity
    pointList.forEach(point= > {
        const angle = (initialAngle[point] + rotate) % 360
        const len = angleToCursor.length
        while (true) {
            lastMatchIndex = (lastMatchIndex + 1) % len
            const angleLimit = angleToCursor[lastMatchIndex]
            if (angle < 23 || angle >= 338) {
                result[point] = 'nw-resize'
                return
            }

            if (angleLimit.start <= angle && angle < angleLimit.end) {
                result[point] = angleLimit.cursor + '-resize'
                return}}})return result
},
Copy the code

As you can see from the GIF above, the cursor is now correctly displayed in all eight directions.

15. Copy, paste and cut

Compared to the drag and drop rotation function, copy and paste is relatively simple.

const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88
let isCtrlDown = false

window.onkeydown = (e) = > {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = true
    } else if (isCtrlDown && e.keyCode == cKey) {
        this.$store.commit('copy')}else if (isCtrlDown && e.keyCode == vKey) {
        this.$store.commit('paste')}else if (isCtrlDown && e.keyCode == xKey) {
        this.$store.commit('cut')}}window.onkeyup = (e) = > {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = false}}Copy the code

Monitor the user’s key operations and trigger corresponding operations when a specific key is pressed.

Copy operation

Use copyData in VUex to represent copied data. When the user presses CTRL + C, the current component data is deeply copied to copyData.

copy(state) {
    state.copyData = {
        data: deepCopy(state.curComponent),
        index: state.curComponentIndex,
    }
},
Copy the code

You also need to record the index of the current component in the component data, which is used in clipping.

Paste operation

paste(state, isMouse) {
    if(! state.copyData) { toast('Please select a component')
        return
    }

    const data = state.copyData.data

    if (isMouse) {
        data.style.top = state.menuTop
        data.style.left = state.menuLeft
    } else {
        data.style.top += 10
        data.style.left += 10
    }

    data.id = generateID()
    store.commit('addComponent', { component: data })
    store.commit('recordSnapshot')
    state.copyData = null
},
Copy the code

When pasting, if it is a key operation CTRL + V. Then add 10 to the top left property of the component so that it does not overlap with the original component. If you are using the right mouse button to perform the paste operation, place the copied component at the mouse click.

The shear action

cut(state) {
    if(! state.curComponent) { toast('Please select a component')
        return
    }

    if (state.copyData) {
        store.commit('addComponent', { component: state.copyData.data, index: state.copyData.index })
        if (state.curComponentIndex >= state.copyData.index) {
            // If the current component index is greater than or equal to the insert index, add one because the current component is moved back one bit
            state.curComponentIndex++
        }
    }

    store.commit('copy')
    store.commit('deleteComponent')},Copy the code

The cut operation is essentially replication, but after the replication is performed, the current component needs to be removed. After the user performs the cut operation, the user does not paste the operation and continues to perform the cut. In this case, the original data needs to be restored. So the index of the record in the copy data comes into play, and you can use the index to restore the original data to its original location.

right-clicking

Right-click operation and button operation is the same, one function two triggering pathways.

<li @click="copy" v-show="curComponent">copy</li>
<li @click="paste">paste</li>
<li @click="cut" v-show="curComponent">shear</li>

cut() {
    this.$store.commit('cut')
},

copy() {
    this.$store.commit('copy')
},

paste() {
    this.$store.commit('paste', true)
},
Copy the code

16. Data interaction

Methods a

Write a list of AJAX request apis in advance, select the API as needed when you click on the component, and fill in the parameters after you select the API. For example, the following component shows how to use Ajax requests to interact with the background:

<template>
    <div>{{ propValue.data }}</div>
</template>

<script>
export default {
    // propValue: {
    // api: {
    // request: a,
    // params,
    / /},
    // data: null
    // }
    props: {
        propValue: {
            type: Object.default: () = >{},}},created() {
        this.propValue.api.request(this.propValue.api.params).then(res= > {
            this.propValue.data = res.data
        })
    },
}
</script>
Copy the code

Way 2

Mode two is suitable for pure display components, such as an alarm component, which can display the corresponding color according to the data transmitted from the background. While editing a page, you can use Ajax to ask the background for webSocket data that the page can use:

const data = ['status'.'text'. ]Copy the code

Then add different properties for different components. For example, you have component A that binds to a property of status.

// Props: {propValue: {type: String,}, element: {type: Object,}, wsKey: {type: String, default: ",},},Copy the code

The properties of this binding are obtained in the component via wsKey. After the page is published or previewed, weboscket requests the background to put the global data on vuEX. Components can then access data through wsKey.

<template>
    <div>{{ wsData[wsKey] }}</div>
</template>

<script>
import { mapState } from 'vuex'

export default {
    props: {
        propValue: {
            type: String,},element: {
            type: Object,},wsKey: {
            type: String.default: ' ',}},computed: mapState([
        'wsData',]),</script>
Copy the code

There are many ways to interact with the background, including not only the two above, but ALSO some ideas for reference.

Released 17.

There are two ways to publish a page: one is to render component data as a single HTML page; The second is to extract a minimum runtime runtime from the project as a separate project.

For the second approach, the minimum runtime in this project is actually a preview page plus a custom component. Extract the code and package it as a separate project. When the page is published, component data is passed to the server in JSON format and a unique ID is generated for each page.

Suppose you have three pages, and the published pages generate ids A, B, and C. You only need to bring the ID with you when you visit the page, so you can get the component data for each page based on the ID.

www.test.com/?id=a
www.test.com/?id=c
www.test.com/?id=b
Copy the code

According to the need to load

If the custom components are too large, say there are dozens or even hundreds of them. At this time, you can import the custom components by import, so as to load them on demand and reduce the first screen rendering time:

import Vue from 'vue'

const components = [
    'Picture'.'VText'.'VButton',
]

components.forEach(key= > {
    Vue.component(key, () = > import(`@/custom-component/${key}`))})Copy the code

Release by version

It is possible for custom components to be updated. For example, the original component used for more than half a year, now there are functional changes, in order not to affect the original page. It is recommended to release with the version number of the component:

- v-text
  - v1.vue
  - v2.vue
Copy the code

For example, if the v-text component has two versions, you can use the version number in the left component list area:

{
  component: 'v-text'.version: 'v1'. }Copy the code

This allows you to import components based on the component version number:

import Vue from 'vue'
import componentList from '@/custom-component/component-list` componentList.forEach(component => { Vue.component(component.name, () => import(`@/custom-component/${component.name}/${component.version}`)) })Copy the code

Visual Drag and Drop series:

  • Analysis of some technical essentials of visual drag and drop component library
  • Analysis of some Technical Points of Visual Drag-and-drop Component Library (part 2)
  • Analysis of some Technical Points of Visual Drag-and-drop Component Library (3)

The resources

  • Math
  • Calculate angles with Math.atan2
  • Why can matrices be used to represent rotations of angles?
  • snapping-demo
  • vue-next-drag