Rough. Js is a freehand style graphics library that provides the ability to draw basic graphics such as:

Although the author is a rough man, but to this lovely things have no resistance, the use of the library itself is very simple, nothing to say, but it only draw ability, no interaction ability, so the use of the scene is limited, first to use it to draw an example of graphics:

import rough from 'roughjs/bundled/rough.esm.js'

this.rc = rough.canvas(this.$refs.canvas)
this.rc.rectangle(100.150.300.200, {
    fillweight: 0.roughness: 3
})
this.rc.circle(195.220.40, {
    fill: 'red'
})
this.rc.circle(325.220.40, {
    fill: 'red'
})
this.rc.rectangle(225.270.80.30, {
    fill: 'red'.fillweight: 5
})
this.rc.line(200.150.150.80, { roughness: 5 })
this.rc.line(300.150.350.80, { roughness: 2 })
Copy the code

The effect is as follows:

Is not a bit silly, the main content of this article is to take you to manually achieve the above graphics, the final effect preview: lxqnsys.com/#/demo/hand… . Without further comment, see you in the code.

Line segment

Everything is based on line segments, so let’s first look at how to draw a line segment. If you look carefully at the figure above, you will find that the hand-drawn line segment is actually composed of two curved line segments. The curve can be drawn using Bezier curve, and here the third Bezier curve is used, so the remaining problem is to find the coordinates of the starting point, the end point and the two control points.

Bessel curves can be tried at cubic-bezier.com/.

First of all, the starting point of a line segment and the end point of a line segment we add some random value to it, and the random value is between, say, [-2,2], and we can also relate that range to the length of the line segment, so the longer the segment, the greater the random value.

// A straight line is a curve
_line (x1, y1, x2, y2) {
    let result = []
    / / starting point
    result[0] = x1 + this.random(-this.offset, this.offset)
    result[1] = y1 + this.random(-this.offset, this.offset)
    / / the end
    result[2] = x2 + this.random(-this.offset, this.offset)
    result[3] = y2 + this.random(-this.offset, this.offset)
}
Copy the code

The next two control points are defined within the rectangle where the line segment is located:

_line (x1, y1, x2, y2) {
    let result = []
    / / starting point
    // ...
    / / the end
    // ...
    // Two control points
    let xo = x2 - x1
    let yo = y2 - y1
    let randomFn = (x) = > {
        return x > 0 ? this.random(0, x) : this.random(x, 0)
    }
    result[4] = x1 + randomFn(xo)
    result[5] = y1 + randomFn(yo)
    result[6] = x1 + randomFn(xo)
    result[7] = y1 + randomFn(yo)
    return result
}
Copy the code

Then plot the curve generated above:

// Draw a freehand line segment
line (x1, y1, x2, y2) {
	this.drawDoubleLine(x1, y1, x2, y2)
}

// Draw two curves
drawDoubleLine (x1, y1, x2, y2) {
    // Draw the two curves generated
    let line1 = this._line(x1, y1, x2, y2)
    let line2 = this._line(x1, y1, x2, y2)
    this.drawLine(line1)
    this.drawLine(line2)
}

// Draw a single curve
drawLine (line) {
    this.ctx.beginPath()
    this.ctx.moveTo(line[0], line[1])
    // bezierCurveTo method: the first two points are the control points and the third point is the end point
    this.ctx.bezierCurveTo(line[4], line[5], line[6], line[7], line[2], line[3])
    this.ctx.strokeStyle = '# 000'
    this.ctx.stroke()
}
Copy the code

The effect is as follows:

But if you try it a few times, you’ll find that it’s too far off, too bent:

Nothing like what a person with normal hands could do. Try the bezier Curve site above a few times and you’ll find that the closer the two control points are off line, the less the curve bends:

Therefore, we need to find a point near the line segment as the control point. First, a random abscissa point can be randomly selected. Then, we can calculate the ordinate point corresponding to the abscissa point on the line segment and add or subtract a random value to the ordinate point.

_line (x1, y1, x2, y2) {
    let result = []
    // ...
    // Two control points
    let c1 = this.getNearRandomPoint(x1, y1, x2, y2)
    let c2 = this.getNearRandomPoint(x1, y1, x2, y2)
    result[4] = c1[0]
    result[5] = c1[1]
    result[6] = c2[0]
    result[7] = c2[1]
    return result
}

// Calculate a random point near the line segment where two points are connected
getNearRandomPoint (x1, y1, x2, y2) {
    let xo, yo, rx, ry
    // Line segments perpendicular to the X axis are treated specially
    if (x1 === x2) {
        yo = y2 - y1
        rx = x1 + this.random(-2.2)// Find a random point near the abscissa
        ry = y1 + yo * this.random(0.1)// Select a random point on the line segment
        return [rx, ry]
    }
    xo = x2 - x1
    rx = x1 + xo * this.random(0.1)// Find a random abscissa
    ry = ((rx - x1) * (y2 - y1)) / (x2 - x1) + y1// Find the equation of the line by using the two-point formula
    ry += this.random(-2.2)// Add a random value to the ordinate
    return [rx, ry]
}
Copy the code

Look at the effect:

Of course, compared with Rough. Js is not good enough, you can have a look at the source code, anyway, I do not understand, too many control variables, there is no comment.

Polygon & rectangle

A polygon is a sequence of points connected end to end, traversing the vertices and calling the method that draws a line segment:

// Draw a hand-painted polygon
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    let len = points.length
    for (let i = 0; i < len - 1; i++) {
        this.line(points[i][0], points[i][1], points[i + 1] [0], points[i + 1] [1])}// End to end
    this.line(points[len - 1] [0], points[len - 1] [1], points[0] [0], points[0] [1])}Copy the code

A rectangle is a special case of a polygon. The four angles are all right angles. The general parameters are the x-coordinate, y-coordinate, width of the rectangle, and height of the rectangle at the top left corner:

// Draw a hand-drawn rectangle
rectangle (x, y, width, height, opt = {}) {
    let points = [
        [x, y],
        [x + width, y],
        [x + width, y + height],
        [x, y + height]
    ]
    this.polygon(points, opt)
}
Copy the code

round

So what do you do with a circle? Well, first of all, you know that you can approximate a circle by using polygons, as long as you have enough sides of a polygon, it will look round enough, but you don’t want it to be too round, so let’s just restore it to a polygon, and we’ve already talked about polygons. Restoring polygons is easy. For example, if we want to make a circle decagon (which you can also relate to the circumference of the circle), then the radian of each side would be 2* Math.pi /10, and then use math.cos and Math.sin to calculate the positions of the vertices. Finally, call the draw polygon method again to draw:

// Draw a hand-painted circle
circle (x, y, r) {
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount
    let points = []
    for (let angle = 0; angle < 2 * Math.PI; angle += step) {
        let p = [
            x + r * Math.cos(angle),
            y + r * Math.sin(angle)
        ]
        points.push(p)
    }
    this.polygon(points)
}
Copy the code

The effect is as follows:

As you can see, the effect is just so-so, even with more edges it doesn’t look like it:

If you connect them directly with normal line segments, they will be a proper polygon, which will not work, so the core is to turn the line segments into random arc. First, to increase the randomness, we add a random increment to the radius of the circle and each vertex:

circle (x, y, r) {
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount
    let points = []
    let rx = r + this.random(-r * 0.05, r * 0.05)
    let ry = r + this.random(-r * 0.05, r * 0.05)
    for (let angle = 0; angle < 2 * Math.PI; angle += step) {
        let p = [
            x + rx * Math.cos(angle) + this.random(-2.2),
            y + ry * Math.sin(angle) + this.random(-2.2)
        ]
        points.push(p)
    }
}
Copy the code

The next question again to calculate two bezier control point, first, because arc affirmation is to go to outside convex polygon, according to the nature of the bezier curve of two control points must be in line outside the two endpoints of a line itself can be used to calculate directly if I tried it on, difficult to deal with, may require special handling different point of view, So we refer to rough.js to interval one point:

Such as above the polygon we grab a segment to BC, for point b is a, a point on the next point is c, b and c respectively cut a horizontal ordinate, the difference between the control point c1, the same is true for other points, finally work out the control point will be on the outside, now also sent a control point, we don’t let the point c idle, Add the difference of two points to it as well:

We can see that the control points C2 and C1 of point C are on the same side, so the curve is clearly drawn in the same direction:

Let’s make it symmetric by subtracting the point c from the point c:

This still doesn’t work:

The reason is simple, the control points are too far away, so we add a little less difference, and the final code is as follows:

circle (x, y, r) {
    // ...
    let len = points.length
    this.ctx.beginPath()
    // Move the starting point of the path to the first point
    this.ctx.moveTo(points[0] [0], points[0] [1])
    this.ctx.strokeStyle = '# 000'
    for (let i = 1; i + 2 < len; i++) {
        let c1, c2, c3
        let point = points[i]
        // Control point 1
        c1 = [
            point[0] + (points[i + 1] [0] - points[i - 1] [0) /5,
            point[1] + (points[i + 1] [1] - points[i - 1] [1) /5
        ]
        // Point 2
        c2 = [
            points[i + 1] [0] + (point[0] - points[i + 2] [0) /5,
            points[i + 1] [1] + (point[1] - points[i + 2] [1) /5
        ]
        c3 = [points[i + 1] [0], points[i + 1] [1]]
        this.ctx.bezierCurveTo(
            c1[0],
            c1[1],
            c2[0],
            c2[1],
            c3[0],
            c3[1])}this.ctx.stroke()
}
Copy the code

We only added a fifth of the difference, I tried it, between 5-7 is the most natural, and rough.js added a sixth.

It doesn’t end there. First of all, there’s a gap in the circle, for the simple reason that I + 2 < len causes the last point to be disconnected, and the first part to be disconnected, and the first part to be unnaturally straight, because we started at the first point, But our first segment of the curve already ends at the third point, so let’s move our path to the second point:

this.ctx.moveTo(points[1] [0], points[1] [1])
Copy the code

This makes the gap even bigger:

The red is the first two points and the blue is the last point. In order to connect to the second point we need to append the first three points of the vertex list to the end of the list:

// Append the first three points to the end of the list
points.push([points[0] [0], points[0] [1]], [points[1] [0], points[1] [1]], [points[2] [0], points[2] [1]])
let len = points.length
this.ctx.beginPath()
// ...
Copy the code

The effect is as follows:

Again, no one should be able to connect the ends of the circle perfectly with their bare hands, so we can’t make the second point exactly the same as the original point, we have to add a little offset:

let end = [] // Handle the last point and make it randomly offset from the original point
let radRandom = step * this.random(0.1.0.5)// Make the point a little ahead of the curve, which means it's overdrawn, or a negative number, which means it's not even connected, but it's ugly
end[0] = x + rx * Math.cos(step + radRandom)// The last point to connect is actually the second point in the list, so the Angle is step instead of 0
end[1] = y + ry * Math.sin(step + radRandom)
points.push(
    [points[0] [0], points[0] [1]],
    [end[0], end[1]],
    [points[2] [0], points[2] [1]])let len = points.length
this.ctx.beginPath()
/ /...
Copy the code

The last point to optimize is the starting point or the end point. Generally, when we draw the circle freehand, we start from the top, because 0 degrees is in the positive X-axis direction, so we can subtract about Math.PI/2 to move the starting point to the top. The complete code is as follows:

drawCircle (x, y, r) {
    // Round polygon
    let stepCount = 10
    let step = (2 * Math.PI) / stepCount// The Angle corresponding to one side of the polygon
    let startOffset = -Math.PI / 2 + this.random(-Math.PI / 4.Math.PI / 4)// Start offset Angle
    let points = []
    let rx = r + this.random(-r * 0.05, r * 0.05)
    let ry = r + this.random(-r * 0.05, r * 0.05)
    for (let angle = startOffset; angle < (2 * Math.PI + startOffset); angle += step) {
        let p = [
            x + rx * Math.cos(angle) + this.random(-2.2),
            y + ry * Math.sin(angle) + this.random(-2.2)
        ]
        points.push(p)
    }
    // Line segment curve
    let end = [] // Handle the last point and make it randomly offset from the original point
    let radRandom = step * this.random(0.1.0.5)
    end[0] = x + rx * Math.cos(startOffset + step + radRandom)
    end[1] = y + ry * Math.sin(startOffset + step + radRandom)
    points.push(
        [points[0] [0], points[0] [1]],
        [end[0], end[1]],
        [points[2] [0], points[2] [1]])let len = points.length
    this.ctx.beginPath()
    this.ctx.moveTo(points[1] [0], points[1] [1])
    this.ctx.strokeStyle = '# 000'
    for (let i = 1; i + 2 < len; i++) {
        let c1, c2, c3
        let point = points[i]
        let num = 6
        c1 = [
            point[0] + (points[i + 1] [0] - points[i - 1] [0]) / num,
            point[1] + (points[i + 1] [1] - points[i - 1] [1]) / num
        ]
        c2 = [
            points[i + 1] [0] + (point[0] - points[i + 2] [0]) / num,
            points[i + 1] [1] + (point[1] - points[i + 2] [1]) / num
        ]
        c3 = [points[i + 1] [0], points[i + 1] [1]]
        this.ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], c3[0], c3[1])}this.ctx.stroke()
}
Copy the code

In the end, you can also draw twice as the line segment above. The combined effect is as follows:

The circle is done, the ellipse is similar, after all, the circle is a special case of the ellipse, by the way, the approximate circumference of the ellipse formula is as follows:

fill

Style 1

Let’s start with a simple fill:

The four sides of the rectangle we drew above are disconnected. If the path is not closed, we cannot directly call the canvas fill method, so we need to connect the four curves from the beginning to the end:

// Draw a hand-painted polygon
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // Add the fill method
    let lines = this.closeLines(points)
    this.fillLines(lines, opt)
    
    / / stroke
    let len = points.length
    // ...
}
Copy the code

The closeLines method is used to close vertices into curves:

// Convert the vertices of the polygon into closed line segments connected end to end
closeLines (points) {
    let len = points.length
    let lines = []
    let lastPoint = null
    for (let i = 0; i < len - 1; i++) {
        // The _line method, which is implemented above, turns a straight segment into a curve
        let arr = this._line(
            points[i][0],
            points[i][1],
            points[i + 1] [0],
            points[i + 1] [1]
        )
        lines.push([
            lastPoint ? lastPoint[2] : arr[0].// If the previous point exists, use the end of the previous point as the beginning of the point
            lastPoint ? lastPoint[3] : arr[1],
            arr[2],
            arr[3],
            arr[4],
            arr[5],
            arr[6],
            arr[7]
        ])
        lastPoint = arr
    }
    // Close both ends
    let arr = this._line(
        points[len - 1] [0],
        points[len - 1] [1],
        points[0] [0],
        points[0] [1]
    )
    lines.push([
        lastPoint ? lastPoint[2] : arr[0],
        lastPoint ? lastPoint[3] : arr[1],
        lines[0] [0].// The end point is the beginning of the first line segment
        lines[0] [1],
        arr[4],
        arr[5],
        arr[6],
        arr[7]])return lines
}
Copy the code

We have a line segment, we just walk through it and call fill:

// Fill the polygon
fillLines (lines, opt) {
    this.ctx.beginPath()
    this.ctx.fillStyle = opt.fillStyle
    for (let i = 0; i + 1 < lines.length; i++) {
        let line = lines[i]
        if (i === 0) {
            this.ctx.moveTo(line[0], line[1])}this.ctx.bezierCurveTo(
            line[4],
            line[5],
            line[6],
            line[7],
            line[2],
            line[3])}this.ctx.fill()
}
Copy the code

The effect is as follows:

The circle is even simpler, and itself is almost closed, as long as we remove the special processing logic for the last point:

// Remove the following lines of code and use the original dots
let end = []
let radRandom = step * this.random(0.1.0.5)
end[0] = x + rx * Math.cos(startOffset + step + radRandom)
end[1] = y + ry * Math.sin(startOffset + step + radRandom)
Copy the code

Styles 2,

The second kind of filling is a little bit more complicated. For example, the simplest kind of filling is just a bunch of slanted line segments, but the problem is how to determine the end points of these lines. Rectangles can be calculated with violence, but what about irregular polygons, so you need to find a general method.

Fill the most violent method is to determine whether each point inside the polygon, but the amount of calculation is too large, I checked the polygon fill, probably there are two kinds of algorithms: scan line filling and seed filling, scan line fill more popular, Rough, js is also this kind of method, so the next introduction of this algorithm.

Scan line filling is very simple, is a line (horizontal) start up from the base of the polygon scan, so each scan line and polygon intersection, the same scan line and polygon intersection point between the region is filled, then the problem comes, how to determine the intersection point, and how to judge the two points of intersection between belongs to inside the polygon.

About the calculation of intersection point, first we of the y coordinates of intersections is known, is the y coordinate scan lines, so only ask the x, know two endpoints of a line segment coordinates, you can find out equation of the straight line, and then calculate, but have a more simple way, is the use of correlation, also is to know that a point on the line, Its adjacent points can be easily calculated according to this point. The derivation process is as follows:

// Let's set the equation of the line
y = kx + b
// If c(x3, y3) and the y-coordinate of point D is the y-coordinate of point C +1, d (x4, y3 +1), then we want to solve for x4
y3 = kx3 + b/ / 1
y3 + 1 = kX4 + b/ / 2
// Insert equation 1 into Equation 2
kx3 + b + 1 = kX4 + b
kx3 + 1 = kX4/ / about to b
X4 = x3 + 1 / kDivide both sides by k
// So the y-coordinate plus 1, and the x-coordinate is the last point's x-coordinate plus the inverse of the slope of the line
A (x1, y1), b (x2, y2), k (x2, y2)
k = (y2 - y1) / 
// The inverse of the slope is going to be the same thing
1/k = (x2 - x1) / (y2 - y1)
Copy the code

So we start at one end of the segment, and we can compute all the points along the segment.

Detailed algorithm is introduced and the derivation process can take a look at this PPT:wenku.baidu.com/view/4ee141… Next, we will directly look at the implementation process of the algorithm.

Just a quick introduction to some nouns:

1. The side table ET

The edge table ET, an array, contains information about all sides of the polygon. The information stored for each side is: the maximum value ymax and the minimum value ymin of the edge y, the x value xi of the lowest point of the edge, and the reciprocal of the slope dx of the side. If ymin is the same, it increases by xi. If xi is the same, it can only look at ymax. If ymax is the same, it means that the two edges overlap.

2. Active edge table AET

It is also an array that holds information about the edges that intersect with the current scan line. It changes as the scan line is scanned, removing the ones that don’t intersect and adding the ones that intersect. The edges in this table are sorted by increasing xi.

For example, the following polygon ET table order is:

// ET
[p1p5, p1p2, p5p4, p2p3, p4p3]
Copy the code

The algorithm steps are as follows:

1. Create an ET table edgeTable based on the vertex data of polygons and sort the table in the preceding order.

2. Create an empty AET table activeEdgeTable.

3. Start the scan, y= the y value of the lowest point of the polygon, i.e. ActiveEdgeTable [0].ymin;

4. Repeat the following steps until both the ET and AET tables are empty:

(1) Take the edges that intersect the current scan line from the ET table and add them to the AET table, again in the order mentioned above

(2) The xi value of edge information in AET table was extracted in pairs and filled in between each pair

(3) Delete the last scanned edge from the AET table, that is, y >= ymax

(4) Update the xi of the remaining edge information in AET table, i.e. Xi = xi + dx

(5) Update the y of the scan line, i.e., y = y + 1

It doesn’t look too hard. To convert it into code, create the following table ET:

// Create sort edge table ET
createEdgeTable (points) {
    / / side table ET
    let edgeTable = []
    // Copy the first point to the end of the line to close the polygon
    let _points = points.concat([[points[0] [0], points[0] [1]]])
    let len = _points.length
    for (let i = 0; i < len - 1; i++) {
        let p1 = _points[i]
        let p2 = _points[i + 1]
        // Filter out lines parallel to the X axis. See the above PPT link
        if (p1[1] !== p2[1]) {
            let ymin = Math.min(p1[1], p2[1])
            edgeTable.push({
                ymin,
                ymax: Math.max(p1[1], p2[1]),
                xi: ymin === p1[1]? p1[0] : p2[0].// The x value of the lowest vertex
                dx: (p2[0] - p1[0]) / (p2[1] - p1[1]) // The reciprocal of the slope of the line segment}}})// Sort the edge list
    edgeTable.sort((e1, e2) = > {
        // Sort by ymin increments
        if (e1.ymin < e2.ymin) {
            return -1
        }
        if (e1.ymin > e2.ymin) {
            return 1
        }
        // If ymin is the same, increase by xi
        if (e1.xi < e2.xi) {
            return -1
        }
        if (e1.xi > e2.xi) {
            return 1
        }
        // if xi is the same, we can only look at ymax
        // ymax is the same, indicating that the two edges overlap
        if (e1.ymax === e2.ymax) {
            return 0
        }
        // If there is no overlap, then sort by yamX incrementing
        if (e1.ymax < e2.ymax) {
            return -1
        }
        if (e1.ymax > e2.ymax) {
            return 1}})return edgeTable
}
Copy the code

Next, scan:

scanLines (points) {
    if (points.length < 3) {
        return[]}let lines = []
    // Create sort edge table ET
    let edgeTable = this.createEdgeTable(points)
    // Active edge table AET
    let activeEdgeTable = []
    // Start the scan, starting at the lowest point of the polygon
    let y = edgeTable[0].ymin
    // The end point of the loop is that both tables are empty
    while (edgeTable.length > 0 || activeEdgeTable.length > 0) {
        // Add the edge of the current scan line from the ET table to the AET table
        if (edgeTable.length > 0) {
            // Add edges from the current ET table that intersect the scan line to the AET table
            for (let i = 0; i < edgeTable.length; i++) {
                // If the interval between the scan lines is larger, it is possible that the whole line segment with small height difference will be skipped, resulting in an infinite loop, which should be considered
                if (edgeTable[i].ymin <= y && edgeTable[i].ymax >= y || edgeTable[i].ymax < y) {
                    let removed = edgeTable.splice(i, 1) activeEdgeTable.push(... removed) i-- } } }Select * from AET where y=ymax
        activeEdgeTable = activeEdgeTable.filter((item) = > {
            return y < item.ymax
        })
        // Order xi from smallest to largest
        activeEdgeTable.sort((e1, e2) = > {
            if (e1.xi < e2.xi) {
                return -1
            } else if (e1.xi > e2.xi) {
                return 1
            } else {
                return 0}})// If there are active edges, fill the area between the active edges
        if (activeEdgeTable.length > 1) {
            // Fill two edges at a time
            for (let i = 0; i + 1 < activeEdgeTable.length; i += 2) {
                lines.push([
                    [Math.round(activeEdgeTable[i].xi), y],
                    [Math.round(activeEdgeTable[i + 1].xi), y]
                ])
            }
        }
        // Update the xi of the active edge
        activeEdgeTable.forEach((item) = > {
            item.xi += item.dx
        })
        // Update the scan line y
        y += 1
    }
    return lines
}
Copy the code

The code is actually a translation of the above algorithm process, it is not difficult to understand the algorithm code, in the polygon method call this method:

// Draw a hand-painted polygon
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // Add the fill method
    let lines = this.scanLines(points)
    lines.forEach((line) = > {
        this.drawDoubleLine(line[0] [0], line[0] [1], line[1] [0], line[1] [1] and {color: opt.fillStyle
        })
    })
    
    / / stroke
    let len = points.length
    // ...
}
Copy the code

Take a look at the final fill:

The effect is already there, but it’s too dense, because our scan line is adding 1 at a time, let’s try adding more:

scanLines (points) {
    // ...
    
    // Let the scan line add 10 at a time
    let gap = 10
    // Update the xi of the active edge
    activeEdgeTable.forEach((item) = > {
        item.xi += item.dx * gap// Why do I have to multiply the inverse of the slope by 10
    })
    // Update the scan line y
    y += gap
    
    // ...
}
Copy the code

By the way, make the width of the line segment thicker, as follows:

You can also alternate the beginning and end of a line segment to create a single stroke:

The concrete implementation can go to the source code to see, next we look at the last problem, is to make the fill line a little Angle, currently are horizontal. To tilt the fill line, we can first rotate the graph at an Angle, so that the scanned line is still horizontal, and then rotate the graph and the fill line together again to get the tilted line.

The figure above shows that the graph is rotated counterclockwise and then scanned forward, and the figure below shows that the graph and the filling line are rotated clockwise and back.

The rotation of the graph is the rotation of the vertices, so it becomes a question of finding the position of a point rotated by a given Angle, so let’s derive that.

The original Angle of point (x,y) in the figure above is a, and the length of the line segment is r. Calculate the coordinates (x1,y1) after the rotation Angle b:

x = Math.cos(a) * r/ / 1
y = Math.sin(a) * r/ / 2

x1 = Math.cos(a + b) * r
y1 = Math.sin(a + b) * r

// Cosine of a plus b, sine of a plus b
x1 = (Math.cos(a) * Math.cos(b) - Math.sin(a) * Math.sin(b)) * r/ / 3
y1 = (Math.sin(a) * Math.cos(b) + Math.cos(a) * Math.sin(b)) * r/ / 4

// Substitute Equations 1 and 2 for equations 3 and 4
Math.cos(a) = x / r
Math.sin(a) = y / r
x1 = ((x / r) * Math.cos(b) - (y / r) * Math.sin(b)) * r
y1 = ((y / r) * Math.cos(b) + (x / r) * Math.sin(b)) * r
/ / about to r
x1 = x * Math.cos(b) - y * Math.sin(b)
y1 = y * Math.cos(b) + x * Math.sin(b)
Copy the code

From this we can obtain the function to find the coordinates of a point rotated by a specified Angle:

getRotatedPos (x, y, rad) {
    return [
        x: x * Math.cos(rad) - y * Math.sin(rad),
        y: y * Math.cos(rad) + x * Math.sin(rad)
    ]
}
Copy the code

With this function we can rotate the polygon:

// Draw a hand-painted polygon
polygon (points = [], opt = {}) {
    if (points.length < 3) {
        return
    }
    // Rotate the polygon before scanning
    let _points = this.rotatePoints(points, opt.rotate)
    let lines = this.scanLines(_points)
    // After scanning the resulting line segment, we rotate it by the opposite Angle
    lines = this.rotateLines(lines, -opt.rotate)
    lines.forEach((line) = > {
        this.drawDoubleLine(line[0] [0], line[0] [1], line[1] [0], line[1] [1] and {color: opt.fillStyle
        })
    })
    
    / / stroke
    let len = points.length
    // ...
}

// Rotate the list of vertices
rotatePoints (points, rotate) {
    return points.map((item) = > {
        return this.getRotatedPos(item[0], item[1], rotate)
    })
}

// Rotate the list of line segments
rotateLines (lines, rotate) {
    return lines.map((line) = > {
        return [
            this.getRotatedPos(line[0] [0], line[0] [1], rotate),
            this.getRotatedPos(line[1] [0], line[1] [1], rotate)
        ]
    })
}
Copy the code

The effect is as follows:

The same goes for the circle, which is converted to a polygon and rotated, then scanned and rotated back:

conclusion

This article introduces several simple graphics hand-painted style implementation method, which involves simple mathematical knowledge and area filling algorithm, if there is an unreasonable or better way to achieve please discuss it in the message area, the complete example code at: github.com/wanglin2/ha… . Thanks for reading and see you next time

Reference article:

  • Github.com/rough-stuff…
  • Wenku.baidu.com/view/4ee141…
  • Blog.csdn.net/orbit/artic…
  • Blog.csdn.net/wodownload2…
  • Blog.csdn.net/keneyr/arti…
  • www.twinklingstar.cn/2013/325/re…