In this tutorial we will see how to implement a creative component to upload files, using JavaScript and the Canvas API. We will implement a rudimentary particle system and handle drag and drop events in the browser.

Introduction

How nice or fun can we make the interactions on a website or web application? The truth is that most could be better than we do today. For example, who would not want to use an application like this:

In this tutorial we will see how to implement a creative component to upload files, using as inspiration the previous animation by Jakub Antalík. The idea is to bring better visual feedback around what happens with the file after is dropped.

We will be focusing only on implementing the drag and drop interactions and some animations, without actually implementing all the necessary logic to actually upload the files to the server and use the component in production.

This is what our component will look like:

You can see the live demo or play with the code in Codepen. But if you also want to know how it works, just keep reading.

During the tutorial we will be seeing two main aspects:

  • We will learn how to implement a simple particle system using Javascript and Canvas.
  • We will implement everything necessary to handle drag and drop events.

In addition to the usual technologies (HTML, CSS, Javascript), to code our component we will use the lightweight animation library anime.js.

Step 1 — Creating the HTML Structure

canvas illustration for: Step 1 — Creating the HTML Structure

In this case our HTML structure will be quite basic:

				
					
<!-- Form to upload the files -->

<form class="upload" method="post" action="" enctype="multipart/form-data" novalidate="">

    <!-- The `input` of type `file` -->

    <input class="upload__input" name="files[]" type="file" multiple=""/>

    <!-- The `canvas` element to draw the particles -->

    <canvas class="upload__canvas"></canvas>

    <!-- The upload icon -->

    <div class="upload__icon"><svg viewBox="0 0 470 470"><path d="m158.7 177.15 62.8-62.8v273.9c0 7.5 6 13.5 13.5 13.5s13.5-6 13.5-13.5v-273.9l62.8 62.8c2.6 2.6 6.1 4 9.5 4 3.5 0 6.9-1.3 9.5-4 5.3-5.3 5.3-13.8 0-19.1l-85.8-85.8c-2.5-2.5-6-4-9.5-4-3.6 0-7 1.4-9.5 4l-85.8 85.8c-5.3 5.3-5.3 13.8 0 19.1 5.2 5.2 13.8 5.2 19 0z"></path></svg></div>

</form>

				
			

As you can see, we only need a form element and a file type input to allow the upload of files to the server. In our component we also need a canvas element to draw the particles and an SVG icon.

Keep in mind that to use a component like this in production, you must fill in the action attribute in the form, and perhaps add a label element for the input, etc.

Step 2 — Adding CSS Styles

We will be using SCSS as the CSS preprocessor, but the styles we are using are very close to being plain CSS and they are quite simple.

Let's start by positioning the form and canvas elements, among other basic styles:

				
					
// Position `form` and `canvas` full width and height

.upload, .upload__canvas {

  position: absolute;

  left: 0;

  top: 0;

  width: 100%;

  height: 100%;

}



// Position the `canvas` behind all other elements

.upload__canvas {

  z-index: -1;

}



// Hide the file `input`

.upload__input {

  display: none;

}

				
			

Now let's see the styles needed for our form, both for the initial state (hidden) and for when it is active (the user is dragging files to upload). The code has been commented exhaustively for a better understanding:

				
					
// Styles for the upload `form`

.upload {

  z-index: 1; // should be the higher `z-index`

  // Styles for the `background`

  background-color: rgba(4, 72, 59, 0.8);

  background-image: radial-gradient(ellipse at 50% 120%, rgba(4, 72, 59, 1) 10%, rgba(4, 72, 59, 0) 40%);

  background-position: 0 300px;

  background-repeat: no-repeat;

  // Hide it by default

  opacity: 0;

  visibility: hidden;

  // Transition

  transition: 0.5s;



  // Upload overlay, that prevent the event `drag-leave` to be triggered while dragging over inner elements

  &:after {

    position: absolute;

    content: '';

    left: 0;

    top: 0;

    width: 100%;

    height: 100%;

  }

}



// Styles applied while files are being dragging over the screen

.upload--active {

  // Translate the `radial-gradient`

  background-position: 0 0;

  // Show the upload component

  opacity: 1;

  visibility: visible;

  // Only transition `opacity`, preventing issues with `visibility`

  transition-property: opacity;

}

				
			

Finally, let's look at the simple styles that we have applied to the upload icon:

				
					
// Styles for the icon

.upload__icon {

  position: relative;

  left: calc(50% - 40px);

  top: calc(50% - 40px);

  width: 80px;

  height: 80px;

  padding: 15px;

  border-radius: 100%;

  background-color: #EBF2EA;



  path {

    fill: rgba(4, 72, 59, 0.8);

  }

}

				
			

Now our component looks like we want, so we're ready to add interactivity with Javascript.

Step 3 — Developing a Particle System

Before implementing the drag and drop functionality, let's see how we can implement a particle system.

In our particle system, each particle will be a simple Javascript Object with basic parameters to define how the particle should behave. And all the particles will be stored in an Array, which in our code is called particles.

Then, adding a new particle to our system is a matter of creating a new Javascrit Object and adding it to the particles array. Check the comments so you understand the purpose of each property:

				
					
// Create a new particle

function createParticle(options) {

    var o = options || {};

    particles.push({

        'x': o.x, // particle position in the `x` axis

        'y': o.y, // particle position in the `y` axis

        'vx': o.vx, // in every update (animation frame) the particle will be translated this amount of pixels in `x` axis

        'vy': o.vy, // in every update (animation frame) the particle will be translated this amount of pixels in `y` axis

        'life': 0, // in every update (animation frame) the life will increase

        'death': o.death || Math.random() * 200, // consider the particle dead when the `life` reach this value

        'size': o.size || Math.floor((Math.random() * 2) + 1) // size of the particle

    });

}

				
			

Now that we have defined the basic structure of our particle system, we need a loop function, which allows us to add new particles, update them and draw them on the canvas in each animation frame. Something like this:

				
					
// Loop to redraw the particles on every frame

function loop() {

    addIconParticles(); // add new particles for the upload icon

    updateParticles(); // update all particles

    renderParticles(); // clear `canvas` and draw all particles

    iconAnimationFrame = requestAnimationFrame(loop); // loop

}

				
			

Now let's see how we have defined all the functions that we call inside the loop. As always, pay attention to the comments:

				
					
// Add new particles for the upload icon

function addIconParticles() {

    iconRect = uploadIcon.getBoundingClientRect(); // get icon dimensions

    var i = iconParticlesCount; // how many particles we should add?

    while (i--) {

        // Add a new particle

        createParticle({

            x: iconRect.left + iconRect.width / 2 + rand(iconRect.width - 10), // position the particle along the icon width in the `x` axis

            y: iconRect.top + iconRect.height / 2, // position the particle centered in the `y` axis

            vx: 0, // the particle will not be moved in the `x` axis

            vy: Math.random() * 2 * iconParticlesCount // value to move the particle in the `y` axis, greater is faster

        });

    }

}



// Update the particles, removing the dead ones

function updateParticles() {

    for (var i = 0; i < particles.length; i++) {

        if (particles[i].life > particles[i].death) {

            particles.splice(i, 1);

        } else {

            particles[i].x += particles[i].vx;

            particles[i].y += particles[i].vy;

            particles[i].life++;

        }

    }

}



// Clear the `canvas` and redraw every particle (rect)

function renderParticles() {

    ctx.clearRect(0, 0, canvasWidth, canvasHeight);

    for (var i = 0; i < particles.length; i++) {

        ctx.fillStyle = 'rgba(255, 255, 255, ' + (1 - particles[i].life / particles[i].death) + ')';

        ctx.fillRect(particles[i].x, particles[i].y, particles[i].size, particles[i].size);

    }

}

				
			

And we have our particle system ready, where we can add new particles defining the options we want, and the loop will be responsible for performing the animation.

Adding animations for the upload icon

Now let's see how we prepare the upload icon to be animated:

				
					
// Add 100 particles for the icon (without render), so the animation will not look empty at first

function initIconParticles() {

    var iconParticlesInitialLoop = 100;

    while (iconParticlesInitialLoop--) {

        addIconParticles();

        updateParticles();

    }

}

initIconParticles();



// Alternating animation for the icon to translate in the `y` axis

function initIconAnimation() {

    iconAnimation = anime({

        targets: uploadIcon,

        translateY: -10,

        duration: 800,

        easing: 'easeInOutQuad',

        direction: 'alternate',

        loop: true,

        autoplay: false // don't execute the animation yet, only on `drag` events (see later)

    });

}

initIconAnimation();

				
			

With the previous code, we only need a couple of other functions to pause or resume the animation of the upload icon, as appropriate:

				
					
// Play the icon animation (`translateY` and particles)

function playIconAnimation() {

    if (!playingIconAnimation) {

        playingIconAnimation = true;

        iconAnimation.play();

        iconAnimationFrame = requestAnimationFrame(loop);

    }

}



// Pause the icon animation (`translateY` and particles)

function pauseIconAnimation() {

    if (playingIconAnimation) {

        playingIconAnimation = false;

        iconAnimation.pause();

        cancelAnimationFrame(iconAnimationFrame);

    }

}

				
			

Step 4 — Adding the Drag and Drop Functionality

Then we can start adding the drag and drop functionality to upload the files. Let's start by preventing unwanted behaviors for each related event:

				
					
// Preventing the unwanted behaviours

['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function (event) {

    document.addEventListener(event, function (e) {

        e.preventDefault();

        e.stopPropagation();

    });

});

				
			

Now we will handle the events of type drag, where we will activate the form so that it is shown, and we will play the animations for the upload icon:

				
					
// Show the upload component on `dragover` and `dragenter` events

['dragover', 'dragenter'].forEach(function (event) {

    document.addEventListener(event, function () {

        if (!animatingUpload) {

            uploadForm.classList.add('upload--active');

            playIconAnimation();

        }

    });

});

				
			

In case the user leaves the drop zone, we simply hide the form again and pause the animations for the upload icon:

				
					
// Hide the upload component on `dragleave` and `dragend` events

['dragleave', 'dragend'].forEach(function (event) {

    document.addEventListener(event, function () {

        if (!animatingUpload) {

            uploadForm.classList.remove('upload--active');

            pauseIconAnimation();

        }

    });

});

				
			

And finally the most important event that we must handle is the drop event, because it will be where we will obtain the files that the user has dropped, we will execute the corresponding animations, and if this were a fully functional component we would upload the files to the server through AJAX.

				
					
// Handle the `drop` event

document.addEventListener('drop', function (e) {

    if (!animatingUpload) { // If no animation in progress

        droppedFiles = e.dataTransfer.files; // the files that were dropped

        filesCount = droppedFiles.length > 3 ? 3 : droppedFiles.length; // the number of files (1-3) to perform the animations



        if (filesCount) {

            animatingUpload = true;



            // Add particles for every file loaded (max 3), also staggered (increasing delay)

            var i = filesCount;

            while (i--) {

                addParticlesOnDrop(e.pageX + (i ? rand(100) : 0), e.pageY + (i ? rand(100) : 0), 200 * i);

            }



            // Hide the upload component after the animation

            setTimeout(function () {

                uploadForm.classList.remove('upload--active');

            }, 1500 + filesCount * 150);



            // Here is the right place to call something like:

            // triggerFormSubmit();

            // A function to actually upload the files to the server



        } else { // If no files where dropped, just hide the upload component

            uploadForm.classList.remove('upload--active');

            pauseIconAnimation();

        }

    }

});

				
			

In the previous code snippet we saw that the function addParticlesOnDrop is called, which is in charge of executing the particle animation from where the files were dropped. Let's see how we can implement this function:

				
					
// Create a new particles on `drop` event

function addParticlesOnDrop(x, y, delay) {

    // Add a few particles when the `drop` event is triggered

    var i = delay ? 0 : 20; // Only add extra particles for the first item dropped (no `delay`)

    while (i--) {

        createParticle({

            x: x + rand(30),

            y: y + rand(30),

            vx: rand(2),

            vy: rand(2),

            death: 60

        });

    }



    // Now add particles along the way where the user `drop` the files to the icon position

    // Learn more about this kind of animation in the `anime.js` documentation

    anime({

        targets: {x: x, y: y},

        x: iconRect.left + iconRect.width / 2,

        y: iconRect.top + iconRect.height / 2,

        duration: 500,

        delay: delay || 0,

        easing: 'easeInQuad',

        run: function (anim) {

            var target = anim.animatables[0].target;

            var i = 10;

            while (i--) {

                createParticle({

                    x: target.x + rand(30),

                    y: target.y + rand(30),

                    vx: rand(2),

                    vy: rand(2),

                    death: 60

                });

            }

        },

        complete: uploadIconAnimation // call the second part of the animation

    });

}

				
			

Finally, when the particles reach the position of the icon, we must move the icon upwards, giving the impression that the files are being uploaded:

				
					
// Translate and scale the upload icon

function uploadIconAnimation() {

    iconParticlesCount += 2; // add more particles per frame, to get a speed up feeling

    anime.remove(uploadIcon); // stop current animations

    // Animate the icon using `translateY` and `scale`

    iconAnimation = anime({

        targets: uploadIcon,

        translateY: {

            value: -canvasHeight / 2 - iconRect.height,

            duration: 1000,

            easing: 'easeInBack'

        },

        scale: {

            value: '+=0.1',

            duration: 2000,

            elasticity: 800

        },

        complete: function () {

            // reset the icon and all animation variables to its initial state

            setTimeout(resetAll, 0);

        }

    });

}

				
			

To finish, we must implement the resetAll function, which resets the icon and all the variables to its initial state. We must also update the canvas size and reset the component on resize event. But in order not to make this tutorial any longer, we have not included these and other minor details, although you can check the complete code in the Github repository.

Conclusion

And finally our component is complete! Let's take a look:

You can check the live demo, play with the code on Codepen, or get the full code on Github.

Throughout the tutorial we saw how to create a simple particle system, as well as handle drag and drop events to implement an eye-catching file upload component.

Remember that this component is not ready to be used in production. In case you want to complete the implementation to make it fully functional, I recommend checking this excellent tutorial in CSS Tricks.