Tutorial

How To Develop an Interactive File Uploader with JavaScript and Canvas

Published on December 12, 2019
author

Luis Manuel

How To Develop an Interactive File Uploader with JavaScript and Canvas

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:

Dribble Shot by Jakub Antalík Credit: Jakub Antalik on dribble

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:

Creative Upload Interaction

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

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:

Creative Upload Interaction

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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Luis Manuel

author

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.