How to create fading cursor trails on an image canvas? - javascript

I've created a cursor trail over an image canvas where the image canvas is at the background and the cursor trail is on a second canvas on top of the image canvas. The issue I'm currently facing is that I am unable to create a trail that fades away gradually.
I've seen tips on how to make it fade with a plain background using fillStyle but I don't know how to make a fading cursor trail work with an image as the background.
<!DOCTYPE html>
<html>
<head>
<style>
.stack {
position: relative;
}
.stack canvas {
position: absolute;
left: 0;
top: 0;
}
.stack,
#main_canvas {
background-size: contain;
width: 100%;
margin: auto;
}
</style>
</head>
<body>
<div class="stack">
<canvas id="main_canvas"> main canvas</canvas>
</div>
<script>
var SCREEN_WIDTH = window.innerWidth;
var SCREEN_HEIGHT = window.innerHeight;
var RADIUS = 70;
var RADIUS_SCALE = 1;
var RADIUS_SCALE_MIN = 1;
var RADIUS_SCALE_MAX = 1.5;
var QUANTITY = 25;
var canvas;
var canvas_bg;
var context;
var context_bg;
var particles;
var slider_image;
var mouseX = SCREEN_WIDTH * 0.5;
var mouseY = SCREEN_HEIGHT * 0.5;
var mouseIsDown = false;
var ind = 0;
function init() {
canvas = document.getElementById("main_canvas");
canvas_bg = document.createElement("canvas"); //<canvas> predefined
canvas.setAttribute("alt", "countless stars");
if (canvas && canvas.getContext) {
windowResizeHandler();
//background canvas
create_sliders();
//main canvas for creating mouse trails
context = canvas.getContext("2d");
// Register event listeners
window.addEventListener("mousemove", documentMouseMoveHandler, false);
window.addEventListener("mousedown", documentMouseDownHandler, false);
window.addEventListener("mouseup", documentMouseUpHandler, false);
document.addEventListener(
"touchstart",
documentTouchStartHandler,
false
);
document.addEventListener(
"touchmove",
documentTouchMoveHandler,
false
);
window.addEventListener("resize", windowResizeHandler, false);
createParticles();
setInterval(loop, 1000 / 60);
}
}
function create_sliders() {
slider_image = new Image();
slider_image.src =
"https://images.pexels.com/photos/956999/milky-way-starry-sky-night-sky-star-956999.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940";
canvas_bg.width = canvas.width;
canvas_bg.height = canvas.height;
// insert into DOM on top:
canvas.parentNode.insertBefore(canvas_bg, canvas);
context_bg = canvas_bg.getContext("2d");
context_bg.drawImage(slider_image, 0, 0, canvas.width, canvas.height);
}
function createParticles() {
particles = [];
for (var i = 0; i < QUANTITY; i++) {
var particle = {
size: 1,
position: { x: mouseX, y: mouseY },
offset: { x: 0, y: 0 },
shift: { x: mouseX, y: mouseY },
speed: 0.01 + Math.random() * 0.04,
targetSize: 1,
fillColor:
"#" + ((Math.random() * 0x404040 + 0xaaaaaa) | 0).toString(16),
orbit: RADIUS * 0.5 + RADIUS * 0.5 * Math.random()
};
particles.push(particle);
}
}
function documentMouseMoveHandler(event) {
mouseX = event.clientX - (window.innerWidth - SCREEN_WIDTH) * 0.5;
mouseY = event.clientY - (window.innerHeight - SCREEN_HEIGHT) * 0.5;
}
function documentMouseDownHandler(event) {
mouseIsDown = true;
}
function documentMouseUpHandler(event) {
mouseIsDown = false;
}
function documentTouchStartHandler(event) {
if (event.touches.length == 1) {
event.preventDefault();
mouseX =
event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * 0.5;
mouseY =
event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * 0.5;
}
}
function documentTouchMoveHandler(event) {
if (event.touches.length == 1) {
event.preventDefault();
mouseX =
event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * 0.5;
mouseY =
event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * 0.5;
}
}
function windowResizeHandler() {
SCREEN_WIDTH = window.innerWidth;
SCREEN_HEIGHT = window.innerHeight;
canvas.width = SCREEN_WIDTH;
canvas.height = SCREEN_HEIGHT;
}
function loop() {
if (mouseIsDown) {
RADIUS_SCALE += (RADIUS_SCALE_MAX - RADIUS_SCALE) * 0.02;
} else {
RADIUS_SCALE -= (RADIUS_SCALE - RADIUS_SCALE_MIN) * 0.02;
}
RADIUS_SCALE = Math.min(RADIUS_SCALE, RADIUS_SCALE_MAX);
// context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillStyle = "rgba(0, 0, 0, 0)";
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
for (i = 0, len = particles.length; i < len; i++) {
var particle = particles[i];
var lp = { x: particle.position.x, y: particle.position.y };
// Rotation
particle.offset.x += particle.speed;
particle.offset.y += particle.speed;
// Follow mouse with some lag
particle.shift.x += (mouseX - particle.shift.x) * particle.speed;
particle.shift.y += (mouseY - particle.shift.y) * particle.speed;
// Apply position
particle.position.x =
particle.shift.x +
Math.cos(i + particle.offset.x) * (particle.orbit * RADIUS_SCALE);
particle.position.y =
particle.shift.y +
Math.sin(i + particle.offset.y) * (particle.orbit * RADIUS_SCALE);
// Limit to screen bounds
particle.position.x = Math.max(
Math.min(particle.position.x, SCREEN_WIDTH),
0
);
particle.position.y = Math.max(
Math.min(particle.position.y, SCREEN_HEIGHT),
0
);
particle.size += (particle.targetSize - particle.size) * 0.05;
if (Math.round(particle.size) == Math.round(particle.targetSize)) {
particle.targetSize = 1 + Math.random() * 7;
}
if (particle.position) context.beginPath();
context.fillStyle = particle.fillColor;
context.strokeStyle = particle.fillColor;
context.lineWidth = particle.size;
context.moveTo(lp.x, lp.y);
context.lineTo(particle.position.x, particle.position.y);
context.stroke();
context.arc(
particle.position.x,
particle.position.y,
particle.size / 2,
0,
Math.PI * 2,
true
);
context.fill();
}
}
window.onload = init;
</script>
</body>
</html>
Currently, the older trails will not fade away and it is creating something more of like a paint effect which I don't want.

One way to fade out your particles is to replace your (currently transparent) drawRect call with a drawImage that draws semi-transparent copy of your background image over each frame before adding particles to the current frame:
In your loop() function:
// Instead of this:
// context.fillStyle = "rgba(0, 0, 0, 0)";
// context.fillRect(0, 0, context.canvas.width, context.canvas.height);
// Do this:
context.save();
context.globalAlpha = 0.1;
context.drawImage(slider_image, 0, 0, canvas.width, canvas.height);
context.restore();
for (i = 0, len = particles.length; i < len; i++) {
...

Related

how to decrease speed of canvas animation?

I am creating an animation using java script & canvas. I am using a fiddle as a reference, currently object are generating randomly & falling from top right corner to bottom left corner which is okay. but the issue is speed the objects are generating & falling in high speed. I want to make animation flow little slow & smooth.
I am new canvas programming, any help will be very useful.
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
window.requestAnimationFrame = requestAnimationFrame;
})();
var particleArr = [],
canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
flakeCount = 700,
mouseX = -100,
mouseY = -100,
xMultiplier = 0.015
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function getRandomColor() {
// Random Color Generate
const colorArr = ["rgba(215,88,69, 1)", "rgba(117, 161, 199, 1)"]; // Blue & Orange Color
const randomColor = colorArr[Math.floor(Math.random() * colorArr.length)];
return randomColor;
}
function flow() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < flakeCount; i++) {
var flake = particleArr[i],
x = mouseX,
y = mouseY,
minDist = 150,
x2 = flake.x,
y2 = flake.y;
var dist = Math.sqrt((x2 - x) * (x2 - x) + (y2 - y) * (y2 - y)),
dx = x2 - x,
dy = y2 - y;
if (dist < minDist) {
var force = minDist / (dist * dist),
xcomp = (x - x2) / dist,
ycomp = (y - y2) / dist,
deltaV = force / 2;
flake.velX -= deltaV * xcomp;
flake.velY -= deltaV * ycomp;
} else {
flake.velX *= .98;
if (flake.velY <= flake.speed) {
flake.velY = flake.speed
}
flake.velX += Math.cos(flake.step += .05) * flake.stepSize;
}
ctx.fillStyle = getRandomColor();
flake.y += flake.velY;
flake.x += flake.velX;
if (flake.y >= canvas.height || flake.y <= 0) {
reset(flake);
}
if (flake.x >= canvas.width || flake.x <= 0) {
reset(flake);
}
ctx.beginPath();
ctx.arc(flake.x, flake.y, flake.size, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(flow);
};
function reset(flake) {
let temp = (Math.random() * 1) + 0.5;
flake.x = canvas.width;
flake.y = 50;
flake.size = (Math.random() * 3) + 5;
flake.speed = (Math.random() * 7) + 0.5;
flake.velY = flake.speed;
flake.velX = -xMultiplier * canvas.width * temp;
// flake.opacity = (Math.random() * 0.5) + 0.3;
}
function init() {
for (var i = 0; i < flakeCount; i++) {
var x = canvas.width,
y = 50,
size = (Math.random() * 3) + 5,
// speed = (Math.random() * 1) + 0.5;
speed = 0;
// opacity = (Math.random() * 0.5) + 0.3;
particleArr.push({
speed: speed,
velY: speed,
velX: -xMultiplier * canvas.width * speed,
x: x,
y: y,
size: size,
stepSize: (Math.random()) / 30,
step: 0,
angle: 360
// opacity: opacity
});
}
flow();
};
canvas.addEventListener("mousemove", function(e) {
mouseX = e.clientX,
mouseY = e.clientY
});
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
init();
canvas {
background-color: #000000 !important;
}
body {
margin: 0;
overflow: hidden;
}
<canvas id="canvas"></canvas>
Could not work out what it was you wanted to slow down as there are several FX and interactions in the code.
I rewrote from the ground up as your code is a little old school.
Rather than play with the constants you had OI added the global variable rate (at the ver top of the source) is used to control the rate at which the animation plays, including the user interaction.
I have added two button to slow or speed up the animation.
Hope this helps :)
var rate = 1;
slower.addEventListener("click", () => rate *= 1 / 1.2);
faster.addEventListener("click", () => rate *= 1.2);
const flakes = [], flakeCount = 700, xMultiplier = 0.015;
const minDist = 150, minDistSqr = minDist * minDist;
const colors = ["#F99","#F83","#AF9","#ED9","#AC8","#FA9" ];
const ctx = canvas.getContext("2d");
const mouse = {x: -100, y: -100};
const randPick = (arr, len = arr.length) => arr[Math.random() * len | 0];
Math.rand = (min, range) => Math.random() * range + min;
function Flake() {
this.reset();
this.stepSize = Math.random() / 30;
this.step = 0;
}
Flake.prototype = {
reset() {
this.x = canvas.width;
this.y = 50;
this.size = Math.rand(5, 3);
this.speed = Math.rand(0.5, 7);
this.velY = this.speed;
this.velX = -xMultiplier * canvas.width * Math.rand(0.5, 1);
this.col = randPick(colors);
},
draw() {
ctx.fillStyle = this.col;
const s = this.size, sh = -s / 2;
ctx.fillRect(this.x + sh, this.y + sh, s, s);
},
update(w, h) {
const f = this;
const dx = f.x - mouse.x;
const dy = f.y - mouse.y;
const distSqr = dx * dx + dy * dy;
if (distSqr < minDistSqr) {
const deltaV = 2 * minDist * rate / distSqr ** 1.5;
f.velX -= deltaV * dx;
f.velY -= deltaV * dy;
} else {
f.velX -= 0.1 * rate * f.velX;
if (f.velY <= f.speed ) { f.velY = f.speed }
f.velX += Math.cos(f.step += 0.05 * rate) * f.stepSize * rate;
}
f.y += f.velY * rate;
f.x += f.velX * rate;
if (f.y >= h || f.y <= 0 || f.x >= w || f.x <= 0) { this.reset() }
else { this.draw() }
}
};
init();
mainLoop();
function mainLoop() {
if (innerWidth !== canvas.width || innerHeight !== canvas.height) { resize() }
else { ctx.clearRect(0, 0, canvas.width, canvas.height) }
for (const f of flakes) { f.update(canvas.width, canvas.height) }
requestAnimationFrame(mainLoop);
}
function init() {
var i = flakeCount;
while (i--) { flakes.push(new Flake()) }
}
canvas.addEventListener("mousemove", e => { mouse.x = e.clientX; mouse.y = e.clientY });
function resize() { canvas.width = innerWidth; canvas.height = innerHeight }
canvas {
background-color: #000;
}
body {
margin: 0;
}
.buttons {
position: absolute;
top: 12px;
left: 12px;
color: #000;
background-color: #AAA;
}
.buttons > div {
margin: 3px;
padding: 3px;
background-color: #EEE;
cursor: pointer;
}
.buttons > div:hover {
background-color: #DEF;
}
<canvas id="canvas"></canvas>
<div class = "buttons">
<div id="slower">Slower</div>
<div id="faster">Faster</div>
</div>
That requestAnimationFrame() function that calls flow() every frame is designed to run as fast as possible for whoever's computer it's running on. I wouldn't mess with your actual render loop.
Try messing with the flake.speed or the xMultiplier. Those are two of the main variables affecting the speed of your particles. You can see how each time through the flow() loop you're adjusting each particle's position based on their velocity properties and position. Then finally rendering the arc with ctx.arc(flake.x, flake.y, flake.size, 0, Math.PI * 2);
So any variable passed to ctx.arc() affects the particle's position. And many of those variables are recalculated each time through the loop.
I'm no expert here, but maybe try fiddling with your variables.
https://codepen.io/nitwit/pen/XWXJNaJ
https://jsfiddle.net/z6r8h5de/
if the issue is there are too many flakes on the screen, turn the count down from 700.
flakeCount = 100,

Anime.js adding consequential animations using canvas

I'm trying to make a simple interaction using canvas and Anime.js, using a modified version of one of Julian's examples. The idea is that a circle would enlarge on screen, then the user clicks anywhere on the screen and the circle would contract. I can't get the second animation to play, not sure if this is because my target is not valid or something to do with the timeline? Here is the example.
//Canvas set up
var canvas = document.querySelector('.gameCanvas');
var ctx = canvas.getContext('2d');
var pointerX = 0;
var pointerY = 0;
var circle;
let basicTimeline = anime.timeline();
function setCanvasSize() {
canvas.width = window.innerWidth * 2;
canvas.height = window.innerHeight * 2;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
canvas.getContext('2d').scale(2, 2);
}
//Objects and animation
function createCircle(x,y) {
var p = {};
p.x = x;
p.y = y;
p.color = "#fff";
p.radius = anime.random(16, 32);
p.alpha = .5;
p.draw = function() {
ctx.globalAlpha = p.alpha;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true);
ctx.fillStyle = p.color;
ctx.fill();
}
return p;
}
function renderParticle(anim) {
for (var i = 0; i < anim.animatables.length; i++) {
anim.animatables[i].target.draw();
}
}
function createExpandCircle(x, y) {
circle = createCircle(x, y);
basicTimeline.add({
targets: circle,
radius: anime.random(80, 160),
alpha: {
value: 1,
easing: 'linear',
duration: anime.random(600, 800),
},
duration: anime.random(1200, 1800),
easing: 'linear',
update: renderParticle,
offset: 0
});
}
function contractCircle(){
basicTimeline.add({
targets: circle,
radius: 20,
easing: 'easeOutExpo'
update: renderParticle
});
}
//Mouse Events
var tap = ('ontouchstart' in window || navigator.msMaxTouchPoints) ? 'touchstart' : 'mousedown';
function updateCoords(e) {
pointerX = e.clientX || e.touches[0].clientX;
pointerY = e.clientY || e.touches[0].clientY;
}
document.addEventListener(tap, function(e) {
updateCoords(e);
contractCircle();
}, false);
var centerX = window.innerWidth / 2;
var centerY = window.innerHeight / 2;
createExpandCircle(centerX, centerY);
setCanvasSize();
window.addEventListener('resize', setCanvasSize, false);
So I was able to solve this by adding the following:
ctx.clearRect(0, 0, canvas.width, canvas.height);
To p.draw, and
basicTimeline = anime.timeline();
To contractCircle.

How do I get this canvas animation script to work in Firefox?

I wrote this canvas animation script in hopes to use it on my portfolio website, but it is basically non-functional in Firefox, and I am not sure why. The script draws stars on the canvas that slowly rotate, and if you hold down the mouse button they spin faster creating trails. It works awesome in chrome, but is extremely slow and choppy in Firefox.
let canvas = document.querySelector('canvas');
let c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let mouse = {
x: window.innerWidth / 2,
y: window.innerHeight / 2
}
let stars = [];
const starCount = 800;
class Star {
constructor(x, y, radius, color){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.draw = () => {
c.save();
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
c.fillStyle = this.color;
c.shadowColor = this.color;
c.shadowBlur = 15;
c.fill();
c.closePath();
c.restore();
};
this.update = () => {
this.draw();
};
}
}
let colors = [
"#A751CC",
"#DE9AF9",
"#F9E0F9",
"#B5ECFB",
"#5F86F7"
];
(initializeStars = () =>{
for(let i = 0; i < starCount; i++){
let randomColorIndex = Math.floor(Math.random() * 5);
let randomRadius = Math.random() * 2;
let x = (Math.random() * (canvas.width + 400)) - (canvas.width + 400) / 2;
let y = (Math.random() * (canvas.width + 400)) - (canvas.width + 400) / 2;
stars.push(new Star(x, y, randomRadius, colors[randomColorIndex]));
}
})();
let opacity = 1;
let speed = 0.0005;
let time = 0;
let spinSpeed = desiredSpeed => {
speed += (desiredSpeed - speed) * 0.01;
time += speed;
return time;
}
let starOpacity = (desiredOpacity, ease) => {
opacity += (desiredOpacity - opacity) * ease;
return c.fillStyle = `rgba(18, 18, 18, ${opacity})`;
}
let animate = () => {
window.requestAnimationFrame(animate);
c.save();
if(mouseDown){
starOpacity(0.01, 0.03);
spinSpeed(0.012);
}else{
starOpacity(1, 0.01);
spinSpeed(0.001);
}
c.fillRect(0,0, canvas.width, canvas.height);
c.translate(canvas.width / 2, canvas.height / 2);
c.rotate(time);
for(let i = 0; i < stars.length; i++){
stars[i].update();
}
c.restore();
}
window.addEventListener('mousemove', e => {
mouse.x = e.clientX - canvas.width / 2;
mouse.y = e.clientY - canvas.height / 2;
});
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
stars = [];
initializeStars();
});
let mouseDown = false;
window.addEventListener("mousedown", () =>{
mouseDown = true;
});
window.addEventListener("mouseup", () => {
mouseDown = false;
});
animate();
Here is the link to the demo on Code Pen; any help is appreciated.
You can use rect instead of arc to draw your stars:
c.rect(this.x, this.y, 2*this.radius, 2*this.radius);
remove the blur It's extremely expensive:
//c.shadowBlur = 15;
You can use a radial gradient going from opaque in the center to transparent in his stead.

How to run multiple instances of a single object JavaScript

Currently attempting to make a physics simulation for elastic collisions of circles. I am having an issue where I do not know how to run the simulation with two circles interacting at the same time. I am not yet looking to create the interaction between the circles just to have them both running simultaneously. Any help is much appreciated. This is my first post so I apologize if I formatted something incorrectly.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
var ball = {
position: {
x: width / 2,
y: height / 2
},
velocity: {
x: 0,
y: 0
},
radius: 15, // 1px = 1cm
restitution: -1
};
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
ball.position.x = mouse.x;
ball.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
ball.velocity.y = (ball.position.y - mouse.y) / 10;
ball.velocity.x = (ball.position.x - mouse.x) / 10;
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
if (!mouse.isDown) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
}
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(ball.position.x, ball.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Here is how I would do it:
Instead of making the ball a kind of static object I made a constructor function (More about that here).
Then I made a ball array to store all the balls.
To make the dragging possible I store a seperate ball, which is not being moved by "physics" in the newBall variable. This ball is either invisible or is the ball currently being dragged.
In mouseDown() the newBall gets positioned under the cursor.
In mouseUp() it gets it's velocity and gets added to the array of animated balls. Also a new newBall gets created.
In loop() I loop two times through the array of animated balls. Once for the physics, once for the painting.
(Normally you would use two different methods with different tickRates to make animation more smooth, because physics calculation doesn't need to happen 60 times per second.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -1
};
var balls = [];
var newBall = new ball();
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y) / 10;
newBall.velocity.x = (newBall.position.x - mouse.x) / 10;
balls.push(newBall);
newBall = new ball();
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
for (var ball of balls) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
}
ctx.clearRect(0, 0, width, height);
for (var ball of balls) {
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
}
ctx.save();
ctx.translate(newBall.position.x, newBall.position.y);
ctx.beginPath();
ctx.arc(0, 0, newBall.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Now to get a bit more complex:
I added tickDelay and tickTimer to use them in a tickLoop
The ball constructor now has two methods:
show() draws the ball on the canvas
tick() does the pysics stuff (dt= deltaTime: time since last tick)
newBall is now null if the mouse isn't pressed
setup() initializes the width and height according to the <canvas> elements real size
tick() loops through the balls and calls .tick() tickDelay is in milliseconds so it gets divided by 1000
drawFrame() is your former loop() and does the drawing stuff
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var tickDelay = frameDelay * 2; //ticks 2 times slower than frames
var frameTimer;
var tickTimer;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -.99;
this.show = function() {
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
};
this.tick = function(dt) {
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
if (this.position.y > height - this.radius) {
this.velocity.y *= this.restitution;
this.position.y = height - this.radius;
}
if (this.position.x > width - this.radius) {
this.velocity.x *= this.restitution;
this.position.x = width - this.radius;
}
if (this.position.x < this.radius) {
this.velocity.x *= this.restitution;
this.position.x = this.radius;
}
if (this.position.y < this.radius) {
this.velocity.y *= this.restitution;
this.position.y = this.radius;
}
}
};
var balls = [];
var newBall;
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
function mouseDown(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
if (!newBall) newBall = new ball();
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
function mouseUp(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y);
newBall.velocity.x = (newBall.position.x - mouse.x);
balls.push(newBall);
newBall = null;
}
}
function setup() {
canvas = document.getElementById("canvas");
width = canvas.getBoundingClientRect().width;
height = canvas.getBoundingClientRect().height;
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
requestAnimationFrame(drawFrame);
frameTimer = setInterval(drawFrame, frameDelay);
tickTimer = setInterval(tick, tickDelay);
}
function tick() {
for (var ball of balls) ball.tick(tickDelay * .001);
}
function drawFrame() {
ctx.clearRect(0, 0, width, height);
for (var ball of balls) ball.show();
if (newBall) newBall.show(ctx);
if (mouse.isDown && newBall) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
A really simple way would to do exactly the same as you do now, but not initiate all functions as a variable. Change all the variables that are functions to just functions, and where you call them. At least the variable called ball. Then after that you could make two variables like this
ball1 = new ball();
ball2 = new ball();
Your script is kind of messy so hard for me to say if this will go through without any errors, but if it does, I am more than happy to help. This is not the very best solution if you only go for the way i presented now so please do not use this as you solution, but more as a way to get started. Also you will not really learn anything of it if we just gave you the answer
Edit:
Another thing to mark is that using setInterval for games and graphical projects may be a bad idea since JavaScript is single threaded. A better solution is using requestAnimationFrame()
It would look something like this
function mainLoop() {
update();
draw();
requestAnimationFrame(mainLoop);
}
// Start things off
requestAnimationFrame(mainLoop);

Canvas Fade Out Particles

I'm a canvas beginner, sorry if this is a trivial question. How can I make the fireworks in my work fade out once they've exploded?
https://jsfiddle.net/ccwhryvv/
var SCREEN_WIDTH = window.innerWidth,
SCREEN_HEIGHT = window.innerHeight,
mousePos = {
x: 400,
y: 300
},
// create canvas
canvas = document.createElement('canvas'),
context = canvas.getContext('2d'),
particles = [],
rockets = [],
MAX_PARTICLES = 400,
colorCode = 0;
// init
$(document).ready(function() {
$('#content')[0].appendChild(canvas);
canvas.width = SCREEN_WIDTH;
canvas.height = SCREEN_HEIGHT;
setInterval(launch, 800);
setInterval(loop, 1000 / 50);
});
// update mouse position
$(document).mousemove(function(e) {
e.preventDefault();
mousePos = {
x: e.clientX,
y: e.clientY
};
});
// launch more rockets!!!
$(document).mousedown(function(e) {
for (var i = 0; i < 5; i++) {
launchFrom(Math.random() * SCREEN_WIDTH * 2 / 3 + SCREEN_WIDTH / 6);
}
});
function launch() {
launchFrom(SCREEN_WIDTH / 2);
}
function launchFrom(x) {
if (rockets.length < 10) {
var rocket = new Rocket(x);
rocket.explosionColor = Math.floor(Math.random() * 360 / 10) * 10;
rocket.vel.y = Math.random() * -3 - 4;
rocket.vel.x = Math.random() * 6 - 3;
rocket.size = 8;
rocket.shrink = 0.999;
rocket.gravity = 0.01;
rockets.push(rocket);
}
}
function loop() {
// update screen size
if (SCREEN_WIDTH != window.innerWidth) {
canvas.width = SCREEN_WIDTH = window.innerWidth;
}
if (SCREEN_HEIGHT != window.innerHeight) {
canvas.height = SCREEN_HEIGHT = window.innerHeight;
}
// clear canvas
context.fillStyle = "rgba(0, 0, 0, 0.0)";
context.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
var existingRockets = [];
for (var i = 0; i < rockets.length; i++) {
// update and render
rockets[i].update();
rockets[i].render(context);
// calculate distance with Pythagoras
var distance = Math.sqrt(Math.pow(mousePos.x - rockets[i].pos.x, 2) + Math.pow(mousePos.y - rockets[i].pos.y, 2));
// random chance of 1% if rockets is above the middle
var randomChance = rockets[i].pos.y < (SCREEN_HEIGHT * 2 / 3) ? (Math.random() * 100 <= 1) : false;
/* Explosion rules
- 80% of screen
- going down
- close to the mouse
- 1% chance of random explosion
*/
if (rockets[i].pos.y < SCREEN_HEIGHT / 5 || rockets[i].vel.y >= 0 || distance < 50 || randomChance) {
rockets[i].explode();
} else {
existingRockets.push(rockets[i]);
}
}
rockets = existingRockets;
var existingParticles = [];
for (var i = 0; i < particles.length; i++) {
particles[i].update();
// render and save particles that can be rendered
if (particles[i].exists()) {
particles[i].render(context);
existingParticles.push(particles[i]);
}
}
// update array with existing particles - old particles should be garbage collected
particles = existingParticles;
while (particles.length > MAX_PARTICLES) {
particles.shift();
}
}
function Particle(pos) {
this.pos = {
x: pos ? pos.x : 0,
y: pos ? pos.y : 0
};
this.vel = {
x: 0,
y: 0
};
this.shrink = .97;
this.size = 2;
this.resistance = 1;
this.gravity = 0;
this.flick = false;
this.alpha = 1;
this.fade = 0;
this.color = 0;
}
Particle.prototype.update = function() {
// apply resistance
this.vel.x *= this.resistance;
this.vel.y *= this.resistance;
// gravity down
this.vel.y += this.gravity;
// update position based on speed
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
// shrink
this.size *= this.shrink;
// fade out
this.alpha -= this.fade;
};
Particle.prototype.render = function(c) {
if (!this.exists()) {
return;
}
c.save();
c.globalCompositeOperation = 'lighter';
var x = this.pos.x,
y = this.pos.y,
r = this.size / 2;
var gradient = c.createRadialGradient(x, y, 0.1, x, y, r);
gradient.addColorStop(0.1, "rgba(255,255,255," + this.alpha + ")");
gradient.addColorStop(0.8, "hsla(" + this.color + ", 100%, 50%, " + this.alpha + ")");
gradient.addColorStop(1, "hsla(" + this.color + ", 100%, 50%, 0.1)");
c.fillStyle = gradient;
c.beginPath();
c.arc(this.pos.x, this.pos.y, this.flick ? Math.random() * this.size : this.size, 0, Math.PI * 2, true);
c.closePath();
c.fill();
c.restore();
};
Particle.prototype.exists = function() {
return this.alpha >= 0.1 && this.size >= 1;
};
function Rocket(x) {
Particle.apply(this, [{
x: x,
y: SCREEN_HEIGHT}]);
this.explosionColor = 0;
}
Rocket.prototype = new Particle();
Rocket.prototype.constructor = Rocket;
Rocket.prototype.explode = function() {
var count = Math.random() * 10 + 80;
for (var i = 0; i < count; i++) {
var particle = new Particle(this.pos);
var angle = Math.random() * Math.PI * 2;
// emulate 3D effect by using cosine and put more particles in the middle
var speed = Math.cos(Math.random() * Math.PI / 2) * 15;
particle.vel.x = Math.cos(angle) * speed;
particle.vel.y = Math.sin(angle) * speed;
particle.size = 10;
particle.gravity = 0.2;
particle.resistance = 0.92;
particle.shrink = Math.random() * 0.05 + 0.93;
particle.flick = true;
particle.color = this.explosionColor;
particles.push(particle);
}
};
Rocket.prototype.render = function(c) {
if (!this.exists()) {
return;
}
c.save();
c.globalCompositeOperation = 'lighter';
var x = this.pos.x,
y = this.pos.y,
r = this.size / 2;
var gradient = c.createRadialGradient(x, y, 0.1, x, y, r);
gradient.addColorStop(0.1, "rgba(255, 255, 255 ," + this.alpha + ")");
// gradient.addColorStop(1, "rgba(255, 255, 255, " + this.alpha + ")");
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
c.fillStyle = gradient;
c.beginPath();
c.arc(this.pos.x, this.pos.y, this.flick ? Math.random() * this.size / 2 + this.size / 2 : this.size, 0, Math.PI * 2, true);
c.closePath();
c.fill();
c.restore();
};
Thank you!
Creating gradients is expensive -- especially inside an animation loop.
It's more efficient is to pre-create a spritesheet of gradient exploding sprites before your app starts:
Create an in-memory canvas to act as a spritesheet,
Choose a dozen standard colors for you explosions.
Create gradient sprites in sequential order of exploding.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var ss=makeSpritesheet(10,15);
ctx.fillStyle='navy';
ctx.fillRect(0,0,cw,ch);
ctx.drawImage(ss,0,0);
function makeSpritesheet(maxRadius,colorCount){
var c=document.createElement('canvas');
var ctx=c.getContext('2d');
var spacing=maxRadius*2.5;
c.width=spacing*maxRadius;
c.height=spacing*(colorCount+1);
for(var colors=0;colors<colorCount;colors++){
var y=(colors)*spacing+spacing/2;
var color = parseInt(colors/colorCount*360);
for(r=2;r<=maxRadius;r++){
var x=(r-1)*spacing;
var gradient = ctx.createRadialGradient(x, y, 0, x, y, r);
gradient.addColorStop(0.2, "white");
gradient.addColorStop(0.7, 'hsla('+color+', 100%, 50%, 1)');
gradient.addColorStop(1.0, "rgba(0,0,0,0)");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
}
}
return(c);
}
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=640 height=512></canvas>
During Animation, draw the sprites from the spritesheet to your canvas.
Fade the opacity of each sprite by setting context.globalAlpha before drawing each sprite.

Categories