Integrate GSAP with canvas to make a curvy timeline - javascript

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

Related

How to animate multiple HTML5 canvas objects one after another?

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>

Stopping animated rain in Canvas smmoothly

Rain Picture HereSo my issue isn't stopping the rain its stopping the making of the rain so the already rendered rain completes its animation off the screen.
I tried setInterval and setTimeout in JS but it just freezes.
And using JQuery to remove canvas tag takes off all drops at once.
Any ideas or direction would be great!
var canvas = document.getElementById("rainCanvas");
var ctx = canvas.getContext("2d");
var canvasW = window.innerWidth;
var canvasH = window.innerHeight;
canvas.height = canvasH;
canvas.width = canvasW;
var mf = 70;
var drops = [];
for(var i = 0; i < mf; i++){
drops.push({
x: Math.random()*canvasW,
y: Math.random()*canvasH,
r: Math.random()*5+2,
d: Math.random() + 1
})
}
function fill() {
ctx.fill();
}
function drawRain(){
ctx.clearRect(0, 0, canvasW, canvasH);
ctx.fillStyle = "rgba(255, 255, 255, .5)";
ctx.beginPath();
for(var i = 0; i < mf; i++){
var f = drops[i];
ctx.moveTo(f.x-5, f.y);
ctx.lineTo(f.x, f.y-15);
ctx.lineTo(f.x+5, f.y);
ctx.arc(f.x, f.y + f.r*.7,5, 0, Math.PI, false);
}
fill();
moveRain();
}
function moveRain(){
for(var i = 0; i < mf; i++){
var f = drops[i];
f.y += Math.pow(f.d, 2) + 1;
if(f.y > canvasH){
drops[i] = {x: Math.random()*canvasW, y: 0, r: f.r, d: f.d};
}
}
}
var i = setInterval(drawRain, 20);
setTimeout(function( ) { clearInterval(); }, 2000);
canvas{ background-color: black }
<canvas id="rainCanvas"></canvas>
What you want to do is set a flag to stop the rain from wrapping back to the top in your function to stop raining:
var stopRain = false;
...
setTimeout(function( ) { stopRain = true; }, 2000);
Now inside moveRain when that variable is true instead of moving it back to the top remove it from the array. Once we removed all the drops we can then clear the interval as it's no longer needed:
function moveRain(){
for(var i = 0; i < mf; i++){
var f = drops[i];
f.y += Math.pow(f.d, 2) + 1;
if(f.y > canvasH){
if(stopRain) { // When we stop raining
drops.splice(i, 1); // Remove drop from array
mf--; // Make sure to update the "length"
if(mf<1) clearInterval(i); // If there are not more drops clear the interval
} else drops[i] = {x: Math.random()*canvasW, y: 0, r: f.r, d: f.d};
}
}
}
Fiddle Example
You could also use drops.length instead of using mf, this way you don't need to do mf--.
Here I fixed some of the code and added a rainDensity variable which controls the count of new raindrops. The original code was designed to have a fixed amount of raindrops, it had to be changed to achieve the desired effect.
window.onload = function(){
var canvas = document.getElementById("rainCanvas");
var ctx = canvas.getContext("2d");
var canvasW = window.innerWidth;
var canvasH = window.innerHeight;
canvas.height = canvasH;
canvas.width = canvasW;
var drops = [];
function makeDrop() {
drops.push({
x: Math.random()*canvasW,
y: -10,
r: Math.random()*4+1,
d: Math.pow(Math.random() + 1, 2) + 1
})
}
function drawRain(){
ctx.clearRect(0, 0, canvasW, canvasH);
ctx.fillStyle = "rgba(128, 128, 255, .5)";
for(var f of drops){
ctx.beginPath();
ctx.moveTo(f.x-5, f.y);
ctx.lineTo(f.x, f.y-15);
ctx.lineTo(f.x+5, f.y);
ctx.arc(f.x, f.y + f.r*.7,5, 0, Math.PI, false);
ctx.fill();
}
}
function handleFrame() {
drawRain();
updateRain();
}
function updateRain() {
moveRain();
makeNewDrops();
}
var dropPerFrameCounter = 0;
var startTime = Date.now();
function makeNewDrops() {
var elapsedTime = (Date.now() - startTime) / 1000;
// rainDensity: set it to 0 to stop rain
var rainDensity = Math.max(0, Math.sin(elapsedTime / 3) + 0.5);
dropPerFrameCounter += rainDensity;
while (dropPerFrameCounter >= 1) {
dropPerFrameCounter--;
makeDrop();
}
}
function moveRain() {
for(var f of drops){
f.y += f.d;
}
drops = drops.filter(d => d.y < canvasH);
}
var intervalRender = setInterval(handleFrame, 20);
}
<canvas id="rainCanvas"></canvas>
Had to add an answer because the example all used setInterval to do the animation which looks awful. Use requestAnimationFrame to animate. Also fixed some other problems with the code.
I have added a pause the pauses the animation when you click on the canvas. Clicking again will continue.
var canvas = document.getElementById("rainCanvas");
var ctx = canvas.getContext("2d");
var canvasW = window.innerWidth;
var canvasH = window.innerHeight;
canvas.height = canvasH;
canvas.width = canvasW;
var mf = 70;
var drops = [];
var paused = false;
for(var i = 0; i < mf; i++){
drops.push({
x: Math.random()*canvasW,
y: Math.random()*canvasH,
r: Math.random()*5+2,
d: Math.random() + 1
})
}
function drawRain(){
if(!paused){
ctx.clearRect(0, 0, canvasW, canvasH);
ctx.fillStyle = "rgba(255, 255, 255, .5)";
ctx.beginPath();
for(var i = 0; i < mf; i++){
var f = drops[i];
ctx.moveTo(f.x - 2.5 * f.d, f.y);
ctx.lineTo(f.x, f.y - 7.5 * f.d);
ctx.lineTo(f.x + 2.5 * f.d, f.y);
ctx.arc(f.x, f.y + f.r * 0.7, 2.5 * f.d, 0, Math.PI, false);
}
ctx.fill();
for(var i = 0; i < mf; i++){
var f = drops[i];
f.y += Math.pow(f.d, 2) + 1;
if(f.y > canvasH + 15){ // make sure completely off the bottom
// dont create a new drop reuse it will stop GC from
// having to interrupt the code.
f.x = Math.random()*canvasW;
f.y = -6; // move off top
}
}
}
requestAnimationFrame(drawRain);
}
requestAnimationFrame(drawRain);
canvas.addEventListener("click",()=>{paused = ! paused});
canvas{
background-color: black;
position : absolute;
top : 0px;
left : 0px;
}
<canvas id="rainCanvas"></canvas>

p5.js Collision Detection

I'm trying to use p5.js's p5.collide2D library to execute some actions. I have a main pulsing module that pulses size-wise to music playing in my sketch, and then as it hits certain shapes I have displaying, I want it to reference the different functions I've set up to transform the main module visually.
Currently in this sketch I'm trying to get the Circle2 function to draw when touchX + touchY collides with the maroon circle. I thought I was using the library correctly, but maybe not. Any help would be great. Thanks!
var circles = [];
var squares = [];
var sizeProportion = 0.2;
//additional shapes
var r = 0;
var velocity = 1;
var fillColor = color(0, 0, 0);
var hit = false;
var startTime;
var waitTime = 3000;
var drawCircles;
var drawSquares;
function preload() {
sound = loadSound('assets/findingnemoegg.mp3');
}
function setup() {
createCanvas(windowWidth, windowHeight);
amplitude = new p5.Amplitude();
sound.loop();
sound.play();
startTime = millis();
}
function draw() {
background(255);
// other shapes + information
r = r + velocity;
if ((r > 256) || (r < 0)) {
velocity = velocity * -1;
}
noStroke();
fill(144, 12, 63, r);
ellipse(100, 100, 80, 80);
// drawing circles
circles.push(new Circle1(touchX, touchY));
for (var i = 0; i < circles.length; i++) {
circles[i].display();
if (circles[i].strokeOpacity <= 0) { // Remove if faded out.
circles.splice(i, 1); // remove
}
}
//collisions
if (pointTouchcircle2(touchX, 100, 100)) { // <- collision detection
//call upon Circle2 function and have the main module draw that
} else {
//stay the same.
}
}
//starting circles
function Circle1(x, y) {
this.x = x;
this.y = y;
this.size = 0;
this.age = 0;
this.fillOpacity = 20
this.strokeOpacity = 30
this.display = function() {
var level = amplitude.getLevel();
this.age++;
if (this.age > 500) {
this.fillOpacity -= 1;
this.strokeOpacity -= 1;
}
var newSize = map(level, 0, 1, 20, 900);
this.size = this.size + (sizeProportion * (newSize - this.size));
strokeWeight(10);
stroke(152, 251, 152, this.strokeOpacity);
fill(23, 236, 236, this.fillOpacity);
ellipse(this.x, this.y, this.size);
}
}
//maroon circles
function Circle2(x, y) {
this.x = x;
this.y = y;
this.size = 0;
this.age = 0;
this.fillOpacity = 20
this.strokeOpacity = 30
this.display = function() {
var level = amplitude.getLevel();
this.age++;
if (this.age > 500) {
this.fillOpacity -= 1;
this.strokeOpacity -= 1;
}
var newSize = map(level, 0, 1, 20, 900);
this.size = this.size + (sizeProportion * (newSize - this.size));
strokeWeight(10);
stroke(173, 212, 92, this.strokeOpacity);
fill(144, 12, 63, this.fillOpacity);
ellipse(this.x, this.y, this.size);
}
}
//collision functions
function pointTouchcircle2(touch, x, y) {
if (hit = collidePointCircle(touchX,touchY,100,100,50)) {
return true
} else {
return false
}
}

How to change the color of this (pure js)?

I am attempting to implement this mousefollow into my website:
http://codepen.io/hakimel/pen/KanIi
But I would like my own custom colors instead of what is default displayed. I changed the fillcolor value and was able to change to one specific color but I would like 4 different colors. So I have tried adding specific colors to this function next to fillColor: but no such luck. I also created a fillcolor2: property like:
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: '#bf3e27',
fillColor2: '#1c305c',
orbit: RADIUS*.1 + (RADIUS * .5 * Math.random())
};
and added:
context.fillStyle = particle.fillColor2;
context.strokeStyle = particle.fillColor2;
But that did not work either. I have also tried to copy and paste the same js code just to see if it would work, and just changed the fillcolor, but it would only display the last one pasted.
Can anyone show me how to get 4 separate colors the easiest way, I feel like I am vastly over-complicating this but obviously a beginner and getting rather frustrated with this?
Lastly, I would like the 4 different colors to span different radii and I messed around with the different RADIUS variables but it is pretty much impossible to figure out how to accomplish what I would like while only having one color. So there will be 4 of each color, I changed the QUANTITY to:
var QUANTITY = 16;
I need the first 4 colors radius of 10 so for the first one I set:
var RADIUS = 10;
Ideally I need the first 4 to be color (#AAAAAA) radius of 10 like it is, but need the second 4 to be color (#BBBBBBB) between radius 10 and 30, the third color (#CCCCCC) to be between radius of 30-50, and the last fourth color (#DDDDDD) to be between 50 and 70.
Any suggestions?
You could replace the definition of QUANTITY, COLOR and RADIUS with an array of these, and at the same time define ranges for RADIUS:
var GROUPS = [
{
QUANTITY: 4,
RADIUS: [ 5, 10],
COLOR: 0x888888
},
{
QUANTITY: 4,
RADIUS: [10, 30],
COLOR: 0xAA80AA
},
{
QUANTITY: 4,
RADIUS: [30, 50],
COLOR: 0xA0A0CC
},
{
QUANTITY: 4,
RADIUS: [50, 70],
COLOR: 0xFFE0E0
}
];
Then inside the createParticles function you would iterate over those GROUPS:
for (var g = 0; g < GROUPS.length; g++) {
var attribs = GROUPS[g];
for (var i = 0; i < attribs.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: '#' + attribs.COLOR.toString(16),
orbit: attribs.RADIUS[0] +
(attribs.RADIUS[1]-attribs.RADIUS[0]) * Math.random()
};
particles.push( particle );
}
}
Here is a snippet:
// One of my first <canvas> experiments, woop! :D
var SCREEN_WIDTH = window.innerWidth;
var SCREEN_HEIGHT = window.innerHeight;
var GROUPS = [
{
QUANTITY: 4,
RADIUS: [ 5, 10],
COLOR: 0x888888
},
{
QUANTITY: 4,
RADIUS: [10, 30],
COLOR: 0xAA80AA
},
{
QUANTITY: 4,
RADIUS: [30, 50],
COLOR: 0xA0A0CC
},
{
QUANTITY: 4,
RADIUS: [50, 70],
COLOR: 0xFFE0E0
}
];
var RADIUS_SCALE = 1;
var RADIUS_SCALE_MIN = 1;
var RADIUS_SCALE_MAX = 1.5;
var canvas;
var context;
var particles;
var mouseX = SCREEN_WIDTH * 0.5;
var mouseY = SCREEN_HEIGHT * 0.5;
var mouseIsDown = false;
function init() {
canvas = document.getElementById( 'world' );
if (canvas && canvas.getContext) {
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();
windowResizeHandler();
setInterval( loop, 1000 / 60 );
}
}
function createParticles() {
particles = [];
for (var g = 0; g < GROUPS.length; g++) {
var attribs = GROUPS[g];
for (var i = 0; i < attribs.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: '#' + attribs.COLOR.toString(16),
orbit: attribs.RADIUS[0] +
(attribs.RADIUS[1]-attribs.RADIUS[0]) * Math.random()
};
particles.push( particle );
}
}
}
function documentMouseMoveHandler(event) {
mouseX = event.clientX - (window.innerWidth - SCREEN_WIDTH) * .5;
mouseY = event.clientY - (window.innerHeight - SCREEN_HEIGHT) * .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) * .5;;
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .5;
}
}
function documentTouchMoveHandler(event) {
if(event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX - (window.innerWidth - SCREEN_WIDTH) * .5;;
mouseY = event.touches[0].pageY - (window.innerHeight - SCREEN_HEIGHT) * .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.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;
}
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;
body {
background-color: #000000;
padding: 0;
margin: 0;
overflow: hidden;
}
<canvas id='world'></canvas>
Define the settings for the particles in an array
// I've changed the color for better visibility
var ParticleSettings = [
{color: "#f00", radiusMin: 10, radiusMax: 10},
{color: "#0f0", radiusMin: 10, radiusMax: 30},
{color: "#00f", radiusMin: 30, radiusMax: 50}
];
To get the correct settings for a particle (in this case its index) use this function
function getParticleSettings(particleIndex) {
var settingsIndex = Math.floor(particleIndex / 4) % ParticleSettings.length,
settings = ParticleSettings[settingsIndex];
return {
color: settings.color,
radius: getRandom(settings.radiusMin, settings.radiusMax)
};
}
getRandom is just a small helper function to get a random number between two boundaries
function getRandom(min, max) {
return Math.floor((Math.random() * (max - min)) + min)
}
In createParticle we then have to change the for loop to get the settings for the current particle
for (var i = 0; i < QUANTITY; i++) {
// get the settings for the current particle
var particleSettings = getParticleSettings(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: particleSettings.color, // <--
orbit: particleSettings.radius // <--
};
particles.push( particle );
}
This would be the result of the changes above.
You could define the colors in an array since you are using a for loop you can index them like this:
var colors = ['#AAAAAA', '#BBBBBB', '#CCCCCC', '#DDDDDD'];
var particle = {
...
fillColor: colors[i],
...
}
This will now only work for the first four colors, but since you stated you have a quantity of 16, you could use 2 for loops to achieve this. Something like this:
//will run 16 times
for(var i = 0; i < QUANTITY; i++) {
for(var x = 0; x < 4; x++) {
var particle = {
//note that i'm using x here since the array has only 4 keys.
fillColor: colors[x];
}
}
}

JavaScript stroke fill on click

I'm trying to to make it possible for an image on a canvas to have a box over it with low opacity, to make it be shown that it has been selected. So I need an onclick function, but i'm stuck on what to do. Any advice? I would aslo appreciate if someone could point me in the right direction on how to make the box also pulsate.
var image1 = new Kinetic.Image({
image: imageObj,
x: xposition,
y: yposition,
width: width,
height: height,
stroke: 'blue',
srokeFill: 'red',
strokeWidth: 5,
draggable: true,
dragBoundFunc: function (pos) {
if (pos.x < this.minX)
this.minX = pos.x;
return {
x: pos.x,
y: this.getAbsolutePosition().y
}
}
});
layer.on('mouseover', function (evt) {
var shape = evt.target;
document.body.style.cursor = 'pointer';
shape.strokeEnabled(true);
layer.draw();
});
layer.on('mouseout', function (evt) {
var shape = evt.target;
document.body.style.cursor = 'default';
shape.strokeEnabled(false);
layer.draw();
});
When making a clickable region on a canvas you need to.
- Create a redraw function.
- Find the mouse point inside of canvas.
- Do collision detection on the mouse point and region.
update: added multiple clickable regions.
// x1, y1, x2, y2, clicked
var boxes = [{
x1: 10,
y1: 10,
x2: 110,
y2: 110,
clicked: false
}, {
x1: 120,
y1: 10,
x2: 220,
y2: 110,
clicked: false
}];
function go() {
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var w = can.width;
var h = can.height;
makeNoise(ctx);
var imageData = ctx.getImageData(0, 0, w, h);
var mx = 0;
var my = 0;
$('#can').mousemove(function(event) {
var offset = $(this).offset();
mx = event.pageX - offset.left;
my = event.pageY - offset.top;
redraw();
});
$('#can').click(function(event) {
var offset = $(this).offset();
mx = event.pageX - offset.left;
my = event.pageY - offset.top;
for (var i = 0, len = boxes.length; i < len; i++) {
var b = boxes[i];
if (hit(b.x1, b.y1, b.x2, b.y2)) {
b.clicked = !b.clicked;
}
}
redraw();
});
function redraw() {
// Draw the original image
ctx.putImageData(imageData, 0, 0);
// Draw the clickable region
for (var i = 0, len = boxes.length; i < len; i++) {
drawBox(boxes[i]);
}
// Draw the mouse. Probably only needed when testing
// mouse location.
drawMouse();
}
redraw();
// Collision dection of a square against mouse point.
// square points x1,y1 must be less than x2,y2
function hit(x1, y1, x2, y2) {
if (mx < x1 || my < y1 || mx > x2 || my > y2) return false;
return true;
}
function drawMouse() {
ctx.moveTo(mx, my);
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(mx, my, 5, 0, Math.PI * 360);
ctx.fill();
}
function drawBox(b) {
if (b.clicked) {
// Mouse clicked.
ctx.fillStyle = "rgba(255,0,255,.5)"
} else if (hit(b.x1, b.y1, b.x2, b.y2)) {
// Mouse Over
ctx.fillStyle = "rgba(255,255,255,.5)"
} else {
// Mouse Out
ctx.fillStyle = "red";
}
ctx.fillRect(b.x1, b.y1, b.x2 - b.x1, b.y2 - b.y1);
}
}
// Fill a canvas context with noise.
function makeNoise(ctx) {
var can = ctx.canvas;
var imageData = ctx.getImageData(0, 0, can.width, can.height);
var d = imageData.data;
for (var i = 0, len = d.length; i < len; i += 4) {
d[i] = d[i + 1] = d[i + 2] = (Math.random() * 8) << 4;
d[i + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
}
go();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="can" width="400" height="300"></canvas>

Categories

Resources