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.
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>
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.
I am using multiple setInterval() e.g. to create, move, delete the strings that fall on the screen
Problem is that the MODE 1 interval causes a lag for the interval1
I've also tried to switch to the MODE 2 STUFF but the lag still occurs...
How can this be improved?
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
var chinese = [`電`,`買`,`開`,`東`,`車`,`紅`,`馬`,`無`,`鳥`,`熱`,`時`,`佛`,`德`,`拜`,`黑`,`冰`,`兔`,`妒`,`壤`,`每`,`步`,`聽`,`實`,`證`,`龍`,`賣`,`龜`,`藝`,`戰`,`繩`,`關`,`鐵`,`圖`,`團`,`轉`,`廣`,`惡`,`豐`,`腦`,`雜`,`壓`,`雞`,`價`,`樂`,`氣`,`廳`,`發`,`勞`,`劍`,`歲`,`權`,`燒`,`贊`,`兩`,`譯`,`觀`,`營`,`處`,`學`,`體`,`點`,`麥`,`蟲`,`舊`,`會`,`萬`,`盜`,`寶`,`國`,`醫`,`雙`,`觸`,`參`];
var japanese = ['が','ぎ','ぐ','げ','ご','ゃ','ゅ','ょ','ざ','じ','ず','ぜ','ぞ','だ','づ','で','ど','ぢ','ば','ぶ','べ','ぼ','ぱ','ぴ', 'ぷ','ぺ','ぽ','あ','い','う','え','お','か','き','く','け','こ','き','さ','し','す','せ','そ','し','た','ち','つ','て','と','ち','な','に','ぬ','ね','の','に','は','ふ','へ','ほ','ひ','ま','み','む','め','も','や','ゆ','よ','ら','る','れ','ろ','り','わ','ゐ','ゑ','を'];
var characters = japanese;
var speedL = [60,80,90,100,120,140,160,180,200,220,240,280,300,400,500,600,700,800,810,820,830,840,850];
var speedDown = [4,5,6,7,8,9,10];
// MODE 2 STUFF
/*var timeStmpSleepMs = 480;
var timeStmp = Date.now();*/
function do_string(){
var string = document.createElement('section');
var sd = speedDown[Math.floor(Math.random()*speedDown.length)];
string.style.left = Math.floor(Math.random() * (document.documentElement.clientWidth-80)) + 'px';
var t = -2200;
string.style.top = t+'px';
var interval1 = setInterval(function(){
// MODE 2 STUFF
/*var newTimeStmp = Date.now();
if((newTimeStmp - timeStmp) > 480){
timeStmp = newTimeStmp;
//console.log('timeStmpSleepMs fired');
do_string();
}*/
if(parseInt(string.style.top) >= (document.documentElement.clientHeight-100)){
clearInterval(interval1);
string.remove();
return false;
}
string.style.top = (t += sd) + 'px';
}, 16);
function letter(){
var letter = document.createElement('div');
letter.innerHTML = characters[Math.floor(Math.random()*characters.length)];
string.appendChild(letter);
// CHANGING LETTER & DELETING LETTER
/*var interval2 = setInterval(function(){
if(parseInt(string.style.top) >= (document.documentElement.clientHeight-200)){
clearInterval(interval2);
letter.remove();
return false;
}
letter.innerHTML = characters[Math.floor(Math.random()*characters.length)];
}, speedL[Math.floor(Math.random()*speedL.length)]);*/
}
for(var i=0; i<getRandomInt(1, 180); i++){
letter();
document.body.appendChild(string);
}
}
do_string();
// MODE 1 STUFF
setInterval(function(){
do_string();
}, 480);
body {
font-size: 16px;
background: black;
color: #25ff00;
font-weight: bold;
font-family: monospace;
line-height: 20px;
}
section {
position: fixed;
}
Hey setInterval uses the same event loop to run, for better performance use requestAnimationFrame which runs on a separate event loop leaving the current one open for other things, see the code below
function doString(){
// Do your string stuff
var rid = requestAnimationFrame(doString);
}
doString();
If you want to control the FPS see this post
I'm making a scheduling calendar. The events are horizontal blocks (Google Cal has vertical ones). And because I have loads of events in one date I want the events to stack onto eachother without wasting any space, like this:
I did find plugins:
http://masonry.desandro.com/
http://isotope.metafizzy.co/
http://packery.metafizzy.co/
But I'm not keen on using a 30kb plugin just to do this simple thing.
To clarify: because this is a timeline, the div-events cannot move left/right, but must fit itself vertically amongst other div-events.
My own solution is a 2.6kb (commented) jQuery script, that uses absolute positioning for events and a div as a container for each row.
This script generates randomly size bars. Each new bar checks for space in each row from top-down. I'm using percentages, because that way calculating bar position against time will be easy (100% / 24h).
http://jsfiddle.net/cj23H/5/
Though works and is efficient enough, it feels bulky. You're welcome to improve.
jQuery code from my jsfiddle:
function rand() {
return Math.floor(Math.random() * 101);
}
function get_target_row(width, left) {
var i = 0;
var target_found = false;
// Loop through all the rows to see if there's space anywhere
while (target_found === false) {
// Define current row selector
var target_i = '.personnel#row' + i;
// Add row if none
if ($(target_i).length === 0) {
$('body').append('<div class="personnel" id="row' + i + '"></div>');
}
// See if there's space
if ($(target_i).children().length === 0) {
target_found = $(target_i);
} else {
var spaceFree = false;
// Check collision for each child
$(target_i).children().each(function () {
// Get left and right coordinates
var thisWidthPx = parseFloat($(this).css('width'), 10);
var thisWidthParentPx = parseFloat($(this).parent().css('width'), 10);
var thisWidth = (thisWidthPx / thisWidthParentPx * 100);
var thisLeftPx = parseFloat($(this).css('left'), 10);
var thisLeftParentPx = parseFloat($(this).parent().css('left'), 10);
var thisLeft = (thisLeftPx / thisWidthParentPx * 100);
var thisRight = thisLeft + thisWidth;
var right = left + width;
// Sexy way for checking collision
if ((right > thisLeft && right < thisRight) || (left < thisRight && left > thisLeft) || (thisLeft > left && thisLeft < right) || (thisRight < right && thisRight > left)) {
spaceFree = false;
return false;
} else {
spaceFree = true;
}
});
// If no children have collided break the loop
if (spaceFree === true) {
target_found = $(target_i);
}
}
i++;
}
// If after all the while loops target is still not found...
if (target_found === false) {
return false;
}
// Else, if target is found, return it.
return target_found;
}
// Generate random bars
for (var i = 0; i < 10; i++) {
var width = rand()/2;
var left = rand()/2;
var right = left + width;
var target = get_target_row(width, left);
target.append('<div class="block" style="width:' + width + '%;position:absolute;left:' + left + '%;background-color:rgba(' + rand() + ',' + rand() + ',' + rand() + ',0.4)" id="bar'+i+'">' + 'b' + i + '</div>');
}
CSS:
.block {
position: absolute;
background-color: rgba(200, 100, 100, 0.4);
height: 32px;
}
.personnel {
position: relative;
float: left;
width: 100%;
height: 33px;
}