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.

2 Answers

0
Marcus On

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.

0
Community On

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.