Animation with JavaScript

An Animation Experiment


JavaScript is still a bit new to me, and I have yet to dive into the <canvas>.  I wanted to explore animation on web pages with vanilla Javascript. This seemed like a great opportunity to experiment, and share it for others to use.

If you haven't seen some of my older posts, I do have experience with python GUI libraries like tkinter and pygame, so I had some basic knowledge on my hands before playing with this code.


If you'd rather just dive in with your web dev tools or fork the github repo and play, here are the links:

webpage:https://nelliesnoodles.github.io/JS_Animation/
repo: https://github.com/nelliesnoodles/JS_Animation

Breakdown of the JavaScript


When I write my javascript, I organize it the same way I would a python file.

At the top most are the functions called and used by the functions below them.
At the bottom of the file will be the functions that get everything moving.

If a function doesn't require the use of any other function in the file, for example it set's DOM elements to global variables, it doesn't matter where it is. I try to put those near the top, because I might be using them in functions below them.  If it will return or get some value for me to be used in the future, it resides near the top.
If it starts everything rolling, such as:





window.addEventListener('load', (event) => {
   console.log("This page has been successfully loaded.");
   start(skynet);
});




I put it at the bottom of the page.  It will be using everything above it, or everything above it is somehow used after the page is loaded and something triggers a starting event.  Maybe a 'start' button or a form submission, or the user selecting a dark mode.

Many of the things I do may not be considered 'best practice', I honestly haven't been told it's wrong to organize things this way, but it never fails me, so it's what I do.

From top down


At the very top of the page are the variables and constants I will be using
--Start code block--



// JavaScript source code
var x = 0;
var y = 0;

var my_interval;


// change speed
const speed_interval = 50;
var speed = 1000;
var min_speed = 50;
var max_speed = 4000;

//change direction
const motion_interval = 5;
var right_motion = 5;
var down_motion = 5;
const max_right_motion = 200;
const max_down_motion = 200;
const min_right = 0;
const min_down = 0;




--End code block--

const vs var:

I'm using the var here for items I plan on letting the user change as it runs through button clicks.
By having these variables set like this at the top of the file, I can assign them new values in functions, and use those values globally throughout other functions.

I use constants const  where I do not want the value altered in any way by the user.
Something as simple as this really poses no threat, but it's a good practice to get into.  If you have a value that should never change, use your const declaration.

If you decide to play with these values, which I encourage, remember that a speed can not be <= 0.  This is used in our setInterval() function, and is a measurement of milliseconds between function calls.  In fact, I'll change that var min_speed, to const min_speed before I forget.

Stackoverflow:
https://stackoverflow.com/questions/9647215/what-is-minimum-millisecond-value-of-settimeout


resetting:


  The function remove_all_children() removes all children from the 'stars' element.  This is the DOM element I will be adding the 'circle' to.
  The circle is just a div with width, height, and a border-radius of 50%.  This bit of css is in a class that I will attach to a node child and append to the parent.
   I'll explain better when that function is next.  This one, because it returns nothing, and just alters something in the page, but it is used by other functions. So it get's a placement near the top of the file.

--start code block--


function remove_all_children() {
    var parent = document.getElementById('stars')
    if (parent.hasChildNodes()) {
        let children = parent.childNodes;

        for (let i = 0; i < children.length; i++) {
            node = children[i]
            node.remove()
        };
    }
}


--end code block--


The animation



This function draw_stars() was actually one of the first to be written.  When I set about this experiment, the first thing I needed to figure out, was how to add some visible element to the web page.

The arguments passed in to this function will determine the ball's location on the page.  It's x and y, or in css, it's top(y) and right||left(x) value.

It removes the old circle, or childNode with remove_all_children(), so that only the new node, in it's new location,  is in our 'stars' div.

I left the console.log in, but remember if you take the comments out and accidently leave this running, you might end up with a massive console.log count.

console.log(variable) is a great way to see what your code is doing.

I add the child to the parent with document.createElement.  After this div is created, I can attach the class to it with .classList.add('classname').  The class 'dots' is what makes the div shape into a circle.

*note about item.toString().*
This wouldn't work properly when I first wrote it because javascript did not like me appending the 'px' to an integer.  Sometimes it lets it pass, sometimes it says 'NOPE'.  So best to convert it before assigning it to my new top and left value.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString

Sometimes JavaScript will give errors when trying to work with integers also.  The parseInt(number) works well for when JavaScript can't decide when your value is a string or an integer. Or sometimes when it can't parse zero.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt

--start code block--

function draw_stars(left_x, top_y) {
    
    var parent = document.getElementById('stars')
    var child = document.createElement("div")
    remove_all_children()
    child.classList.add('dots')
    new_x = left_x.toString() + 'px'
    //console.log(new_x)
    new_y = top_y.toString() + 'px'
    child.style.top = new_y
    child.style.left = new_x
    //console.log(new_x)
    parent.appendChild(child)
    
};


--end code block--

The next function, move_star() is where the movement happens.

This will alter our position of the x and y coordinates as the setInterval runs.
It changes the position by assigning new value to our top variables x and y.
It then runs the draw_stars(left_x, top_y) to place the new circle onto the page.

z-index and transparency has been applied in the css file to make the ball visible even when placed behind the '.change_bar' where the user buttons live.

 Any function()  it uses are above it in the file. The calls always look up the file, not down.

--start code block--
function move_star() {
    max_y = window.innerHeight - 50
    max_x = window.innerWidth - 50
    //console.log(max_x)
    if (x > max_x) {
        x = 0
    }
    else {
        x += right_motion;
    }
    if (y > max_y) {
        y = 0
    }
    else {
        y += down_motion;
        
    }  
    draw_stars(x, y)



--end code block--


stop those intervals

The start and stop interval functions control when the animation from this point in the code.
when the run() is called, the function checks for a current 'my_interval'.  If there is one, it clears it, and then sets a new interval.  This interval calls the move_star, and the x and y positions and speeds are grabbed to make all the motion happen from there.  If a user wishes to stop the process, the can use the stop button. 

We stop each interval before adjusting any of the animation items, (x, y, speed) then run it again for each button click.

At first I did not have the if clause that checked if my_interval was already set in the run() function, and the ball did a bit of hopping about if the start button was spam clicked.  Opps.

--start code block--
function stop_run() {
    clearInterval(my_interval)

}
function run() {
    if (my_interval) {
        clearInterval(my_interval)
    }
    
    my_interval = setInterval(move_star, speed);
}

--end code block--

The next few functions control adding and subtracting from the x and y variables.  The user clicks a button and decrements or increments the x or y value, or speed.   I have max and min for these values set so that if the over click any button, it doesn't try to exceed the screen size, or ability of setInterval.

--start code block--


function slow_speed() {
    stop_run()
    if (speed < max_speed) {
        speed += speed_interval
    }

    /* alerts cause violations here 
    else {
        alert("You are at the set slowest speed.")
    }
    */
    run()
}


function increase_speed() {
    stop_run()
    if (speed > min_speed) {
        speed -= speed_interval
    }
    
    run()
}

function drop_plus() {
    stop_run()
    if (down_motion < max_down_motion) {
        down_motion += motion_interval
    }
    run()
}

function change_fall_left() {
    stop_run()
    if (right_motion < max_right_motion) {
        right_motion += motion_interval;
    }
    run()
}

function change_fall_right() {
    stop_run()
    if (right_motion > min_right) {
        right_motion -= motion_interval
    }
    run()
}

function drop_minus() {
    stop_run()
    if (down_motion > min_down) {
        down_motion -= motion_interval
    }
    run()

}


--end code block--


The next two functions will hide, or show the instructions when a user clicks on the instructions button.
If a user opens the instructions, we'll clear the interval.  They can start it back up with any button that alters or uses run().
The close button hides the instructions again, by setting the display to 'none'.

--start code block--


function close_inst() {
    element = document.getElementById("instructions")
    element.style.display = 'none'
}

function show_inst() {
    if (my_interval) {
        clearInterval(my_interval);
    }

    element = document.getElementById('instructions')
    element.style.display = 'flex'
}


--end code block--

resetting again:


This function reset_all() will be called on the click of the reset button.  It will change all my user altered variables back to their original setting, stop the interval, and remove any nodes in the 'stars'.
This calls two functions above it, so it is lower in the file.  Like I said, I don't know for sure that the written order of the function calls is worth all the trouble, but it has yet to fail me.


--start code block--


function reset_all() {
    speed = 1000
    right_motion = 5
    down_motion = 10
    x = 0
    y = 0
    stop_run()
    remove_all_children()
}


--end code block--

These last two pieces are the ones that make all the magic happen.  My Event Listeners control what happens if a button is clicked, calling the function that should be used when this event happens.  It's almost at the bottom, because, look at all those functions!

The final bit will call this 'setter'  when the web page has loaded.  If I try to set the event listeners before the page has been loaded, javascript will get a little grumpy and complain that many of the elements I'm trying to watch and listen to, don't exist.  Which they don't, until the page is loaded.

--start code block--


function set_EventListeners() {
    //test on each added eventlistener
    let start = document.getElementById('start')
    start.addEventListener('click', run)

    let speedUp = document.getElementById('speedPlus')
    speedUp.addEventListener('click', increase_speed)

    let slowdown = document.getElementById('speedMinus')
    slowdown.addEventListener('click', slow_speed)

    let moveMoreRight = document.getElementById('leftPlus')
    moveMoreRight.addEventListener('click', change_fall_left)

    let moveLessRight = document.getElementById('leftMinus')
    moveLessRight.addEventListener('click', change_fall_right)

    let dropMore = document.getElementById('downPlus')
    dropMore.addEventListener('click', drop_plus)

    let downLess = document.getElementById('downMinus')
    downLess.addEventListener('click', drop_minus)

    let stop = document.getElementById('stop')
    stop.addEventListener('click', stop_run)

    let reset = document.getElementById('reset')
    reset.addEventListener('click', reset_all)

    let show_about = document.getElementById("about")
    show_about.addEventListener('click', show_inst)

    let close_about = document.getElementById("close_instructions")
    close_about.addEventListener('click', close_inst)

    

}




window.addEventListener('load', (event) => {
   set_EventListeners()
});



--end code block--


That's the end of the javascript.

Now when I was building this, I didn't just write all this code and then run it for bugs.  I built it one piece at a time.  Started with making just the ball appear on the page with that appendChild(node).  Then I started tinkering with moving it.... Then added each button, and running it to see how well it functioned before moving on to the next button.

There's a bit of css to this to make things a little visually interesting.  When it started, I wanted to recreate that fabulous 90's screensaver stars, hence the dark mode.  If you don't have access to github,  I will copy the css, and the html below:


--  start CSS file --

body {
    width: 100vw;
    height: 100vh;
    background: radial-gradient(#393c50, black);
    background-repeat: no-repeat;
    
    
}

#instructions {
    display: none;
    flex-direction: column;
    justify-content: center;
    align-content: center;
    text-align: center;
    color: white;
    background: darkblue;
    position: absolute;
    top: 0;
    z-index: 2;
}
#stars {

    height: 100vh;
    width: 100vw;
    background: rgba(0, 0, 0, 0.3);
    position: relative;
    z-index: 2;
}

.dots {
    height: 30px;
    width: 30px;
    border-radius: 50%;
    border: 2px solid aqua;
    position: fixed;
    z-index: 10;
    background: white;
    box-shadow: rgba(0, 0, 0, 0.3) 3px 3px;
}

.change_bar {
    margin-top: 20px;
    width: 100vw;
    position: fixed;
    top: 0;
    height: 40px;
    background: transparent;
    display: flex;
    align-content: center;
    justify-content: center;
    z-index: 3;
}

button {
    font-size: 1em;
    padding: 1ch;
    margin-right: 2ch;
}

@media all and (max-width: 799px){
    .change_bar {
        position: relative;
        flex-direction: column;
        height: auto;
        width: 40vw;
        
    }
   
    body {
        display: flex;
        flex-direction: row;

    }
}


--end CSS file--


-- start HTML file--

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="draw_stars.js"></script>
    <link rel="stylesheet" href="stars_css.css" />

    <title>Javascript Animation Experiment</title>
</head>
<body>
    <div class="change_bar">
        <button id="about">Instructions</button>
        <button id="start">start</button>
        <button id="leftPlus">Right+</button>
        <button id="leftMinus">Right-</button>
        <button id="speedPlus">speed+</button>
        <button id="speedMinus">speed-</button>
        <button id="downPlus">drop+</button>
        <button id="downMinus">drop-</button>
        <button id="stop">stop</button>
        <button style="color: darkblue; border: 4px solid red;" id="reset">RESET</button>
    </div>
    <div id="instructions">
        <p>
            As the speed interval changes, the ball will appear to move faster.
            Along with speed, the rate at which the ball moves to the right, can also effect 'speed',
            as can the drop, down motion.
            <br />
            The ball will start in the upper left corner of the page
            <br />
            For instance, if you turn the speed and both right, and down to max, (set in the code), the ball will appear
            to be zipping diagonally across the screen.
            Consider the speed a frame rate,  and the drop/right motions as the direction.
            This was an experiment to play with animation with javascript.
            See the blog post here:

        </p>
        <button id="close_instructions">CLOSE</button>
    </div>

    <div id="stars">

    </div>
    


</body>
</html>



-- end HTML file--

There are lots of things you can do with this, but canvas is a much better tool.  I imagine it is far superior for drawing images to a surface.

Experiment of your own:


Add a way to reverse directions, create min and maximums by the size of the parent container, and when the ball reaches those, have it reverse direction, like a bounce.

Can other elements exist on top, or below the ball?

Could you make this ball an image or sprite and have it move along a scrolling background?
How would the sprite move, or jump?

Hack it, break it, fix it, have fun.
Why not?

Comments

Popular posts from this blog

JavaScript Ascii animation with while loops and console.log

playing with trigonometry sin in pygame

JavaScript and a Matrix