preface
Well, one day I was walking around and I came across a post that drew a dog on Canvas that would follow the mouse. Ah, it’s interesting, a short and interesting demo.
I then recreated it using the Canvas Sprite, which can be used for canvas animations in many scenarios. Here is the introduction:
Sprite is not a Canvas API. It is a graphic object that abstracts the behavior and method of animation. Below you’ll see how to move sprites without affecting the background of the animation and give them various behaviors, such as adding a slow-walking behavior to the 🐶 object, which becomes a run under certain mechanisms. These behaviors can be repeated indefinitely, they can occur over time or over a distance, and they can change their appearance over time.
The elves
To make a useful Sprite, you first need to draw it and be able to place it in a specific position in the animation. It can also accept different behavior functions and perform certain actions.
So the Sprite object contains two methods
paint
update
Paint is a method for drawing sprites, and Update is used to perform the behavior of sprites.
It is a graphic object that abstracts the animation behavior and the creation methods. This means that the two methods of the Sprite are common methods that abstract each Sprite object. The paint method draws the Sprite, which is done by the painter, because some sprites are generated from images and others are drawn from canvas, and they have name, size, and position properties. The Update method executes an array of behaviors objects, each of which uses the execute method to do something to the Sprite. A basic Sprite abstraction comes out:
// The following code is only used to illustrate the idea, the implementation of the reader's personal style.
class Sprite {
// Accept an array of names, drawers, and behavior objects
constructor(name, painter, behaviors) {
this.name = name;
this.painter = painter;
this.behaviors = behaviors;
// Some default properties
this.left = 0;
this.top = 0;
this.width = 10;
this.height = 10;
}
paint (ctx) {
this.painter.paint(this, ctx) }, update (ctx, ... args) {for (var i = this.behaviors.length; i > 0; --i) {
this.behaviors[i-1].execute(this, ctx, ... args) } } }Copy the code
Create the dog with the Sprite object.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const dog = new Sprite('dog', {
painter: function (sprite, ctx) {
dogImg = new Image();
dogImg.src = require('./assets/dog/dog1.png');
dogImg.onload = function () {ctx.drawimage (dogImg, sprite.left, sprite.top, sprite.width, sprite.height)}}, []}); dog.paint(ctx)Copy the code
The puppy has appeared!
Sprite painter
The Sprite object does not need to do the drawing itself; instead, it proxies the drawing operation to another object. This means that the Sprite object is decoupled from the painter object. In this way, you can dynamically set the painter for the Sprite object while the program is running, which greatly increases the flexibility of the program. This is also a practical application of the strategy pattern.
In this demo, we use an image painter. The image painter object contains a reference to the image object that draws the image to the drawing environment object passed in via the paint() method.
export class ImagePainter {
constructor (img) {
this.image = new Image();
this.image.src = img;
}
paint (sprite, ctx) {
if (this.image ! = =undefined) {
if (!this.image.complete) {
this.image.onload = function () {
sprite.width = this.width;
sprite.height = this.height;
ctx.drawImage(this, sprite.left, sprite.top, sprite.width, sprite.height)
}
} else {
ctx.drawImage(this.image, sprite.left, sprite.top, sprite.width, sprite.height)
}
}
}
}
Copy the code
There are two cases:
- When the image is not fully loaded, create a function wrapped around it
drawImage
Method and assign the function toimage.onload
Is executed when the image has been loadedonload
Methods. To performdrawImage
To draw on itcanvas
On. - When the image has been loaded, execute directly
drawImage
Methods.
When you create an ImagePainter, you need to pass a reference to the URL of the image to the ImagePainter constructor. Only when the image is fully loaded does the image painter perform paint() to draw it.
const dog = new Sprite('dog'.new ImagePainter('dog1.png'), []);
function animate () {
ctx.clearRect(0.0, canvas.width, canvas.height);
dog.paint(ctx);
window.requestAnimationFrame(animate);
}
animate();
dog.paint(ctx);
Copy the code
Because the Sprite is used in the animation, instead of drawing once and stopping, draw the Sprite object over and over again. So if you call the image painter and the image has not been loaded, this method will not do anything until the Sprite is fully loaded.
Because to achieve the animation effect of the Sprite is not only one image, is the role of multiple images to show the dynamic effect. So you need a SpriteAnimator –SpriteAnimator
SpriteAnimator
The SpriteAnimator object is used to control the animated image of the Sprite. It contains an array. Each element in the array is an object that implements the paint method, and these objects can be drawn across you. Each Sprite object has a Sprite painter dedicated to its drawing.
Every once in a while, the SpriteAnimator object selects a painter object from the array objects in order to draw sprites. So when you create a SpriteAnimator object, you pass an array of Sprite painters to the constructor. The spriteAnimator. start method is used for animation playback, accepting the number of milliseconds that the Sprite object to be played and the animation to be maintained.
class SpriteAnimator {
constructor (painters, elapsedCallback) {
this.painters = painters;
this.elapsedCallback = elapsedCallback;
this.painter = [];
this.timerList = [];
this.duration = 1000;
this.startTime = 0;
this.index = 0;
}
start (sprite, duration) {
let endTime = +new Date() + duration;
let period = duration / this.painters.length;
let interval = undefined;
let originalPainter = sprite.painter
this.index = 0;
sprite.animating = true;
sprite.painter = this.painters[this.index];
interval = setInterval(() = > {
if (+new Date() < endTime) {
sprite.painter = this.painters[++this.index]
} else {
this.end(sprite, originalPainter);
clearInterval(interval)
}
}, period)
}
end (sprite, orginalPainter) {
sprite.animating = false;
this.elapsedCallback ? this.elapsedCallback(sprite) : sprite.painter = orginalPainter; }}Copy the code
In order to play the animation effect, the Start method of the SpriteAnimator object needs to calculate the stop time of the animation by adding the duration of the animation to the current time. Then, according to the duration of the animation and the length of the painters array to be drawn, the “period” of the animation is calculated, that is, the display time allocated to each animation image. Use setInterval to replace Sprite with period for painter and requestAnimationFrame to display the current image for a specified period of time, when the specified time is reached, call clearInterval to stop animation.
Finally, when the spriteAnimator. start method is finished, call the end method to show the final Painter.
Look at the effect,
const dog = new Sprite('dog'.new ImagePainter('dog1.png'), []);
let dogPainterList = ['dog1.png'.'dog2.png'.'dog3.png'.'dog4.png'.'dog5.png'.'dog6.png'.'dog7.png'.'dog8.png'].map(item= > new ImagePainter(item));
let dogAnimator = new SpriteAnimator(dogPainterList)
function animate () {
ctx.clearRect(0.0, canvas.width, canvas.height);
dog.paint(ctx);
window.requestAnimationFrame(animate);
}
animate();
dog.paint(ctx);
dogAnimator.start(dog, 2000);
Copy the code
1. First create an array of images to play, consisting of a series of image painters.
2. Generate an image array as a parameterSpriteAnimator
object
3, callSpriteAnimator
The object’sstart
Method, the parameters are sprites and the duration.
Animation lasts two seconds (usingmouseenter
Method, only animate when the mouse moves in)
behavior
To give the dog an accelerated behavior, define the behavior:
- Only when the
mousedown
When the action is performed and recordedmousedown
The position of theta, figure it outcanvas
Position 222222222222222222. - Compare the Sprite position to the click position, and if less than the click position, run faster.
const accelerate = {
velocityX: 1.execute: function(sprite, ctx, pos) {
this.velocityX = 1; // Reset the acceleration
if (sprite.left + sprite.right < pos) {
sprite.left += this.velocityX;
} else {
this.velocityX = 0; }}}const dog = new Sprite('dog'.new ImagePainter('dog1.png'), [accelerate]);
Copy the code
Add a little bit of detail
Finally, the background animation is perfect. This part is not difficult. Set them according to different ratesoffset
, cooperatetranslate
Function to create the backward background effect. usemouseenter
Method, and only animate when the mouse moves in
usemousedown
Method to perform the running behavior when the mouse is clicked
Mirror with scale(-1, 1) to move left and right. It’s worth noting that there is a big problem with element positioning in the Canvas. This is because Canvas’s coordinate transformation system is different from CSS, so to achieve the center flip effect, you need to move the center point of the target element to the transformation axis before the flip.
ctx.translate(dog.left, 0)
ctx.scale(-1.1)
ctx.translate(- dog.left - dog.width, 0)
Copy the code
The specific code is relatively simple, not posted, students can have a try.
At the end
For more articles, please visit Github. If you like, please click star. It is also a kind of encouragement to the author.