How to animate multiple HTML5 canvas objects one after another? - javascript

I want to make an animation using the HTML5 canvas and JavaScript. The idea is to write classes for different objects, like this:
class Line {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y2;
...
}
draw() {
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
...
}
draw() {}
}
...
Then all you would have to do in the main code is to draw the shapes one after another with pauses in between:
let line1 = new Line(x1, y1, x2, y2);
let circle = new Circle(x, y, r);
let line2 = new Line(x1, y1, x2, y2);
line1.draw()
pause()
circle.draw()
pause()
line2.draw()
...
Is there an easy way to this (without having to deal with Promises and nested Callback Functions), for example by using some library?

Key frames
You can use key frames to great effect to animate almost anything.
The example below (was going to do more of a write up but I was too late, you have accepted an answer) shows how a very basic key frame utility can create animations.
A key frame is just a time and a value
Key frames are added to tracks that give a name to the value.
Thus the name x (position) and the keys {time:0, value:100}, {time:1000, value:900} will change the x property from 100 to 900 during the time 0 to 1 second
For example a circle
const circle = {
x: 0,
y: 0,
r: 10,
col : "",
draw() {
ctx.fillStyle = this.col;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill()
}
};
can have any of its properties changed over time.
First create a tracks object and define the keys
const circleTracks = createTracks();
// properties to animate
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");
Then add key frames at specific time stamps.
circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"});
circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"});
circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"});
circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"});
circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20});
circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"});
circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"});
circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"});
When ready clean up the the keys (You can add them out of time order)
circleTracks.clean();
Seek to the start
circleTracks.seek(0);
And update the object
circleTracks.update(circle);
To animate just call the tick and update functions, and draw the circle
circleTracks.tick();
circleTracks.update(circle);
circle.draw();
Example
Click to start the animation.
When it ends you can scrub the animation using tracks.seek(time)
This is the most basic keyframe animations.
And the best thing about key frames is that they separate the animation from the code, letting you import and export animations as simple data structures.
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
const allTracks = [];
function addKeyframedObject(tracks, object) {
tracks.clean();
tracks.seek(0);
tracks.update(object);
allTracks.push({tracks, object});
}
const FRAMES_PER_SEC = 60, TICK = 1000 / FRAMES_PER_SEC; //
const key = (time, value) => ({time, value});
var playing = false;
var showScrubber = false;
var currentTime = 0;
function mainLoop() {
ctx.clearRect(0 ,0 ,ctx.canvas.width, ctx.canvas.height);
if(playing) {
for (const animated of allTracks) {
animated.tracks.tick();
animated.tracks.update(animated.object);
}
}
for (const animated of allTracks) {
animated.object.draw();
}
if(showScrubber) {
slide.update();
slide.draw();
if(slide.value !== currentTime) {
currentTime = slide.value;
for (const animated of allTracks) {
animated.tracks.seek(currentTime);
animated.tracks.update(animated.object);
}
}
} else {
if(mouse.button) { playing = true }
}
if(allTracks[0].tracks.time > 6300) {
showScrubber = true
playing = false;
}
requestAnimationFrame(mainLoop);
}
const text = {
x: canvas.width / 2,
y: canvas.height / 2,
alpha: 1,
text: "",
draw() {
ctx.font = "24px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#000";
ctx.globalAlpha = this.alpha;
ctx.fillText(this.text, this.x, this.y);
ctx.globalAlpha = 1;
}
}
const circle = {
x: 0,
y: 0,
r: 10,
col : "",
draw() {
ctx.fillStyle = this.col;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill()
}
}
const circleTracks = createTracks();
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");
circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"});
circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"});
circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"});
circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"});
circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20});
circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"});
circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"});
circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"});
addKeyframedObject(circleTracks, circle);
const textTracks = createTracks();
textTracks.addTrack("alpha");
textTracks.addTrack("text");
textTracks.addKeysAtTime(0, {alpha: 1, text: "Click to start"});
textTracks.addKeysAtTime(1, {alpha: 0});
textTracks.addKeysAtTime(20, {alpha: 0, text: "Simple keyframed animation"});
textTracks.addKeysAtTime(1000, {alpha: 1});
textTracks.addKeysAtTime(2000, {alpha: 0});
textTracks.addKeysAtTime(3500, {alpha: 0, text: "The END!" });
textTracks.addKeysAtTime(3500, {alpha: 1});
textTracks.addKeysAtTime(5500, {alpha: 1});
textTracks.addKeysAtTime(6000, {alpha: 0, text: "Use slider to scrub"});
textTracks.addKeysAtTime(6300, {alpha: 1});
addKeyframedObject(textTracks, text);
function createTracks() {
return {
tracks: {},
addTrack(name, keys = [], value) {
this.tracks[name] = {name, keys, idx: -1, value}
},
addKeysAtTime(time, keys) {
for(const name of Object.keys(keys)) {
this.tracks[name].keys.push(key(time, keys[name]));
}
},
clean() {
for(const track of Object.values(this.tracks)) {
track.keys.sort((a,b) => a.time - b.time);
}
},
seek(time) { // seek to random time
this.time = time;
for(const track of Object.values(this.tracks)) {
if (track.keys[0].time > time) {
track.idx = -1; // befor first key
}else {
let idx = 1;
while(idx < track.keys.length) {
if(track.keys[idx].time > time && track.keys[idx-1].time <= time) {
track.idx = idx - 1;
break;
}
idx += 1;
}
}
}
this.tick(0);
},
tick(timeStep = TICK) {
const time = this.time += timeStep;
for(const track of Object.values(this.tracks)) {
if(track.keys[track.idx + 1] && track.keys[track.idx + 1].time <= time) {
track.idx += 1;
}
if(track.idx === -1) {
track.value = track.keys[0].value;
} else {
const k1 = track.keys[track.idx];
const k2 = track.keys[track.idx + 1];
if (typeof k1.value !== "number" || !k2) {
track.value = k1.value;
} else if (k2) {
const unitTime = (time - k1.time) / (k2.time - k1.time);
track.value = (k2.value - k1.value) * unitTime + k1.value;
}
}
}
},
update(obj) {
for(const track of Object.values(this.tracks)) {
obj[track.name] = track.value;
}
}
};
};
const slide = {
min: 0,
max: 6300,
value: 6300,
top: 160,
left: 1,
height: 9,
width: 438,
slide: 10,
slideX: 0,
draw() {
ctx.fillStyle = "#000";
ctx.fillRect(this.left-1, this.top-1, this.width+ 2, this.height+ 2);
ctx.fillStyle = "#888";
ctx.fillRect(this.left, this.top, this.width, this.height);
ctx.fillStyle = "#DDD";
this.slideX = (this.value - this.min) / (this.max - this.min) * (this.width - this.slide) + this.left;
ctx.fillRect(this.slideX, this.top + 1, this.slide, this.height - 2);
},
update() {
if(mouse.x > this.left && mouse.x < this.left + this.width &&
mouse.y > this.top && mouse.y < this.top + this.height) {
if (mouse.button && !this.captured) {
this.captured = true;
} else {
canvas.style.cursor = "ew-resize";
}
}
if (this.captured) {
if (!mouse.button) {
this.captured = false;
canvas.style.cursor = "default";
} else {
this.value = ((mouse.x - this.left) / this.width) * (this.max - this.min) + this.min;
canvas.style.cursor = "none";
this.value = this.value < this.min ? this.min : this.value > this.max ? this.max : this.value;
}
}
}
};
const mouse = {x : 0, y : 0, button : false};
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
canvas { border: 1px solid black; }
<canvas id="canvas" width="440" height="170"><canvas>

A good question given that what you don't want to do (use promises and/or callbacks) would effectively mean hard coding the animation in script with limited potential for re-use, and possibly creating difficulties in making modifications in the future.
A solution that I've used is to create a story book of functions that draw frames, so you would put
()=>line1.draw()
into the book rather than
line1.draw()
which would draw it immediately and try adding its return value to the book!
The next part (in no particular order) is a player that uses requestAnimationFrame to time stepping through the story book and calling functions to draw the frame. Minimally it would need methods for script to
add a frame drawing function,
add a delay before advancing to the next frame, and
play the animation.
Making the delay function take a number of frames to wait before calling the next entry in the story book keeps it simple, but creates timings based on frame rate which may not be constant.
Here's a simplified example in pure JavaScript that changes background color (not canvas manipulation) for demonstration - have a look for reference if you can't get it working.
"use strict";
class AnimePlayer {
constructor() {
this.storyBook = [];
this.pause = 0;
this.drawFrame = this.drawFrame.bind( this);
this.frameNum = 0;
}
addFrame( frameDrawer) {
this.storyBook.push( frameDrawer);
}
pauseFrames(n) {
this.storyBook.push ( ()=>this.pause = n);
}
play() {
this.frameNum = 0;
this.drawFrame();
}
drawFrame() {
if( this.pause > 0) {
--this.pause;
requestAnimationFrame( this.drawFrame);
}
else if( this.frameNum < this.storyBook.length) {
this.storyBook[this.frameNum]();
++this.frameNum;
requestAnimationFrame( this.drawFrame);
}
}
}
let player = new AnimePlayer();
let style = document.body.style;
player.addFrame( ()=> style.backgroundColor = "green");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "yellow");
player.pauseFrames(5);
player.addFrame( ()=>style.backgroundColor = "orange");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "red");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "");
function tryMe() {
console.clear();
player.play();
}
<button type="button" onclick="tryMe()">try me</button>

Related

How can i make a single straight line in HTML Canvas which is bound to the input of the mouse

I've got a question which i can't seem to figure out the right way. For some context. I'm trying to make a sort of connect the dots game. People can click on a circle and link the circle to the next one. Connecting the dots is working fine, but i wanted to add a function that when clicked on the first dot, users get a perfect straight line from the clicked dot to the mouse input. This way the users get some feedback on which dot they clicked, and how they can link to each other.
Here is a codePen that shows what i want to achieve. The only thing different is that the dots in this "pen" are animated, and i want them to stand still.
I tried a lots of things so far, and found multiple StackOverflow articles about this subject. So far i got connecting the dots working. Also the user draws a line when a dot is selected. The big issue however is that every movement of the user is resulting in drawing a line on the canvas. This line which is only used as feedback which dot is selected and how the user can draw his cursor to the next dot, is filling the screen with thousands of instances in no-time. I only want to show them one single line at a time, leading to their mouse cursor as the endpoint. Just like the codePen i did mention earlier on.
Here is my JS code so far.
var dotColor = "#FF0000";
var strokeColor = "#FF0000";
var mouse = {
x: undefined,
y: undefined
};
var obj;
var data = {
canvas: null,
ctx: null,
clickedDot: null,
dots: [{
x: 180,
y: 50
}, {
x: 20,
y: 50
}]
};
window.addEventListener('mousemove', function(e) {
mouse.x = e.x;
mouse.y = e.y;
renderActiveLink();
});
function circleCollision(c1, c2) {
var a = c1.r + c2.r,
x = c1.x - c2.x,
y = c1.y - c2.y;
if (a > Math.sqrt((x * x) + (y * y))) return true;
else return false;
}
function prepCanvas() {
var res = window.devicePixelRatio || 1,
scale = 1 / res;
data.canvas = document.getElementById('dots');
data.ctx = data.canvas.getContext('2d');
data.canvas.width = 500;
data.canvas.height = 300;
data.ctx.scale(res, res);
data.canvas.addEventListener('mousedown', function(e) {
checkForDot(e);
});
data.canvas.addEventListener('mouseup', function(e) {
checkForDot(e);
});
}
function drawDots() {
var i = 0;
for (; i < data.dots.length; i++) {
var d = data.dots[i];
data.ctx.beginPath();
data.ctx.arc(d.x, d.y, 5, 0, 2 * Math.PI);
data.ctx.fillStyle = dotColor;
data.ctx.fill();
data.ctx.closePath();
}
}
function drawLine(toDot) {
data.ctx.beginPath();
data.ctx.moveTo(data.clickedDot.x, data.clickedDot.y);
data.ctx.lineTo(toDot.x, toDot.y);
data.ctx.lineWidth = 5;
data.ctx.strokeStyle = strokeColor;
data.ctx.stroke();
data.ctx.closePath();
}
function checkForDot(e) {
var i = 0,
col = null;
for (; i < data.dots.length; i++) {
var d = data.dots[i],
c1 = {
x: d.x,
y: d.y,
r: 50
},
c2 = {
x: e.pageX,
y: e.pageY,
r: 50
};
if (circleCollision(c1, c2)) {
col = d;
}
}
if (col !== null) {
if (data.clickedDot !== null) drawLine(col);
data.clickedDot = col;
obj = col;
} else data.clickedDot = null;
}
function renderActiveLink() {
data.ctx.beginPath();
data.ctx.lineWidth = 5;
data.ctx.shadowBlur = 0;
data.ctx.moveTo(obj.x, obj.y);
data.ctx.lineTo(mouse.x, mouse.y);
data.ctx.strokeStyle = '#000000';
data.ctx.stroke();
}
prepCanvas();
drawDots();
*{
margin:0;
padding: 0;
}
#dots {
border: 1px solid black;
}
<canvas id="dots"></canvas>
Also to be seen in this JSFiddle.
Hope someone here can help me out. If any more information is needed, i will be glad to answer your questions :D.
This code just to help you to use canvas.
I added a start status in the data.
var data = {
start: false, // initial value is false
canvas: null,
ctx: null,
clickedDot: null,
dots: [{
x: 180,
y: 50
}, {
x: 20,
y: 50
}]
};
And i changed start state in the mousedown and mouseup listeners.
data.canvas.addEventListener('mousedown', function(e) {
data.start = true
checkForDot(e);
});
data.canvas.addEventListener('mouseup', function(e) {
data.start = false
checkForDot(e);
});
so the renderActiveLink method, Only works when the start position is true.
and Clears everything before drawing each line.
function renderActiveLink() {
if(!data.start) return;
data.ctx.clearRect(0, 0, data.canvas.width, data.canvas.height);
drawDots();
...
Final code:
var dotColor = "#FF0000";
var strokeColor = "#FF0000";
var mouse = {
x: undefined,
y: undefined
};
var obj;
var data = {
start: false,
canvas: null,
ctx: null,
clickedDot: null,
dots: [{
x: 180,
y: 50
}, {
x: 20,
y: 50
}]
};
window.addEventListener('mousemove', function(e) {
mouse.x = e.x;
mouse.y = e.y;
renderActiveLink();
});
function circleCollision(c1, c2) {
var a = c1.r + c2.r,
x = c1.x - c2.x,
y = c1.y - c2.y;
if (a > Math.sqrt((x * x) + (y * y))) return true;
else return false;
}
function prepCanvas() {
var res = window.devicePixelRatio || 1,
scale = 1 / res;
data.canvas = document.getElementById('dots');
data.ctx = data.canvas.getContext('2d');
data.canvas.width = 500;
data.canvas.height = 300;
data.ctx.scale(res, res);
data.canvas.addEventListener('mousedown', function(e) {
data.start = true
checkForDot(e);
});
data.canvas.addEventListener('mouseup', function(e) {
data.start = false
checkForDot(e);
});
}
function drawDots() {
var i = 0;
for (; i < data.dots.length; i++) {
var d = data.dots[i];
data.ctx.beginPath();
data.ctx.arc(d.x, d.y, 5, 0, 2 * Math.PI);
data.ctx.fillStyle = dotColor;
data.ctx.fill();
data.ctx.closePath();
}
}
function drawLine(toDot) {
data.ctx.beginPath();
data.ctx.moveTo(data.clickedDot.x, data.clickedDot.y);
data.ctx.lineTo(toDot.x, toDot.y);
data.ctx.lineWidth = 5;
data.ctx.strokeStyle = strokeColor;
data.ctx.stroke();
data.ctx.closePath();
}
function checkForDot(e) {
var i = 0,
col = null;
for (; i < data.dots.length; i++) {
var d = data.dots[i],
c1 = {
x: d.x,
y: d.y,
r: 50
},
c2 = {
x: e.pageX,
y: e.pageY,
r: 50
};
if (circleCollision(c1, c2)) {
col = d;
}
}
if (col !== null) {
if (data.clickedDot !== null) drawLine(col);
data.clickedDot = col;
obj = col;
} else data.clickedDot = null;
}
function renderActiveLink() {
if(!data.start) return;
data.ctx.clearRect(0, 0, data.canvas.width, data.canvas.height);
drawDots();
data.ctx.beginPath();
data.ctx.lineWidth = 5;
data.ctx.shadowBlur = 0;
data.ctx.moveTo(obj.x, obj.y);
data.ctx.lineTo(mouse.x, mouse.y);
data.ctx.strokeStyle = '#000000';
data.ctx.stroke();
}
prepCanvas();
drawDots();
*{
margin:0;
padding: 0;
}
#dots {
border: 1px solid black;
}
<canvas id="dots"></canvas>

How do I repeatedly interpolate linearly between two values within a requestAnimationFrame loop

I'm trying to implement multiplayer position interpolation for my canvas game but I'm having trouble using my linear interpolation (lerp) function. I experience slight jitter when using t = 0.1 so I'm looking for some alternative way to calculate t such that movement will appear smooth to the user - or a different solution altogether.
I render a canvas as follows (simplified):
function lerp (a, b, t) {
return a + (b - a) * t;
}
function tick() {
// Process position update(s).
const t = 0.1;
position.x = lerp(position.x, target_position.x, t);
position.y = lerp(position.y, target_position.y, t);
// Camera follow by offsetting canvas transform.
update_camera();
window.requestAnimationFrame(tick);
}
function update_camera() {
const position_delta_x = position.x - initial_position.x;
const position_delta_y = position.y - initial_position.y;
const offset_x = Math.round(position_delta_x);
const offset_y = Math.round(position_delta_y);
ctx.setTransform(1, 0, 0, 1, -offset_x, -offset_y);
}
tick();
I receive about 10 updates every second via a websocket that contains new position data:
function handle_position_update(new_position) {
target_position.x = new_position.x;
target_position.y = new_position.y;
}
I've noticed the jitter is coming from my camera follow logic but I'm sure as to why this is happening.
JSFIDDLE
// Shared code.
const INITIAL_POSITION = {
x: 300,
y: 80
};
// Server-side code.
const server_state = {
x: INITIAL_POSITION.x,
y: INITIAL_POSITION.y
};
const UPDATE_TICK_RATE = 10;
const PHYSICS_TICK_RATE = 60;
setInterval(() => {
// Simulate server physics update.
const w = input[87] ? 1 : 0;
const a = input[65] ? 1 : 0;
const s = input[83] ? 1 : 0;
const d = input[68] ? 1 : 0;
const vertical = w ? 1 : s ? -1 : 0;
const horizontal = d ? 1 : a ? -1 : 0;
server_state.x += horizontal * 5;
server_state.y -= vertical * 5;
}, 1000 / PHYSICS_TICK_RATE)
setInterval(() => {
// Simulate server sending updates.
target_pos_x = server_state.x;
target_pos_y = server_state.y;
}, 1000 / UPDATE_TICK_RATE);
// Client-side code.
let target_pos_x = INITIAL_POSITION.x,
target_pos_y = INITIAL_POSITION.y;
let input = [];
window.addEventListener('keydown', e => input[e.keyCode] = true);
window.addEventListener('keyup', e => input[e.keyCode] = false);
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width = 600;
const H = canvas.height = 160;
const circle = {
position: {
x: INITIAL_POSITION.x,
y: INITIAL_POSITION.y
},
tick: function() {
let t = 0.1;
this.position.x = lerp(this.position.x, target_pos_x, t);
this.position.y = lerp(this.position.y, target_pos_y, t);
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'white'
ctx.fill();
}
}
const reference_point = {
position: {
x: 240,
y: 60
},
tick: function() {
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 3, 0, 2 * Math.PI);
ctx.fillStyle = 'red'
ctx.fill()
}
}
function tick(now) {
clear();
circle.tick();
reference_point.tick();
camera_follow();
window.requestAnimationFrame(tick);
}
tick();
function clear() {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
function camera_follow() {
const position_delta_x = circle.position.x - INITIAL_POSITION.x;
const position_delta_y = circle.position.y - INITIAL_POSITION.y;
const offset_x = Math.round(position_delta_x);
const offset_y = Math.round(position_delta_y);
ctx.setTransform(1, 0, 0, 1, -offset_x, -offset_y);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
<canvas></canvas>
<div>WASD to move</div>

Animating multiple circles in a canvas

I'm trying to make an animation inside a canvas: here, a numbered circle must be drawn and move from left to right one single time, disappearing as soon as it reaches the end of animation.
For now I managed to animate it in loop, but I need to animate at the same time (or with a set delay) multiple numbered circles, strating in different rows (changing y position) so they wont overlap.
Any idea how can I manage this? my JS code is the following:
// Single Animated Circle - Get Canvas element by Id
var canvas = document.getElementById("canvas");
// Set Canvas dimensions
canvas.width = 300;
canvas.height = 900;
// Get drawing context
var ctx = canvas.getContext("2d");
// Radius
var radius = 13;
// Starting Position
var x = radius;
var y = radius;
// Speed in x and y direction
var dx = 1;
var dy = 0;
// Generate random number
var randomNumber = Math.floor(Math.random() * 60) + 1;
if (randomNumber > 0 && randomNumber <= 10) {
ctx.strokeStyle = "#0b0bf1";
} else if (randomNumber > 10 && randomNumber <= 20) {
ctx.strokeStyle = "#f10b0b";
} else if (randomNumber > 20 && randomNumber <= 30) {
ctx.strokeStyle = "#0bf163";
} else if (randomNumber > 30 && randomNumber <= 40) {
ctx.strokeStyle = "#f1da0b";
} else if (randomNumber > 40 && randomNumber <= 50) {
ctx.strokeStyle = "#950bf1";
} else if (randomNumber > 50 && randomNumber <= 60) {
ctx.strokeStyle = "#0bf1e5";
}
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, 300, 900);
if (x + radius > 300 || x - radius < 0) {
x = radius;
}
x += dx;
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(randomNumber, x - 5, y + 3);
}
// Animate the Circle
animate3();
<canvas id="canvas"></canvas>
Here is a solution which doesn't use classes as such and separates the animation logic from the updating - which can be useful if you want more precise control over timing.
// Some helper functions
const clamp = (number, min, max) => Math.min(Math.max(number, min), max);
// Choose and remove random member of arr with equal probability
const takeRandom = arr => arr.splice(parseInt(Math.random() * arr.length), 1)[0]
// Call a function at an interval, passing the amount of time that has passed since the last call
function update(callBack, interval) {
let now = performance.now();
let last;
setInterval(function() {
last = now;
now = performance.now();
callBack((now - last) / 1000);
})
}
const settings = {
width: 300,
height: 150,
radius: 13,
gap: 5,
circles: 5,
maxSpeed: 100,
colors: ["#0b0bf1", "#f10b0b", "#0bf163", "#f1da0b", "#950bf1", "#0bf1e5"]
};
const canvas = document.getElementById("canvas");
canvas.width = settings.width;
canvas.height = settings.height;
const ctx = canvas.getContext("2d");
// Set circle properties
const circles = [...Array(settings.circles).keys()].map(i => ({
number: i + 1,
x: settings.radius,
y: settings.radius + (settings.radius * 2 + settings.gap) * i,
radius: settings.radius,
dx: settings.maxSpeed * Math.random(), // This is the speed in pixels per second
dy: 0,
color: takeRandom(settings.colors)
}));
function drawCircle(circle) {
ctx.strokeStyle = circle.color;
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(circle.number, circle.x - 5, circle.y + 3);
}
function updateCircle(circle, dt) {
// Update a circle's position after dt seconds
circle.x = clamp(circle.x + circle.dx * dt, circle.radius + 1, settings.width - circle.radius - 1);
circle.y = clamp(circle.y + circle.dy * dt, circle.radius + 1, settings.height - circle.radius - 1);
}
function animate() {
ctx.clearRect(0, 0, settings.width, settings.height);
circles.forEach(drawCircle);
requestAnimationFrame(animate);
}
update(dt => circles.forEach(circle => updateCircle(circle, dt)), 50);
animate();
<canvas id="canvas" style="border: solid 1px black"></canvas>
Here I transformed your sample code into a class ...
We pass all the data as a parameter, you can see that in the constructor, I simplified a lot of your code to keep it really short, but all the same drawing you did is there in the draw function
Then all we need to do is create instances of this class and call the draw function inside that animate3 loop you already have.
You had a hardcoded value on the radius:
ctx.arc(x, y, 12, 0, Math.PI * 2, false)
I assume that was a mistake and fix it on my code
var canvas = document.getElementById("canvas");
canvas.width = canvas.height = 300;
var ctx = canvas.getContext("2d");
class Circle {
constructor(data) {
this.data = data
}
draw() {
if (this.data.x + this.data.radius > 300 || this.data.x - this.data.radius < 0) {
this.data.x = this.data.radius;
}
this.data.x += this.data.dx;
ctx.beginPath();
ctx.arc(this.data.x, this.data.y, this.data.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(this.data.number, this.data.x - 5, this.data.y + 3);
}
}
circles = []
circles.push(new Circle({radius:13, x: 10, y: 15, dx: 1, dy: 0, number: "1"}))
circles.push(new Circle({radius:10, x: 10, y: 50, dx: 2, dy: 0, number: "2"}))
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(item => item.draw());
}
animate3();
<canvas id="canvas"></canvas>
Code should be easy to follow let me know if you have any questions

Crafty.js not recognizing collision with other entities

I'm trying to learn crafty by coding a simplistic pacman game but I have had trouble ever since I changed the main player from a square to a sprite. I have the code below and I get a chomping pacman that I can move around the screen that changes directions but when the sprite runs over a dot, nothing happens. Is there something that I am missing that I need to do?
var x= 0;
var y= 0;
var w= 50;
var h= 50;
var color= "#F00";
var speed= 500;
var dot=0;
var score=0;
var power=0;
var spriteSheet;
var ent1;
function hitSolid(){
console.log('hit a solid');
Crafty('PlayerSprite').x -= Crafty("PlayerSprite").motionDelta().x;
Crafty('PlayerSprite').y -= Crafty("PlayerSprite").motionDelta().y;
}
function processDot(e){
console.log('hit a dot');
console.log(e);
dot++;
Crafty(e).destroy();
score = score + 10;
scoreBoard.text('Score: '+ score);
}
function animateDirection(e){
var signY = e.y;
var signX = e.x;
Crafty('PlayerSprite').origin();
if (this.lastSignY !== signY) {
if (signY === 1) {
console.log('down');
Crafty('PlayerSprite').rotation = 90;
} else if (signY === -1) {
console.log('up');
Crafty('PlayerSprite').rotation = 270;
} else if (signY === 0) {
Crafty('PlayerSprite').pauseAnimation;
Crafty('PlayerSprite').resetAnimation;
}
this.lastSignY = signY;
}
if (this.lastSignX !== signX) {
Crafty('PlayerSprite').flip('X');
if (signX === 1) {
console.log('right');
Crafty('PlayerSprite').rotation = 0;
} else if (signX === -1) {
console.log('left');
Crafty('PlayerSprite').rotation = 180;
} else if (signX === 0) {
Crafty('PlayerSprite').pauseAnimation;
Crafty('PlayerSprite').resetAnimation;
}
this.lastSignY = signY;
this.lastSignX = signX;
}
}
function initLoads(){
console.log('initLoads called');
spriteSheet = Crafty.sprite(13,13, "http://i192.photobucket.com/albums/z138/mbafernandez/player_zpsrdewekon.png", {
player: [0,0]
},0,0,0);
ent1 = Crafty.e('2D, DOM, Color, Fourway, SpriteAnimation, Collision, wiredhitbox, player, PlayerSprite').attr({x:100, y: 100, z:1, w:24, h:24}).fourway(speed).collision().onHit('Solid',console.log('hit solid'));
ent1.reel('chomp', 500, 0, 0, 4);
ent1.animate('chomp', -1);
ent1.bind('NewDirection', function(e){animateDirection(e);});
console.log('load sprites');
console.log(ent1.getId);
}
function makeDot(xPos, yPos){
Crafty.e('Dot, 2D, Canvas, Color, Solid, Collision')
.attr({x: xPos, y: yPos, w:10, h:10})
.color('white')
.checkHits('player')
.bind("HitOn",function(hitData){
processDot(this.getId);
});
}
Crafty.init(420,600, document.getElementById('test'));
initLoads;
Crafty.background('#000000');
scoreBoard = Crafty.e('2D, DOM, Text, ScoreBoard').attr({x: 350,y: 12, z:1, w: 100, h:20});
scoreBoard.textColor('white');
scoreBoard.text('Score: '+ score);
powerBoard = Crafty.e('2D, DOM, Text, ScoreBoard').attr({x: 350,y: 25, z:1, w: 100, h:20});
powerBoard.textColor('white');
powerBoard.text('Power: '+ power);
Crafty.e('Solid, 2D, Canvas, Color').attr({x: 0, y: 0, w: 1, h: 600}).color('white');
Crafty.e('Solid, 2D, Canvas, Color').attr({x: 430, y: 0, w: 1, h: 600}).color('white');
Crafty.e('Solid, 2D, Canvas, Color').attr({x: 0, y: 0, w: 430, h: 1}).color('white');
Crafty.e('Solid, 2D, Canvas, Color').attr({x: 0, y: 610, w: 350, h: 1}).color('white');
makeDot(200,200);
Origin must be set once and with a value or it kills collision

Integrate GSAP with canvas to make a curvy timeline

I'm currently working on a canvas timeline-like animation.
This is what I made so far...
$(function() {
'use strict';
var canvas = document.querySelector('#canvas');
var ctx = canvas.getContext('2d');
var s = 20;
var arr = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100];
var colorP = ['#ff5454', '#ffa144', '#ffe256', '#aaff75', '#8cd8ff', '#b5b6ff', '#b882ff'];
var dots = [];
var rDots = [];
function init() {
var reverse = true;
for (var i = 0; i < 100; i++) {
var dot = new Object();
var height = null;
if (arr.indexOf(i) != -1) {
dot.x = s;
dot.y = 50;
dot.r = 3;
dot.c = 'red';
dot.f = 'rgba(0,0,0,0)';
dot.t = '1';
dot.s = 0;
rDots.push(dot);
} else {
dot.x = s;
dot.y = 50;
dot.r = 1;
dot.c = 'red';
dot.f = '';
dot.t = '';
dot.s = 0;
}
s += 10;
dots.push(dot);
};
function tween() {
height = Math.floor(Math.random() * (75 - 25) + 25);
TweenMax.staggerTo(dots, 5, {
y: height,
yoyo: true,
repeat: 'repeat',
repeatDelay: 1,
ease: Sine.easeInOut
}, 0.5);
};
tween();
setInterval(function() {
tween()
}, 4800);
}
init();
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < dots.length - 1; i++) {
ctx.beginPath();
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[i + 1].x, dots[i + 1].y);
ctx.lineWidth = 3;
ctx.strokeStyle = 'red';
ctx.stroke();
};
for (var i = 0; i < dots.length; i++) {
ctx.beginPath();
ctx.arc(dots[i].x, dots[i].y, dots[i].r, 0, 2 * Math.PI);
ctx.strokeStyle = dots[i].c;
ctx.lineWidth = 1;
ctx.fillStyle = dots[i].f;
ctx.fill();
ctx.stroke();
ctx.font = dots[i].s + 'px Arial';
ctx.textAlign = 'center';
ctx.fillStyle = '#FFF';
ctx.fillText(dots[i].t, dots[i].x, dots[i].y + 4);
};
setTimeout(function() {
draw();
}, 5);
}
draw();
function hover(e, bool) {
var dot = canvas.getBoundingClientRect();
var x = e.clientX - dot.left;
var y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
if (x == rDots[i].x) {
TweenMax.to(rDots[i], 0.1, {
r: 10,
f: 'red',
s: 8
});
$('body').css('cursor', 'pointer');
} else {
TweenMax.to(rDots[i], 0.1, {
r: 3,
f: 'rgba(0,0,0,0)',
s: 0
});
}
};
};
$(canvas).on('mousemove', function(e) {
hover(e, true);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" height="100" width="1050" style="background: #EEE"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
The idea is,
I want it to swing randomly (checked)
and when the cursor close in the knot, it will enlarge and show the text in it...
I tried to use x and y axis to do the trick,
but it doesn't work well...
Then I tried to make another function to draw a bigger circle to cover the original knot,
but since my draw() keeping clear the canvas, so I failed again...
Wondering is there any better ways to make it work?
any suggestions or hints are welcome!
You may find jCanvas helpful.
It's a JavaScript library that wraps the HTML5 canvas API, letting you add certain object-like functionality using a jQuery-style syntax. You could refactor your code a bit and use a mouseOver effect rather than binding a mousemove event to the canvas, which will let you create a smother animation.
Also, if you increase the area of the rDots.x that's triggering your animation and set your Tween time interval to something a bit longer than 0.1 that makes the animation slightly less jerky as well.
Not sure if this solves your issue, but I hope it helps!
Ok, I've work my way out.
$(function() {
'use strict';
var dots = [],
eDots = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100],
rDots = [],
stagger = 0;
var canvas = document.querySelector('#canvas'),
ctx = canvas.getContext('2d');
//initialize all the dots obj
function init() {
for (var i = 0; i < 100; i++) {
if (eDots.indexOf(i) != -1) {
var dot = {
xAxis: stagger,
yAxis: 50,
radius: 3,
color: 'rgba(0,0,0,0)',
num: 1,
};
rDots.push(dot);
} else {
var dot = {
xAxis: stagger,
yAxis: 50,
radius: .5,
color: 'rgba(0,0,0,0)',
num: ''
};
}
dots.push(dot);
stagger += 10;
}
};
init();
//Save position property for click event
function getSize() {
for (var i = 0; i < rDots.length; i++) {
rDots[i].top = rDots[i].yAxis - rDots[i].radius;
rDots[i].right = rDots[i].xAxis + rDots[i].radius;
rDots[i].bottom = rDots[i].yAxis + rDots[i].radius;
rDots[i].left = rDots[i].xAxis - rDots[i].radius;
}
}
getSize();
//Hover event dots to change style
function hover() {
$(canvas).bind('mousemove', function(e) {
var dot = canvas.getBoundingClientRect(),
x = e.clientX - dot.left,
y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
ctx.beginPath();
ctx.arc(rDots[i].xAxis, rDots[i].yAxis, rDots[i].radius, 0, 2 * Math.PI);
//rDots[i].radius = ctx.isPointInPath(x, y) ? 10 : 3;
//rDots[i].color = ctx.isPointInPath(x, y) ? 'red' : 'rgba(0, 0, 0, 0)';
if (ctx.isPointInPath(x, y)) {
TweenMax.to(rDots[i], 0.1, {
radius: 10,
color: 'red',
});
$(canvas).css({
cursor: 'pointer'
});
return;
} else {
TweenMax.to(rDots[i], 0.1, {
radius: 3,
color: 'rgba(0,0,0,0)'
});
}
ctx.stroke();
ctx.fill();
$(canvas).css({
cursor: 'default'
});
}
});
};
hover();
//Setup click event for functioning purpose
function click(e) {
var dot = canvas.getBoundingClientRect(),
x = e.clientX - dot.left,
y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
if (x < rDots[i].right && x > rDots[i].left && y > rDots[i].top && y < rDots[i].bottom) {
console.log('This is dot ' + i);
}
}
};
$(canvas).on('click', function(e) {
click(e);
})
//Let the line start to twist
function tween() {
var height = Math.floor(Math.random() * (75 - 25) + 25);
TweenMax.staggerTo(dots, 4, {
yAxis: height,
yoyo: true,
repeat: 'repeat',
repeatDelay: 1,
ease: Sine.easeInOut
}, 0.5);
setTimeout(function() {
tween();
}, 3800);
}
tween();
//Let's get some paint
function draw() {
//clear canvas for animate
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw the lines
for (var i = 0; i < dots.length - 1; i++) {
ctx.moveTo(dots[i].xAxis, dots[i].yAxis);
ctx.lineTo(dots[i + 1].xAxis, dots[i + 1].yAxis);
ctx.lineWidth = 3;
ctx.stroke();
}
//draw the dots
for (var i = 0; i < dots.length; i++) {
ctx.beginPath();
ctx.arc(dots[i].xAxis, dots[i].yAxis, dots[i].radius, 0, 2 * Math.PI);
ctx.strokeStyle = 'red';
ctx.strokeWidth = '1px';
ctx.fillStyle = dots[i].color;
ctx.stroke();
ctx.fill()
};
setTimeout(function() {
draw();
}, 10);
}
draw();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
<canvas id="canvas" height="100" width="1000" style="background:#EEE"></canvas>
Turn's out all I need is to do is use isPointOnPath to get the path's axis,
then manipulate the certain dot's property with if statement, in my case is it's radius and color.
simple enough...
Can't believe I couldn't figured it out.
I guess I need some sleep now XD

Categories

Resources