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
- 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