The beginning of the story, I want a ball
Objective: Canvas is used to make a ball that simulates free fall. The ball is elastic.
I used to study gravity in physics in junior high school, and I still remember that there is height, speed and gravitational acceleration. After being dropped from high altitude, the ball will fall freely. If the ball is elastic, it will bounce back.
-
Build HTML template, initialize canvas canvas, brush
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title></title>
<style>
html.body.canvas {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
window.onload = () = > {
const canvas = document.getElementById('canvas');
// How big the world stage must be how big 😄
// ps: the width and height here are not the width and height of the CSS style layer, but pixels
canvas.width = window.document.body.clientWidth;
canvas.height = window.document.body.clientHeight;
const ctx = canvas.getContext('2d');
// next do some things
// ...
}
</script>
</body>
</html>
Copy the code
There you have it, a clean canvas,
-
So let’s draw a ball, let’s define a class not car today, not foo today but
Ball
Class.
class Ball {
// Initialization characteristics
constructor(options = {}) {
const {
x = 0./ / x coordinate
y = 0./ / y
ctx = null.// The Magic Brush 🖌️
radius = 0.// Radius of the ball
color = '# 000' / / color
} = options
this.x = x;
this.y = y;
this.ctx = ctx;
this.radius = radius;
this.color = color
}
/ / rendering
render() {
this.ctx.beginPath();
this.ctx.fillStyle = this.color;
/ / draw circles
this.ctx.arc(this.x, this.y, this.radius, 0.2 * Math.PI)
this.ctx.fill()
}
}
Copy the code
-
With this
Ball
Class to generate a ball
window.onload = () = > {
// ...
const ctx = canvas.getContext('2d');
const ball = new Ball({
ctx,
x: ctx.canvas.width * 0.5.// In the center of the canvas
y: ctx.canvas.height * 0.5.radius: 20.color: '#66cccc'
})
ball.render();
}
class Ball {
// ...
}
Copy the code
Little ball is born
-
Get him moving, use speed and acceleration,
We’ll use the requestAnimationFrame method, which allows us to call the specified function at the start of the next frame, requestAnimationFrame details.
window.onload = () = > {
// ...
ball.render();
// loop painting
const loopDraw = () = > {
requestAnimationFrame(loopDraw);
ball.render();
}
loopDraw(); // Start the animation
}
class Ball {
// ...
}
Copy the code
Ugh ~~~~ the ball hasn’t moved yet, it’s dripping! You also need a method to update the position of the ball. Keep working on the Ball and add a updata method
window.onload = () = > {
// ...
const loopDraw = () = > {
requestAnimationFrame(loopDraw);
// Clear the canvas, otherwise you can see the movement of each frame, this piece has a chew, you can also make cool things.
ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
ball.render();
ball.updata(); // Update the location
}
loopDraw();
}
class Ball {
// ...
this.radius = radius;
this.color = color
/ / speed
this.vy = 0; // At first it is static
/ / acceleration
this.gvy = 1;
render() {
// ...
}
updata() {
this.y += this.vy; // Change y by speed per frame
this.vy += this.gvy; // The speed increases with acceleration per frame
// Hit bottom collision detection, otherwise the ball will fly off the screen.
if (this.y >= this.ctx.canvas.height - this.radius) {
this.y = this.ctx.canvas.height - this.radius;
// Rebound is a 180 degree change in the direction of movement
this.vy = -this.vy * 0.75;// Speed loss, roughly imitate the effect of gravity, arbitrarily adjust to your favorite value, about.}}}Copy the code
Little ball it’s moving! It’s moving!
-
summary
1. Animation needs to use requestAnimationFrame, of course you can use setTimeout or setInterval to simulate loop.
2. Clear the canvas of the previous frame before drawing the next frame, otherwise the effect of the previous frame will remain on the canvas. Of course you can keep it if you want.
3. The speed of motion can be understood as the momentum of motion needed for each frame. It takes time to draw each frame, and the time of each frame is not necessarily fixed. According to this feature, the animation can also be optimized to be smoother.
Two, continue to play ball
-
Let the ball move in a chaotic manner, if in space, the effects of gravity are ignored
Experience with y motion, add x motion, remove acceleration, because we’re in space, add omnidirectional collision detection
window.onload = () = > {
// ...
}
class Ball {
// ...
/ / speed
this.vx = -2; // This is the new member
this.vy = 2;
/ / acceleration
this.gvx = 0;
this.gvy = 0; I don't need you this time
render() {
// ...
}
updata() {
this.x += this.vx;
this.y += this.vy;
this.vy += this.gvy;
this.vx += this.gvx;
/ / peaked
if (this.y - this.radius <= 0) {
this.y = this.radius
this.vy = -this.vy * 0.99 / /
}
/ / bottom
if (this.y >= this.ctx.canvas.height - this.radius) {
if (this.vy <= this.gvy * 2 + this.vy * 0.8) this.vy = 0;
this.y = this.ctx.canvas.height - this.radius;
this.vy = -this.vy * 0.75; / / then
}
/ / right
if (this.x - this.radius <= 0) {
this.x = this.radius
this.vx = -this.vx * 0.5 / / set
}
/ / touch to the left
if (this.x + this.radius >= this.ctx.canvas.width) {
this.x = this.ctx.canvas.width - this.radius
this.vx = -this.vx * 0.5 / / buy}}}Copy the code
look! Bouncing balls, hitting walls everywhere.
-
More ball movement
Ball is a class, so initialize it several times and give the Ball a random speed
window.onload = () = > {
// ...
const num = 100;
let balls = []
// It is colorful
const colors = ['#66cccc'.'#ccff66'.'#ff99cc'.'#ff9999'.'# 666699'.'#ff0033'.'#FFF2B0'];
// I'll take 100
for (let i = 0; i < num; i++) {
balls.push(new Ball({
ctx,
// Randomly appear anywhere on the canvas
x: Math.floor(Math.random() * ctx.canvas.width),
y: Math.floor(Math.random() * ctx.canvas.height),
radius: 10.color: colors[Math.floor(Math.random() * 7)]}}))// loop painting
const loopDraw = () = > {
requestAnimationFrame(loopDraw);
ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((ball, index) = >{ ball.render(); ball.updata(); }}})class Ball {
constructor(options = {}) {
// ...
/ / speed
this.vx = (Math.random() - 0.5) * 10;
this.vy = (Math.random() - 0.5) * 10;
/ / acceleration
this.gvx = (Math.random() - 0.5) * 0.01;
this.gvy = (Math.random() - 0.5) * 0.01
}
// ...
}
Copy the code
Well ~
Third, let neighbors more contact
Neighborhood can only be within a certain range, too far is not oh, then you need to know the distance between two balls, calculate the distance between two points, very familiar ah, an unknown enthusiastic children’s shoes instantly said junior high school (probably) learned the formula
-
Connecting action: Connecting two balls with a wire
A new member of the Ball enters renderLine, drawing lines between points;
// js version of the calculation of the distance between two points formula
function twoPointDistance(p1, p2) {
let distance = Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2));
return distance;
}
window.onload = () = > {
// ...
// loop painting
const loopDraw = () = > {
requestAnimationFrame(loopDraw);
ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((ball, index) = > {
ball.render();
ball.updata();
balls.forEach(ball2= > {
const distance = twoPointDistance(ball, ball2)
// Exclude yourself and those beyond 100 pixels
if (distance && distance < 100) {
ball.renderLine(ball2)
}
})
})
}
}
class Ball {
// ...
render() {
// ...
}
updata() {
// ...
}
renderLine(target) {
this.ctx.beginPath();
this.ctx.strokeStyle = "ddd";
this.ctx.moveTo(this.x, this.y);
this.ctx.lineTo(target.x, target.y);
this.ctx.stroke(); }}Copy the code
-
Add a special bond
If our colors are different, let’s draw a bond to make the balls smaller and more numerous
window.onload = () = > {
// ...
}
class Ball {
// ...
render() {
// ...
}
updata() {
// ...
}
renderLine(target) {
// ...
// Gradient, composed of me and Target
var lingrad = this.ctx.createLinearGradient(this.x, this.y, target.x, target.y);
lingrad.addColorStop(0.this.color);
lingrad.addColorStop(1, target.color);
this.ctx.strokeStyle = lingrad;
// ...}}Copy the code
-
Add a drag shadow to the colorful phantom
window.onload = () = > {
// ...
// loop painting
const loopDraw = () = > {
/ /...
// Replace clearRect to make the last effect transparency 0.3
ctx.fillStyle = 'rgba (255255255,0.3)';
ctx.fillRect(0.0, canvas.width, canvas.height);
// ..}}class Ball {
// ...
}
Copy the code
-
One more circle,
Ball added renderCircle
// ...
class Ball {
// ...
renderCircle(target, radius) {
this.ctx.beginPath();
this.ctx.strokeStyle = this.color;
this.ctx.arc((this.x + target.x) / 2, (this.y + target.y) / 2, radius, 0.2 * Math.PI);
this.ctx.stroke(); }}Copy the code
-
One more… Well, I don’t have enough space.
Four, optimization
-
Optimize to each frame
The time of each frame is different, so the speed of both x and Y axis should be the same every millisecond. In this way, we need to get the elapsed time of each frame, and then adjust the speed increment of updata to make the animation smoother
let delayTime = 0;
// The time of the last frame
let lastTime = +new Date;
// loop painting
const loopDraw = () = > {
requestAnimationFrame(loopDraw);
// The current time
const now = +new Date;
delayTime = now - lastTime;
lastTime = now;
if (delayTime > 50) delayTime = 50;
balls.forEach((ball, index) = > {
ball.render();
// Adjust the increment in updata according to the time
ball.updata(delayTime && delayTime);
// ...})}// ...
updata(delayTime) {
// The time of each frame is different, so use every millisecond
this.x += this.vx / (delayTime || 1) * 3;
this.y += this.vy / (delayTime || 1) * 3;
// ...
}
Copy the code
-
Along with a frame rate monitor
Animation has frame rates, so there’s a way to check it to see if the animation is smooth. < 30 frames -> red > 30 frames -> green.
If the frame rate is too low, consider optimizing the callback function in the requestAnimationFrame to see if you are doing too much. Of course, there are a lot of optimization methods, animation this I do not understand. I don’t teach fish to swim
;
The main drawing method in the small plug-in
// Draw method
const FPS = (fpsList) = > {
ctx.font = '14px serif';
ctx.fillStyle = "#fff"
const len = fpsList.length;
ctx.fillText(`FPS: ${fpsList[len - 1]}`.5.14);
ctx.lineWidth = '2'
fpsList.forEach((time, index) = > {
if (time < 30) {
ctx.strokeStyle = '#fd5454';
} else {
ctx.strokeStyle = '#6dc113';
}
ctx.beginPath();
ctx.moveTo(ctx.canvas.width - ((len - index) * 2), ctx.canvas.height);
ctx.lineTo(ctx.canvas.width - ((len - index) * 2), (ctx.canvas.height - time * 0.5));
ctx.stroke();
});
// Delete the extra ones
if (len > 50) {
fpsList.shift()
}
}
Copy the code
The last
Based on these, we can continue to expand, such as moving the cursor on the canvas, and automatically connecting the ball near the mouse; It can also pull its motion; The effect of the collision between the balls; Ideas come out one by one, based on a simple ball, generating various ideas, from drawing a circle to various cool effects, the more I try, the more surprises, this is an interesting label. And it doesn’t use very complicated apis, such as canvas moveTo lineTo arc and other common apis, plus some math or physics knowledge. Because of these surprises, I will not be bored on the way of learning.
update
-
Mouse will not get lost in animation, focus is me, 2020-11-11 21:30
// Load the image
const loadImage = (src) = > new Promise(resolve= > {
const img = document.createElement('img');
img.src = src;
img.onload = () = > {
returnresolve(img); }})window.onload = async() = > {// ...
const bg = await loadImage('./media/bg.jpg');
let mouseBall;
// ...
// loop painting
const loopDraw = () = > {
// ...
balls.forEach((ball, index) = > {
// ...
if (mouseBall) {
const lineMouse = twoPointDistance(ball, mouseBall);
if (lineMouse && lineMouse < 100) {
ball.renderLine(mouseBall)
}
}
// ...})}window.addEventListener('mousemove'.(e) = > {
mouseBall = new Ball({
ctx,
x: e.pageX,
y: e.pageY,
radius: 1.color: "#fff"})})}Copy the code