Project background

The company is developing a tagging platform recently, and one of its function modules is to tag images (such as people and cars in the picture), which is simply to add a shape (rectangle, circle) to the picture. , can be moved and size Settings. Since I was learning D3 recently, I wondered if I could implement this function based on D3, and then I implemented this requirement with D3. Here is a small demo I tested:

D3. Introduction of js

As the saying goes, if you want to do a good job, you must sharpen your tools. So what is D3? Along with echarts and AntV libraries, D3 is one of the most popular data-driven visualization libraries. The difference with D3 is that instead of giving you a set of diagrams to use, D3 provides a very user-friendly comparison low-level API that is highly customizable, and you can do almost anything you want with D3. When designers come up with a nice visual icon, D3 is probably the best choice.

Implementation approach

There are two main pieces:

1, add rectangle:.

Mousedown to get a coordinate (x0, y0), mouseup to a coordinate (x1, y1), from these two points can define a rectangle in SVG (vertex coordinates (x, y), width, height).

Specifically broken down into four cases :(blue represents the mousedown point, yellow represents the mouseup point, red circle represents the vertex of the rectangle), there are vertices, there are width and height (the coordinates of two points can be easily calculated), the rectangle comes out.

2. Drag and drop

There are two cases of drag:

1. Dragging and dropping a rectangle will only change the position of the rectangle, but not the size of the rectangle.

2, drag the rectangle on the four corners of the circle, not only will change the position of the rectangle, but also change the size (grasp the vertex and drag point ok), there is a drop of elementary school calculation problem, clear analysis is very simple, do not panic.

Code practice!

1. Preparation: a canvas, insert an image, bind the event (not implemented for now), add a cross coordinate (displayed when adding rectangle, improve visual effect)

Attr (‘ XXX ‘, params) adds (with arguments) or reads (without arguments) element attributes. The syntax is a bit like jquery

Const SVG = d3. select('#container5').append(' SVG ').attr('width', this.width).attr('height', Append ('image').attr('class', 'control-img').attr('height', this.height).attr('width', this.width) .attr('href', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1601013602408&di=a2354eb5ba74fef7511e60c47cab5d97&i mgtype=0&src=http%3A%2F%2Ft8.baidu.com%2Fit%2Fu%3D3571592872%2C3353494284%26fm%3D79%26app%3D86%26f%3DJPEG%3Fw%3D1200%26h %3D1290') SVG. On ('mousemove', this.mousemove) On ('mousedown', this.mousedown) // mousedown trigger SVG. On ('mousedown', this.mousedown) // mousedown trigger SVG. Const positionXY = svg.append('g').attr('class', 'line - g) SVG. Append (" g "). Attr (' id', 'the rect - g) / / to hold rectangular container positionXY. Append (' line'). Attr (' id ', 'the line - x'). The attr (x1, 0).attr('y1', 0).attr('x2', 700).attr('y2', 0).attr('stroke', 'white').attr('stroke-width', 0) positionXY.append('line').attr('id', 'line-y').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 700).attr('stroke', 'white').attr('stroke-width', 0) positionXY.append('circle').attr('id', 'line-circle').attr('cx', -10).attr('cy', -10).attr('r', 5).attr('fill', 'red')Copy the code

** Cross coordinates: ** is the two white lines and a red dot that appear when adding a rectangle. This is very simple, the coordinates of the points are the current mouse position, and each line has two endpoints. From the coordinates of the dots, the coordinates of the endpoints of the line segment can be obtained (I gave them initial values in the above code).

Note: Appears when adding a rectangle (mouseover event listens for changes), not when dragging a rectangle or ball.

Two, add rectangle (core: start, end. Rectangle width and height, vertices derived from these two points)

SVG structure diagram:Easy to understand code

1, Mousedown: Get the first point, create rectangle assign ID, define some basic styles (border stroke, background fill), one point can’t define rectangle, need another point, another point is the coordinate of moving mouse (found in Mousemove)

mouseDown(e) { const that = this if (! This.isdrag) {// Not in drag mode const id = new Date().getTime() + "// Get a non-duplicate ID, assign an id to the rectangle const xy = [(+ e.ffsetx), (+ e.o ffsetY)] / / get the coordinates of the mouse to press the D3. Select (' # the rect - g), append (" g "). Attr (' id '` the rect - g - ${id} `), append (' the rect). Attr (" id ", `rect-g-${id}-rect`).attr('x', xy[0]).attr('y', xy[1]).attr('stroke', 'yellow').attr('fill', 'yellow').attr('fill-opacity', Start = true // Add rectangle to start (to monitor another point in moseover) this.startDom = 'rect-g-${id}-rect' // record the current id of the added rectangle.Copy the code

2, mouseomove:In mousedown we have a coordinate point xy (x1, y1) (fixed, opposite the center of the cross) and in Mousemove we have a point dot(x2, y2) (dynamic, center of the cross). These two points will help us get the vertex coordinates and width and height of the rectangle. As shown in the figure, the red circle is the vertex coordinates of the rectangle, top(x0, y0)(simple calculation can be obtained), width is math.abs (x1-x2), height is math.abs (y1-y2).

mouseMove(e) { if (! This.isdrag) {const xy = [(+ e.ffsetx), (+ e.ffty)] D3 dots and lines. The select (' # line - x). Attr (x1, 0). The attr (' y1, xy [1]). The attr (x2, 700). Attr (' y2, xy [1]). The attr (" stroke ", 'white').attr('stroke-width', 2) D3.select('#line-y').attr('x1', xy[0]).attr('y1', 0).attr('x2', xy[0]).attr('y2', 700).attr('stroke', 'white').attr('stroke-width', 2) D3.select('#line-circle').attr('cx', xy[0]).attr('r', 6).attr('cy', xy[1]).attr('fill', 'red') if (this.start) {// adding a rectangle (mousedown sets start to true) let top If (xy[0] >= this.rectData[0]) {if (xy[1] >= this.rectData[1]) {// Top = this.rectData} else {// top = right [this rectData [0], xy [1]]}} else {the if (x, y [1] > = this. RectData [1]) {/ / lower left top = [xy [0], This. RectData [1]]} else {// top = xy} D3.select(`#${this.startDom}`).attr('x', top[0]).attr('y', top[1]).attr('width', Math.abs(this.rectData[0] - xy[0])).attr('height', Math.abs(xy[1] - this.rectData[1])) } } },Copy the code

Mouseup: To finish adding the rectangle, in mouseup we just change start to false and the rectangle is fixed (mousemove will no longer dynamically monitor the rectangle after start becomes false)

mouseUp(e) { if (! This.isdrag) {this.start = false // Add rectangle action to cancel this.startdom = "// this.rectData = [] // reference mouseDown}}Copy the code

So now that I’ve added the rectangle, it looks like I’ve written all this up here but it’s pretty straightforward logic, you can see right through it. And then drag and drop

Drag and drop functionality

When entering the drag state (isDrag = true), the mouse moves over the rectangle (mouseEnter) and four small balls appear, as shown in the picture:

On (‘ mouseEnter ‘, function(){})

** Note: ** The entire code for adding a rectangle is not posted here, because the rest of the code for adding a rectangle was shown earlier

D3.select('#rect-g').append('g').attr('id', `rect-g-${id}`).append('rect').attr('id', `rect-g-${id}-rect`) .attr('x', Xy [0]). Attr (' y ', x, y [1]). The attr (" stroke ", "yellow"). Attr (' fill ', 'yellow'). Attr (' the fill opacity - ', 0.1) on (" mouseenter ", function() { if (that.isDrag) { D3.select(this).attr('cursor', Const xy = [(+ d3.select (this).attr('x'))), (+ D3. The select (this). Attr (' y ')] / / vertex coordinates const wh = [(+ D3. The select (this). Attr (' width ')), (+ D3. The select (this). Attr (' height ')] / / rectangle high const wide dots = [xy, [xy [0] + wh [0], xy [1]], [xy [0] + wh [0], y + wh [1] [1]]. [xy[0], Select (this)._groups[0]. Parentnode. id // select(this)._groups[0] D3.select(`#${id}`).selectAll('circle').data(dots).enter().append('circle') .attr('cx', d => d[0]).attr('cy', d => d[1]).attr('r', 6).attr('fill', 'yellow').attr('parent', id) .on('mouseenter', Function () {that.drag (d3.select (this))}) that.drag (d3.select (this))}})Copy the code

Add four circles (radius) to the four corners of the rectangle after entering the drag state (isDrag = true). The four center coordinates are the coordinates of the four corners of the rectangle: (x+width, y), (x+width, y+height), (x, y+height)

That.Drag(XXX) adds the current element to the Drag instance created in D3.

Ok, now for our most important drag method:

Step 1: Build the drag instance

Drag instances are used as a tool that can be created at a very early age by calling the createDrag() method when creating an SVG canvas, which uses D3’s drag constructor to implement drag

Const SVG = d3. select('#container5').append(' SVG ').attr('width', this.width).attr('height', this.height) this.createDrag()Copy the code
Note: There are two types of drag: 1, drag rectangle (only change rectangle position, width and height, position will change),2, drag ball (width and height, position will change) createDrag method is the most complex method, but it is a lot of code, the logic is very simple, do not worry.

*** Drag the rectangle (this is easy, do it first) **

CreateDrag () {let color, widget const that = this this.drag = d3.drag () Select (this).attr('fill') widget = d3. select(this).attr('fill') widget = d3. select(this).attr('fill', 'lime') const dot = [(+e.sourceEvent.offsetX), (+ e.sourceEvent.offsety)] const id = Widget._groups [0][0].parentNode.id (widget. _groups [0] [0]. LocalName = = = 'the rect') {/ / when drag and drop the object for rectangular widget. The attr (' o - x ', dot [0] - (+ widget. Attr (' x '))). The attr (' o - y ' dot[1] - (+widget.attr('y'))) D3.select(`#${id}`).selectAll('circle').each(function() { const origin = [dot[0] - (+D3.select(this).attr('cx')), dot[1] - (+D3.select(this).attr('cy'))] D3.select(this).attr('o-x', Origin [0]).attr('o-y', origin[1])})}}).on('drag', function(e) {const dot = [(+ e.sourceEvent.offsetx), If (widget._groups[0][0].localName === 'rect') {// Drop the object to a rectangle const id = Widget. _groups [0] [0]. ParentNode. Id / / to get id of the parent element widget. The attr (' x ', dot [0] - (+ widget. Attr (' o - x))). The attr (' y ' Dot [1] - [+ widget. Attr (' o - y))) / / update the rectangle vertex information D3. Select (` # ${id} `) selectAll (' circle '). Each (function () { D3.select(this).attr('cx', dot[0] - (+D3.select(this).attr('o-x'))).attr('cy', dot[1] - (+D3.select(this).attr('o-y'))) }) } }) .on('end', function(e) { widget.attr('fill', Color) widget = null // Drag element set to null})},Copy the code
The parsing step by step

D3.drag() is a drag and drop method built into D3, providing ‘start’: start drag, ‘drag’ : drag, ‘end’ : Drag and drop completes the three hook functions. In the previous tutorial, when entering the Drag state (isDrag=true), add the current element to the Drag method (that.drag ()) when the mouse moves over the rectangle (mouseEnter).

Note: there is a lot of (+ XXXX) in the above code, because most of the time when we get the value of the attribute is a string, we need to add and subtract them, and convert them to a number by (+) so that there are no bugs

Widget: We assign the dragged element to the widget on(‘start’) so that we can manipulate the element through the widget

As mentioned above, when we drag a rectangle, only the position changes, the width and height stays the same, that is, only the vertex coordinates (x, y) change. How do we know the real-time vertex coordinates during drag? Easy, there are so many ways to do this, I think the reader can think of many, many, many, I will tell you one.

As soon as WE start dragging, we’re going to get a coordinate point (x0, y0), so I’m going to get the relative position of the vertex coordinates (x, y)

Is that it? No, in addition to changing the position of the rectangle, remember that the four corners of the rectangle also need to change the position of the four circles, using the same method as this, recording the positions of the four balls relative to (x0, y0).Maybe some friends see my above code has lost effort, mainly is not written neatly, but this is secondary, the idea on the line. There are many, many ways to find the position after the move, and you can do it in a different way.

Next step: Drag the ball, this will cause the size of the rectangle to change, a bit more complicated than the above. Don’t panic, this is the last step, then you can go home for dinner!!

Take a look at the results first:

Notice the pattern: when you select to drag a ball, its position (x0, y0) of the diagonal ball (also an Angle of the rectangle) remains unchanged, and the coordinates of the other real-time moving point (x1, y1) are also available in on(‘drag’) at !!!! Have you found that this is exactly the same as the beginning of the add rectangle !!!!! (x0, y0) this is the same as mousedown to get the points, the change points are from mousemove, and the rectangle comes out. We can get the vertex and width of the rectangle by using two points. We just need to dynamically change the vertex and width properties of the rectangle as we did when we added the rectangle

No more nonsense, on the code:

CreateDrag () {let color, widget const that = this this.drag = d3.drag ().on('start', Select (this).attr('fill') widget = d3. select(this).attr('fill') widget = d3. select(this).attr('fill', 'lime') const dot = [(+e.sourceEvent.offsetX), (+ e.sourceEvent.offsety)] const id = Widget._groups [0][0].parentNode.id (widget._groups[0][0].localName === 'circle') {  x, y, width, height const cxy = [(+D3.select(`#${id}-rect`).attr('x')), (+D3.select(`#${id}-rect`).attr('y')), (+D3.select(`#${id}-rect`).attr('width')), (+D3.select(`#${id}-rect`).attr('height'))] const dot = [(+D3.select(this).attr('cx')), (+ D3. The select (this). Attr (' cy)] / / drag and drop point diagonal the vertex of the if (dot [0] > cxy [0]) {if (dot [1] > cxy [1]) {/ / lower right that topDot = Cxy [cxy [0], [1]]} else {/ / upper right that. TopDot = [cxy [0]. Cxy cxy [1] + [3]]}} else {the if (dot [1] > cxy [1]) {/ / lower left that. TopDot = [dot [0] + cxy [2]. Cxy [1]]} else {/ / upper left that topDot = [cxy cxy [0] + [2], cxy cxy [1] + [3]]}}}}) on (' drag ', Function (e) {const dot = [(+ e.sourceEvent.offsetx), If (widget._groups[0][0]. LocalName === 'circle') {if (widget._groups[0]. LocalName === 'circle') { Let top if (dot[0] >= that.topdot [0]) {if (dot[1] >= that.topdot [1]) {// top = that.topdot} else {// top right Top = [that topDot [0], dot [1]]}} else {the if (dot [1]. > = that topDot [1]) {/ / lower left top = [dot [0], TopDot [1]]} else {// top = dot}} const id = widget.attr('parent') // Get the id of the parent element // update the rectangle D3.select(`#${id}-rect`).attr('x', top[0]).attr('y', top[1]) .attr('width', Math.abs(that.topDot[0] - dot[0])).attr('height', Math.abs(dot[1] - that.topdot [1]) D3. Select (' #${id} ').selectAll('circle').remove() const rect = D3. Select (` # ${id} - the rect `) const circles = [/ / get the coordinates of the four dot [(+ the rect. Attr (' x ')), (+ the rect. Attr (' y ')], [(+ the rect. Attr (' x ')), (+rect.attr('y')) + (+rect.attr('height'))], [(+rect.attr('x')) + (+rect.attr('width')), (+rect.attr('y'))], [(+rect.attr('x')) + (+rect.attr('width')), (+rect.attr('y')) + (+rect.attr('height'))] ] D3.select(`#${id}`).selectAll('circle').data(circles).enter().append('circle') .attr('cx', d => d[0]).attr('cy', d => d[1]).attr('r', 6).attr('fill', 'yellow').attr('parent', id) .on('mouseenter', function() { that.Drag(D3.select(this)) }) } }) .on('end', function(e) { widget.attr('fill', Color) widget = null // Drag element set to null})},Copy the code

The above code seems to be a bit long ah, don’t be afraid, these logic is very simple, elementary school addition, subtraction, multiplication and division, do not believe:

start: Began to drag There what we did, drag a ball, we can find the ball diagonal coordinates (rectangular) the coordinates of one corner, red dots on the graph (the diagram shows a only, there are three, so need to decide, how to judge by your current drag point compared with rectangular vertices available), you can refer to the top of the rectangular logic.

Drag: What are we doing here? Calculate real-time vertex coordinates, width and height, and the logic is the same as when adding the rectangle, get the new vertex and width and height and then copy it. Instead of dragging the rectangle, we did not change the position of the four balls by changing the coordinates of the center of the balls. Instead, we removed the four balls and rebuilt the four balls. And people say, how do I know the central coordinates of my new ball? I’ve got the rectangle, so I can just as easily get the four angular coordinates of the rectangle, the center coordinates of the four balls are the coordinates of the four angles.

This Demo uses vUE, this is the address of the Vue file, you can test it yourself.

Conclusion:

Although this is implemented in D3, you could have done it in Canvas or something else with the same idea. This is my first time to write a nuggets article, for my poor Chinese level really eat the strength to use, this is a simple basic version, if more than 3 likes, 2.0 will soon come.

And finally, is it weird that you don’t play and write shit on The Fourth of July, because because I lost my ID…

Ha ha, I wish everyone a happy National Day