How to remove CSS animation from element in vanilla JS - javascript

I've got a square grid of n x n smaller square div elements that I want to illuminate in a sequence with a CSS background color animation. I have a function to generate a random array for the sequence. The trouble I'm having is that once a certain square has been illuminated once, if it occurs again within the array it won't illuminate a second time. I believe it's because once the element has been assigned the CSS animation, the animation can't trigger again on that element, and I can't figure a way to make it work. It's for a Responsive Web Apps course I'm taking, and the assessment stipulates that we're only to use vanilla JS, and that all elements must be created in JS and appended to a blank <body> in our index.html.
Each flash according to the sequence is triggered through a setTimeout function that loops through all elements in the array increasing it's timer by 1s for each loop (the animation length is 1s also).
Defining containers and child divs:
function createGameContainer(n, width, height) {
var container = document.createElement('div');
//CSS styling
container.style.margin = '50px auto'
container.style.width = width;
container.style.height = height;
container.style.display = 'grid';
// loop generates string to create necessary number of grid columns based on the width of the grid of squares
var columns = '';
for (i = 0; i < n; i++) {
columns += ' calc(' + container.style.width + '/' + n.toString() + ')'
}
container.style.gridTemplateColumns = columns;
container.style.gridRow = 'auto auto';
// gap variable to reduce column and row gap for larger grid sizes
// if n is ever set to less than 2, gap is hardcoded to 20 to avoid taking square root of 0 or a negative value
var gap;
if (n > 1) {
gap = 20/Math.sqrt(n-1);
} else {
gap = 20;
}
container.style.gridColumnGap = gap.toString() + 'px';
container.style.gridRowGap = gap.toString() + 'px';
container.setAttribute('id', 'game-container');
document.body.appendChild(container);
}
/*
function to create individual squares to be appended to parent game container
*/
function createSquare(id) {
var square = document.createElement('div');
//CSS styling
square.style.backgroundColor = '#333';
//square.style.padding = '20px';
square.style.borderRadius = '5px';
square.style.display = 'flex';
square.style.alignItems = 'center';
//set class and square id
square.setAttribute('class', 'square');
square.setAttribute('id', id);
return square;
}
/*
function to create game container and and squares and append squares to parent container
parameter n denotes dimensions of game grid - n x n grid
*/
function createGameWindow(n, width, height) {
window.dimension = n;
createGameContainer(n, width, height);
/*
loop creates n**2 number of squares to fill game container and assigns an id to each square from 0 at the top left square to (n**2)-1 at the bottom right square
*/
for (i = 0; i < n**2; i++) {
var x = createSquare(i);
document.getElementById('game-container').appendChild(x);
}
}
The CSS animation:
#keyframes flash {
0% {
background: #333;
}
50% {
background: orange
}
100% {
background: #333;
}
}
.flashing {
animation: flash 1s;
}
The code to generate the array:
function generateSequence(sequenceLength) {
var sequence = [];
for (i = 0; i < sequenceLength; i++) {
var random = Math.floor(Math.random() * (dimension**2));
// the following loop ensures each element in the sequence is different than the previous element
while (sequence[i-1] == random) {
random = Math.floor(Math.random() * (dimension**2));
}
sequence[i] = random;
};
return sequence;
}
Code to apply animation to square:
function flash(index, delay) {
setTimeout( function() {
flashingSquare = document.getElementById(index);
flashingSquare.style.animation = 'flashOne 1s';
flashingSquare.addEventListener('animationend', function() {
flashingSquare.style.animation = '';
}, delay);
}
I've also tried removing and adding a class again to try and reset the animation:
function flash(index, delay) {
setTimeout( function() {
flashingSquare = document.getElementById(index);
flashingSquare.classList.remove('flashing');
flashingSquare.classList.add('flashing');
}, delay);
}
And the function to generate and display the sequence:
function displaySequence(sequenceLength) {
var sequence = generateSequence(sequenceLength);
i = 0;
while (i < sequence.length) {
index = sequence[i].toString();
flash(index, i*1000);
i++;
}
}
Despite many different attempts and a bunch of research I can't figure a way to get the animations to trigger multiple times on the same element.

Try this one:
function flash(index, delay){
setTimeout( function() {
flashingSquare = document.getElementById(index);
flashingSquare.classList.add('flashing');
flashingSquare.addEventListener('animationend', function() {
flashingSquare.classList.remove('flashing');
}, delay);
});
}
Don't remove the animation, remove the class.
Remove the class direct AFTER the animation is done. So the browser have time to handle everything to do so. And when you add the class direct BEFORE you want the animation, the browser can trigger all needed steps to do so.
Your attempt to remove and add the class was good but to fast. I think the browser and the DOM optimize your steps and do nothing.

After some research, I figured out a work around. I rewrote the function so that the setTimeout was nested within a for loop, and the setTimeout nested within an immediately invoked function expression (which I still don't fully understand, but hey, if it works). The new function looks like this:
/*
function to display game sequence
length can be any integer greater than 1
speed is time between flashes in ms and can presently be set to 1000, 750, 500 and 250.
animation length for each speed is set by a corresponding speed class
in CSS main - .flashing1000 .flashing750 .flashing500 and .flashing250
*/
function displaySequence(length, speed) {
var sequence = generateSequence(length);
console.log(sequence);
for (i = 0; i < sequence.length; i++) {
console.log(sequence[i]);
// immediately invoked function expression
(function(i) {
setTimeout( function () {
var sq = document.getElementById(sequence[i]);
sq.classList.add('flashing' + speed.toString());
sq.addEventListener('animationend', function() {
sq.classList.remove('flashing' + speed.toString());
})
}, (speed * i))
})(i);
}
}
the CSS for each class:
#keyframes flash {
0% {
background: #333;
}
50% {
background: orange
}
100% {
background: #333;
}
}
.flashing1000 {
animation: flash 975ms;
}
.flashing750 {
animation: flash 725ms;
}
.flashing500 {
animation: flash 475ms;
}
.flashing250 {
animation: flash 225ms;
}
A few lazy work arounds, I know, but it works well enough.

Related

I'm stuck in a project about snow animation in javascript

I have to create a script that simulates the animation of snow in javascript (I would prefer without using canvas if possible). I'm stuck at this point and I don't know where the mistake is. when I run the program google chrome crashes and the console does not give me errors, I think the error is due to a loop or some incorrect statements, I am attaching the code hoping for your help!
var direction=true; //true=right false =left
var active=true; //if true run the cycle,false stops the cycle
function startSnow() {
var _snowflakes = new Array(30);
var wid=50; //distance from a snowflake to another
var counterX; //var for editing the x position
var counterY; //var for editing the y position
for (var i=0; i< 30; i++) //cycle that initializes snowflakes
{
_snowflakes[i] = document.createElement("img");
_snowflakes[i].setAttribute("src","snowflake.png"); // setting the image
_snowflakes[i].setAttribute("class", "snowflake"); //setting the css style
_snowflakes[i].style.visibility ="inherit"; //when the function is running snowflakes have to be visible, when it finishes they have to be invisible
_snowflakes[i].style.right = (wid*i)+"px"; //set the distance from the left margin
_snowflakes[i].style.top = "30px"; // set the distance from the top
document.getElementById("background").appendChild(_snowflakes[i]);
}
while(active)
{
move();
}
function move() //function that moves the snowflake
{
for(;;)
{
if(counterY>=600) //when the snowflake reaches 600px from the top the function has to stop and hide snowflakes
{
for(var i=0;i<30;i++)
_snowflakes[i].style.visibility = "hidden";
active=false;
break;
}
else
{
if ((counterY%50)==0)
{
direction=!direction //every 50 Y pixels the snoflake change direction
}
counterY++;
for(var i=0;i<30;i++)
{
_snowflakes[i].style.top = counterY+"px"; //Y movement
if (direction==true)
{
_snowflakes[i].style.right = (_snowflakes[i].offsetLeft+counterX) + "px"; //x right movement
counterX++;
}
else
{
_snowflakes[i].style.right = (_snowflakes[i].offsetLeft+counterX) + "px"; //x left movement
counterX--;
}
}
}
}
}
}
Indeed, your code gets into an infinite loop because your variable counterY is not initialised and so it has the value undefined. Adding one to undefined gives NaN, and so you never get to that break statement.
But more importantly, you need a different approach, because even if you fix this, you'll never see an animation happening. For actually seeing the animation, you need to give the browser time to update the display. So a while and for(;;) loop are out of the question.
Instead call move once, and inside that function, in the else block, call requestAnimationFrame(move). This will give the browser time to repaint before move is called a gain. Remove the for(;;) loop, and remove the break.
There is also a logical error in how you move horizontally. As you read the current offsetLeft it makes no sense to increment counterX as that will lead to increasing (relative) jumps to the right (or left). Instead you should just add (or subtract) 1 to offsetLeft -- you can do away with counterX. Also, don't assign that to style.right, but to style.left.
So everything put together (cf. comments where there is a change):
var direction=true;
var active=true;
function startSnow() {
var _snowflakes = new Array(30);
var wid=50;
// var counterX = 0; -- not used.
var counterY = 0; // Need to initialise!
for (var i=0; i< 30; i++) {
_snowflakes[i] = document.createElement("img");
_snowflakes[i].setAttribute("src", "snowflake.png");
_snowflakes[i].setAttribute("alt", "❄"); // added for when image not found
_snowflakes[i].setAttribute("class", "snowflake");
_snowflakes[i].style.visibility = "inherit";
_snowflakes[i].style.left = (wid*i)+"px"; // assign to style.left
_snowflakes[i].style.top = "30px";
document.getElementById("background").appendChild(_snowflakes[i]);
}
// No while loop. Just call move
move();
function move() {
//No for (;;) loop
if (counterY>=600) {
for (var i=0;i<30;i++) {
_snowflakes[i].style.visibility = "hidden"; // you forgot the underscore
}
active=false;
// No break -- function will just return
} else {
if ((counterY%50)==0) {
direction=!direction
}
counterY++;
for (var i=0;i<30;i++) {
_snowflakes[i].style.top = counterY+"px";
if (direction==true) {
// assign to style.left,
_snowflakes[i].style.left = (_snowflakes[i].offsetLeft+1) + "px"; // add 1
} else {
_snowflakes[i].style.left = (_snowflakes[i].offsetLeft-1) + "px"; // sub 1
}
}
requestAnimationFrame(move); // schedule next run of this function
}
}
}
startSnow(); // Need to start it!
#background { width: 800px; height: 800px }
.snowflake { position: absolute; font-size: 40px }
<div id="background">
</div>

Paperjs bounce animation

Theres some wierd behaviour when playing with Paperjs, i was trying to curve a line up with 7 points separately - which works fine once, but when trying to make the link overshoot and return to 3 different points (to create a bounce effect) doesn't seem to play ball. On the second if statement, the 'counter' variable doesnt seem to increase instead of decrease, '+ steps' instead of '- steps'.
Maybe i'm not using if statements properly in this case, or paperjs has some strange behaviour?
Heres the codepen for it in full, click above the blue line to trigger it off. . Following is one setInterval for one of the points of the segment.
var seg6first = true;
var seg6sec = false;
var seg6thir = false;
setInterval(function() {
if (seg6first == true) {
counter = counter - steps;
if (counter >= 230) {
path.segments[6].point.y = counter;
path.smooth(); }
else {
seg6first = false;
seg6sec = true;
}
}
if (seg6sec == true) {
counter = counter + steps;
if (counter <= 260) {
path.segments[6].point.y = counter;
path.smooth();}
else {
seg6sec = false;
seg6thir = true;
}
}
if (seg6sec == true) {
counter = counter - steps;
if (counter >= 250) {
path.segments[6].point.y = counter;
path.smooth(); }
else {
seg6thir = false;
}
}
}, mintiming);
Thanks!
Rather than manually building your bounce effect, you can use an animation library like GSAP.
It has a lot of features that will make your task easier (see easing documentation).
Here is an example of what you are trying to do (click on the canvas to animate the line).
html,
body {
margin: 0;
overflow: hidden;
height: 100%;
}
canvas[resize] {
width: 100%;
height: 100%;
}
<canvas id="canvas" resize></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.11.8/paper-full.min.js"></script>
<script type="text/paperscript" canvas="canvas">
// user defined constants
var SEGMENTS_COUNT = 6;
var CURVE_HEIGHT = 80;
var ANIMATION_DURATION = 2;
// init path
var path = new Path({
fillColor: 'orange',
selected: true
});
// add points
for (var i = 0; i <= SEGMENTS_COUNT; i++) {
path.add([view.bounds.width * i / SEGMENTS_COUNT, view.center.y]);
}
// on mouse down...
function onMouseDown() {
// ...animate points
for (var i = 0, l = path.segments.length; i < l; i++) {
// get a reference to the point
var point = path.segments[i].point;
// calculate offset using sine function to form a curve
var offset = CURVE_HEIGHT * Math.sin(point.x * Math.PI / view.bounds.width);
// register animation
TweenLite.fromTo(
// target
point,
// duration
ANIMATION_DURATION,
// initial value
{ y: view.center.y },
{
// final value
y: view.center.y - offset,
// easing
ease: Elastic.easeOut.config(1, 0.3),
// on update...
onUpdate: function() {
// ...smooth the path
path.smooth();
}
}
);
}
}
</script>

How to assign each variable a partner variable?

Sorry, if the title doesn't make too much sense, it's just that i don't even know what my problem is to begin trying to solve it. This code was meant to execute a function every second for 10 times that adds a div element, gives it a class, gives it a starting left position and starts moving it left. I've succeeded to create the div, give it a class and append it but where the problem comes in is making it move left.
I wanted to move it by having a variable that keeps track of the div left attribute, subtracting from that variable and always setting the divs left attribute to be equal to that variable, but it doesn't seem to be working correctly because all created divs follow one of that position tracking variable.
setInterval(function() {
var Count = 0;
var createddiv = document.createElement("div");
var divX = 1000;
if (Count < 1000) {
Count = Count + 100;
createddiv.classList.add("NewDiv");
createddiv.style.left = divX + "px";
document.body.appendChild(createddiv);
divX = divX - 100;
createddiv.style.left = divX + "px";
}
}, 1000)
From what I understand you want to move div from right to left in 10 seconds
JS
(function() {
var Count = 0;
var divX = 1000;
var createddiv;
var intervalRef = setInterval(function() {
if (Count < 1000) {
if(createddiv !== undefined) {
createddiv.parentNode.removeChild(createddiv);
}
createddiv = document.createElement("div");
Count = Count + 100;
createddiv.classList.add("NewDiv");
createddiv.style.left = divX + "px";
document.body.appendChild(createddiv);
divX = divX - 100;
createddiv.style.left = divX + "px";
} else {
clearInterval(intervalRef);
}
}, 1000);
})();
CSS
.NewDiv {
height: 100px;
width: 100px;
background-color: red;
position: relative;
}
Here is working demo in jsfiddle
Suggestion instead of creating new div every time, add one div and change it's left property.

How to add elements fast but not instantly in javascript?

I would like to fill an element with dots in random order. I have managed to write all the functionality, but I am not satisfied with the execution speed.
If I add all of the points using a while loop, the points just seem to appear all at the same time.
Therefore I add points one by one using a function that I call recursively with a timeout. This, on the other hand, appears too slow. Is there any chance to run a sequence of actions slower than in a loop but faster than setTimeout() can?
var dotCellSize;
var initialOffset;
var slotsHorizontally;
var slotsVertically;
var container;
var redDots;
var dots;
var newDotElement = $('<div class="dot">');
function randomInteger(min,max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function addDots()
{
if (!dots.length)
return;
var dotIndex = randomInteger(0, dots.length - 1);
var dot = dots[dotIndex];
dots.splice(dotIndex, 1);
var column = dot % slotsHorizontally;
var row = Math.floor(dot/slotsHorizontally);
var position = {
left: initialOffset + column*dotCellSize,
top: initialOffset + row*dotCellSize
};
var dotElement = newDotElement.clone().css(position);
if (-1 != redDots.indexOf(dot))
dotElement.addClass('red');
dotElement.appendTo(container);
setTimeout(function() {
addDots();
}, 1);
}
function generateDots(dotContainer, cellSize, numberOfRedDots)
{
container = dotContainer;
dotCellSize = cellSize;
dots = [];
redDots = [];
container.find('div.dot').remove();
numberOfRedDots = typeof numberOfRedDots !== 'undefined' ? numberOfRedDots : 3;
initialOffset = Math.floor(dotCellSize/2);
slotsHorizontally = Math.ceil(container.width()/dotCellSize);
slotsVertically = Math.ceil(container.height()/dotCellSize);
var numberOfSlots = slotsHorizontally*slotsVertically;
while (dots.length < numberOfSlots)
dots.push(dots.length);
while (redDots.length < numberOfRedDots)
{
var newRedDot = randomInteger(0, numberOfSlots - 1);
if (-1 == redDots.indexOf(newRedDot))
redDots.push(newRedDot);
}
addDots();
}
generateDots($('.dot-container'), 18, 15);
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #C0E3EA;
position: absolute;
z-index: 1;
}
.dot.red {
background-color: #EF3D48;
}
.dot-container {
width: 420px;
height: 280px;
background-color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="dot-container"></div>
Unfortunately, not really, this is because of how the browser engine decides to repaint the screen. Without the timeout, the browser engine recognizes it's going to do a bunch of updates (adding the dots to the DOM). Because repainting the screen is expensive, it waits to do as much as possible at one time, and, in your case, all of the dots show up at once. With the timeout added, each call to your function gets "deferred" for future execution.
This may or may not happen "right away" and is non-trivial to explain in detail so I would recommend watching this video https://www.youtube.com/watch?v=8aGhZQkoFbQ which explains the JS event loop or read some articles on browser reflow:
Minimizing browser reflow
What is Layout Thrashing?
Without changing much of what you've already done, one solution is to batch a few of the dots to be drawn together. I've added a for loop to your function which will make five dots get drawn together. Adjust this to 10, 20, or higher and you'll see the dots get painted even faster. I hope there is a number that you'll find suitable. I understand you may want to just speed up the drawing of every dot individually, but bear in mind that screens have refresh rates, so the faster you want the routine to finish the more they will appear in batches any way.
var dotCellSize;
var initialOffset;
var slotsHorizontally;
var slotsVertically;
var container;
var redDots;
var dots;
var newDotElement = $('<div class="dot">');
function randomInteger(min,max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function addDots()
{
if (!dots.length)
return;
for (let i = 0; i < 5; i++) {
var dotIndex = randomInteger(0, dots.length - 1);
var dot = dots[dotIndex];
dots.splice(dotIndex, 1);
var column = dot % slotsHorizontally;
var row = Math.floor(dot/slotsHorizontally);
var position = {
left: initialOffset + column*dotCellSize,
top: initialOffset + row*dotCellSize
};
var dotElement = newDotElement.clone().css(position);
if (-1 != redDots.indexOf(dot))
dotElement.addClass('red');
dotElement.appendTo(container);
}
setTimeout(function() {
addDots();
}, 1);
}
function generateDots(dotContainer, cellSize, numberOfRedDots)
{
container = dotContainer;
dotCellSize = cellSize;
dots = [];
redDots = [];
container.find('div.dot').remove();
numberOfRedDots = typeof numberOfRedDots !== 'undefined' ? numberOfRedDots : 3;
initialOffset = Math.floor(dotCellSize/2);
slotsHorizontally = Math.ceil(container.width()/dotCellSize);
slotsVertically = Math.ceil(container.height()/dotCellSize);
var numberOfSlots = slotsHorizontally*slotsVertically;
while (dots.length < numberOfSlots)
dots.push(dots.length);
while (redDots.length < numberOfRedDots)
{
var newRedDot = randomInteger(0, numberOfSlots - 1);
if (-1 == redDots.indexOf(newRedDot))
redDots.push(newRedDot);
}
addDots();
}
generateDots($('.dot-container'), 18, 15);
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #C0E3EA;
position: absolute;
z-index: 1;
}
.dot.red {
background-color: #EF3D48;
}
.dot-container {
width: 420px;
height: 280px;
background-color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="dot-container"></div>
Quickly profiling on my i7 3770k revealed that newDotElement.clone().css(position) took about .1 seconds. If you are running at 30 FPS, your frame time is .03 seconds. So you can see that Jquery clone is somewhat of a bottleneck.
However, your initial approach of drawing all the dots at once is sound, if you flag their styles to be "hidden". Then, when all the dots are added to the DOM, but are not visible, retrieve a list of their nodes (forgive the vanilla JS):
Array.from(document.getElementsByClassName("dot-container")[0].childNodes);
Now you can iterate over them and simply change their visibility style from "hidden" to "visible". As skyline3000 points out, the limit with setTimeout (or even requestAnimationFrame) is in the browser, and looping and setting one dot per iteration will take a little over 1 frame, which is actually a little slow. So you can write yourself a little abraction which per call will set a certain number of elements' visibility styles to "visible". By adjusting the quantity of dots you make visible per call, you will speed up or slow down the animation.
function showDots() {
var list = Array.from(document.getElementsByClassName("dot-container")[0].childNodes);
function draw(q) {
var e;
for (var i = 0; i < q; i++) {
if (list.length == 0) {
return;
}
e = list.shift();
e.style.visibility = "visible";
}
}
function callback() {
if (list.length == 0) {
return;
}
draw(4);
setTimeout(callback);
}
callback();
}

Fade in element by setting opacity with Javascript

I have decided to create a fade in animation effect using vanilla javascript.
This is the code for my fade in effect:
document.querySelector('.open-1_1').onclick = function() {
document.getElementById('about-frame').style.display = 'block';
for (opacity = 0; opacity < 1.1; opacity = opacity + 0.1)
{
setTimeout(function(){document.getElementById('about').style.opacity = opacity;},100)
}
};
What I am trying to do is incrementally increasing the opacity of the #about div from 0 to 1 by running through a for loop which is supposed to wait 100 miliseconds for every iteration of the loop
However the #about div goes from dark to opacity 1 after a set time without seeing the fade in effect.
What is wrong with my logic?
This for loop is not on a delay, it sets ten timeouts to take place in 100 miliseconds.
for (opacity = 0; opacity < 1.1; opacity = opacity + 0.1)
{
setTimeout(function(){document.getElementById('about').style.opacity = opacity;},100)
}
So the fade only takes 1 ms.
This on the other hand loops the MyFadeFunction 10 times over a one second period, which is what you are asking for.
var opacity = 0;
function MyFadeFunction() {
if (opacity<1) {
opacity += .1;
setTimeout(function(){MyFadeFunction()},100);
}
document.getElementById('about').style.opacity = opacity;
}
http://jsfiddle.net/dL02zqku/1/
Note var opacity in this example and MyFadeFunction() are global, not nested within the startup function, but called via a function call. This is so that the jquery library being used to call the function is not held in a closure state.
I tried Mr.Wayne's code, it's beautifully written, but I was trying to fade a lot of things at the same time and I could see my browser slowing down using his code. After trying a few options I came up with this:
function fading(){
var increment = 0.045;
var opacity = 0;
var instance = window.setInterval(function() {
document.getElementById('about').style.opacity = opacity
opacity = opacity + increment;
if(opacity > 1){
window.clearInterval(instance);
}
},100)
}
fading();
You can check it out here on jsfiddle :
https://jsfiddle.net/b12yqo7v/
main = $('#main');
opacity = 0;
setOpacity(main) {
if (this.opacity > 1) {
main.css('opacity', 1);
return;
}
setTimeout(() => {
opacity += 0.2;
main.css('opacity', opacity);
setOpacity(main);
}, 100);
}
document.querySelector('.open-1_1').onclick = function () {
document.getElementById('about-frame').style.display = 'block';
const about = document.getElementById('about');
let fade = setInterval(() => {
about.style.opacity += .02; // 500 milliseconds
if (about.style.opacity >= 1) {
clearInterval(fade);
}
}, 10); // 100 iterations per second
};

Categories

Resources