PK Creative Spring Festival, I am participating in the “Spring Festival Creative Submission Contest”, please see: Spring Festival Creative Submission Contest”

preface

I double 叒 to participate in the activities, the New Year new weather, as the first article of the New Year, must be full of blessings, give yourself a good omen (reading notes temporarily dove, ha ha ha). I don’t know if JYM still remembers the activity of writing “fu” on Alipay last year. I also wrote a “fu” that I thought was very good-looking.

A few days ago, as a front end, why can’t you do one? Just do it!

How to implement

It is called writing a blessing. In fact, it is drawing in the browser. We can easily think of using Canvas to achieve it. All we need to do is capture the mouse movement track with canvas and draw it to achieve this effect. . This requires us to have a sufficient understanding of canvas API, which will not be introduced here. If you want to know more about it, you will know ~ on Baidu

V1.0 implementation

First of all we definitely need a canvas. For easy observation, give it a background color:

<canvas id="canvas" style="background:#ffffcc" width="400" height="500"></canvas>
Copy the code

Next we need to define a few variables and start implementing the effect:

  • MoveFlag

    : Flag to start drawing
  • Offset : indicates the current position of the mouse
  • PosList

    : set of mouse motion positions

The drawn line is essentially a collection of points, so we need to record where the mouse moves.

First we need to define the color of the brush and bind the mouse related events to the canvas (mobile can be adjusted to touch related events, not written here).

var moveFlag = false var offset = {}, posList = [] var canvas = document.getElementById('canvas') var ctx = canvas.getContext('2d') ctx.fillStyle = 'rgba(0,0,0,0.3)' canvas.onmousedown = (e) => {downEvent(e)} canvas.onmousemove = (e) => {moveEvent(e)} canvas.onmouseup = (e) => { upEvent(e) } canvas.onmouseout = (e) => { upEvent(e) }Copy the code

The logic of the mouse-down event is to change the flag to true, clear the location set and save the current mouse position, and lift and remove events is simply to change the flag to false. The implementation focuses on the movement event, which will be explained later.

function downEvent(e) {
    moveFlag = true
    posList = []
    offset = getPos(e)
}
function upEvent(e) {
    moveFlag = false
}
Copy the code

Mouse down uses a utility function to get position:

function getPos(e) {
    return {
        x: e.clientX - canvas.offsetLeft,
        y: e.clientY - canvas.offsetTop
    }
}
Copy the code

Get the current mouse position, put the distance of the two mouse positions into the set, draw countless points based on the distance to form a line, and then turn the current point into the end of the mouse movement.

function moveEvent(e) { if (! moveFlag) return var currentOffset = getPos(e) var prevOffset = offset var radius = 1 posList.unshift({ distance: getDistance(prevOffset, currentOffset), time: new Date().getTime() }) var dis = 0, time = 0 for (var i = 0, l = posList.length - 1; i < l; i++) { dis += posList[i].distance time += posList[i].time - posList[i + 1].time } offset = currentOffset for (var i = 0,  l = Math.round(posList[0].distance / 1); i < l + 1; i++) { var x = prevOffset.x + (currentOffset.x - prevOffset.x) / l * i var y = prevOffset.y + (currentOffset.y - prevOffset.y) / l * i ctx.beginPath() ctx.arc(x, y, radius, 0, 2 * Math.PI, true) ctx.fill() } }Copy the code

In the above code, in the first for loop we calculate the distance of mouse movement, and in the second for loop we divide the distance into several parts in the unit of 1, and draw a circle for each part to form a straight line. The code uses a utility function to calculate the distance:

function getDistance(a, b) {
    return Math.sqrt(Math.pow((b.x - a.x), 2) + Math.pow((b.y - a.y), 2))
}
Copy the code

Our V1.0 version is now complete, and we can now compete on the canvas.

Although it is implemented, it is too ugly. There is no brush edge and no change in thickness. How can we write a beautiful fu character?

V2.0 implementation

Compared to version 1.0, we need to add more realistic effects to the brush, such as the pressure of touch, the maximum and minimum width of the pen, and the smoothness of the pen. Restore the true use of feeling as much as possible.

Add the following parameters

  • LineMax

    : maximum line width
  • LineMin

    : minimum line width
  • Smoothness

    : How smooth the strokes are
  • LinePressure

    : stroke pressure

How to realize the smoothness of strokes? We judge the current distance and the smoothness degree of strokes when calculating the distance. If the distance is greater than the smoothness degree, we will jump out of this cycle.

for (var i = 0, l = posList.length - 1; i < l; i++) { dis += posList[i].distance time += posList[i].time - posList[i + 1].time if (dis > smoothness) break; // Add, keep smooth}Copy the code

Then how do we achieve the pressure effect of strokes? In use, it is nothing more than a long time to stay and hard work will make the lines thicker and rougher. In the code, we can simulate this use scenario through the time interval and distance between two points, and dynamically generate the radius of the circle for drawing.

var offsetRadius = Math.min((time / dis) * linePressure + lineMin, lineMax) / 2 
Copy the code

As can be seen here, we simulated the radius of a dynamic circle using the minimum line width, pressure, point time and distance, and restricted the circle radius to no more than the maximum line width. Then in the second for loop we can draw circles of different sizes using dynamic circle radii to simulate smooth strokes of uniform thickness.

for (var i = 0, l = Math.round(posList[0].distance / 1); i < l + 1; i++) {
    var x = prevOffset.x + (currentOffset.x - prevOffset.x) / l * i
    var y = prevOffset.y + (currentOffset.y - prevOffset.y) / l * i
    var r = currentRadius + (offsetRadius - currentRadius) / l * i
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, true)
    ctx.fill()
}
Copy the code

The full 2.0 code is not here, there is the full 3.0 version below, I will include the full 3.0 code at the end

See the effect:

In fact, basically the simulation of drawing and strokes has been completed, but after all, it is the New Year, need a New Year atmosphere, and about the line width we can also give it to the user, let them configure their own, the next is the 3.0 ultimate version.

V3.0 implementation

V3.0 mainly added undo functionality. To put it bluntly, the implementation of undo is to maintain a history array of strokes when the mouse moves. After clicking undo, the last one in the history array is removed and the whole canvas is redrawn.

back() { history.pop(); ctx.clearRect(0, 0, canvas.width, canvas.height); for (var i = 0; i < history.length; i++) { var h = history[i]; for (var j = 0; j < h.length; j += 3) { ctx.beginPath(); canvas .getContext("2d") .arc(h[j], h[j + 1], h[j + 2], 0, 2 * Math.PI, true); ctx.fill(); }}},Copy the code

Results the following

As you can see in the image above, I have visualized the line width, stroke pressure and smoothness to make it easy for the user to customize and adjust the desired effect. Here I have simply written a native bidirectional binding support that takes effect as it changes.

Finally, in order to enhance the atmosphere of Chinese New Year, we find a festive background as the canvas background to draw.

Some people may ask why I don’t directly add background in CSS. In fact, I did so in the beginning. However, when transferring pictures to canvas in this way, the background will not be part of the canvas, so the picture is directly drawn on the canvas.

var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.onload = function () {
    ctx.drawImage(img, 0, 0);
}
img.src = './drawBG.jpg';
Copy the code

The last thing is to add a save logic, which is not listed.

Finished, we finally formed is such a page, focus on the function of ha, the page is too lazy to beautify, at least this background looks very New Year atmosphere ha ha ha. One of the defects is that the simulation of the brush is not enough, but this is the best plan I can think of so far. Welcome to discuss in the comments section of friends, I will listen to you with an open mind. The final code for v3.0 is at the end.

The last

I write not worthy of the blessing issued to wish you all happy New Year full of blessings!

The source code

<! DOCTYPEhtml>
<html lang="en">

<head>
	<meta charset="UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
	<style type="text/css">
	</style>
</head>

<body>
	<canvas id="canvas" style="background:#ffffcc" width="400" height="700"></canvas>
	<! -- <img src="./drawBG.jpg" crossorigin="anonymous" id="bg" alt="" style="display: none;" > -->
	<br />
	<div style="position: absolute; top: 10px; left: 420px;">Line width range:<input style="display:inline" type="text" model='lineMin' id="lineMin" /> - <input style="display:inline"
			type="text" model='lineMax' id="lineMax" /><br />Stroke pressure:<input type="text" model='linePressure' id="linePressure" /><br />Degree of smoothness:<input type="text" model='smoothness' id="smoothness" /><br />
		<input type="button" id='back' value="Cancel" onclick="back()" />
		<input type="button" id='clear' value="Empty" onclick="clear()" />
		<input type="button" id='save' value="Save" onclick="save()" />
	</div>

	<script type="text/javascript">
		var moveFlag = false
		var offset = {}, // The current position
			posList = [] // Set of motion positions

		var drawHistory = [],
			startOffset = null

		var lineMax = 30,
			lineMin = 2,
			linePressure = 3,
			smoothness = 80

		var radius = 0
		var canvas = document.getElementById('canvas')
		var ctx = canvas.getContext('2d')

		var img = new Image();
		img.setAttribute('crossOrigin'.'anonymous');
		img.onload = function () {
			ctx.drawImage(img, 0.0);
		}
		img.src = './drawBG.jpg';
		ctx.fillStyle = 'rgba (0,0,0,0.3)'

		canvas.onmousedown = (e) = > {
			downEvent(e)
		}
		canvas.onmousemove = (e) = > {
			moveEvent(e)
		}
		canvas.onmouseup = (e) = > {
			upEvent(e)
		}
		canvas.onmouseout = (e) = > {
			upEvent(e)
		}

		function downEvent(e) {
			moveFlag = true
			posList = []
			drawHistory.push([])
			console.log(drawHistory);
			startOffset = offset = getPos(e)
		}

		function moveEvent(e) {
			if(! moveFlag)return

			var currentOffset = getPos(e)
			var prevOffset = offset
			var currentRadius = radius

			posList.unshift({
				distance: getDistance(prevOffset, currentOffset),
				time: new Date().getTime()
			})

			var dis = 0,
				time = 0

			for (var i = 0, l = posList.length - 1; i < l; i++) {
				dis += posList[i].distance
				time += posList[i].time - posList[i + 1].time
				if (dis > smoothness) break; // Add, keep it smooth
			}

			var offsetRadius = Math.min((time / dis) * linePressure + lineMin, lineMax) / 2 // Add pressure control circle radius
			radius = offsetRadius / / new
			offset = currentOffset
			if (dis < 7) return;
			if (startOffset) {
				prevOffset = startOffset
				currentRadius = offsetRadius
				startOffset = null
			}

			for (var i = 0, l = Math.round(posList[0].distance / 1); i < l + 1; i++) {
				var x = prevOffset.x + (currentOffset.x - prevOffset.x) / l * i
				var y = prevOffset.y + (currentOffset.y - prevOffset.y) / l * i

				var r = currentRadius + (offsetRadius - currentRadius) / l * i

				ctx.beginPath()
				ctx.arc(x, y, r, 0.2 * Math.PI, true)
				ctx.fill()

				drawHistory[drawHistory.length - 1].push(x, y, r)
			}
		}

		function upEvent(e) {
			moveFlag = false
		}

		function getPos(e) {
			return {
				x: e.clientX - canvas.offsetLeft,
				y: e.clientY - canvas.offsetTop
			}
		}

		function getDistance(a, b) {
			return Math.sqrt(Math.pow((b.x - a.x), 2) + Math.pow((b.y - a.y), 2))}function clear() {
			drawHistory = []
			ctx.clearRect(0.0, canvas.width, canvas.height);
		}

		function back() {
			drawHistory.pop();
			ctx.clearRect(0.0, canvas.width, canvas.height);
			ctx.drawImage(img, 0.0);
			for (var i = 0; i < drawHistory.length; i++) {
				var h = drawHistory[i];
				for (var j = 0; j < h.length; j += 3) {
					ctx.beginPath();
					canvas
						.getContext("2d")
						.arc(h[j], h[j + 1], h[j + 2].0.2 * Math.PI, true); ctx.fill(); }}}function save() {
			var url = canvas.toDataURL("image/png");

			var oA = document.createElement("a");
			oA.download = ' '; // Set the file name for the download. Default is' download '.
			oA.href = url;
			document.body.appendChild(oA);
			oA.click();
			oA.remove(); // Delete the created element after downloading
		}
		// input bidirectional binding
		const ngmodel = {
			lineMin,
			lineMax,
			linePressure,
			smoothness
		};
		// Initialize the assignment
		const inputs = document.querySelectorAll('input[model]');
		for (let i = 0; i < inputs.length; i++) {
			inputs[i].value = ngmodel[inputs[i].getAttribute('model')]
			inputs[i].addEventListener('keyup', change)

		};
		// Input operation assignment
		function change(e) {
			const attr = e.target.getAttribute('model');

			window[attr] = ngmodel[attr] = e.target.value
		}
	</script>
</body>

</html>
Copy the code