如何使用 JavaScript 和 Canvas 开发交互式文件上传程序

介绍

我們能在網站或網頁應用程式上進行互動有多麼愉快或有趣?事實是,大多數人可能比我們今天做得更好。

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

在本教程中,我们将看到如何实施一个创意组件来上传文件,使用以前的灵感(Jakov Antalík的动画)(https://dribbble.com/shots/4174256-Drag-and-Drop-to-upload)。

我们将专注于实现拖放拖放互动和一些动画,而不是实际上实现所有必要的逻辑,实际上将文件上传到服务器并在生产中使用该组件。

我们的组件将是这样的:

Creative Upload Interaction

你可以看到 直播演示或玩用 代码在 Codepen,但如果你也想知道它是如何工作的,只需继续阅读。

在教程中,我们将看到两个主要方面:

  • 我们将学习如何使用JavaScript和Canvas实现简单的粒子系统。
  • 我们将实现处理拖动放弃事件所需的一切。

除了通常的技术(HTML,CSS,JavaScript),我们还将使用轻量级动画库(Anime.js)。

第1步:创建HTML结构

在这种情况下,我们的HTML结构将是相当基本的:

1<!-- Form to upload the files -->
2<form class="upload" method="post" action="" enctype="multipart/form-data" novalidate="">
3    <!-- The `input` of type `file` -->
4    <input class="upload__input" name="files[]" type="file" multiple=""/>
5    <!-- The `canvas` element to draw the particles -->
6    <canvas class="upload__canvas"></canvas>
7    <!-- The upload icon -->
8    <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>
9</form>

正如你所看到的,我们只需要一个形式元素和一个文件类型的输入,以便将文件上传到服务器上。

请记住,要在生产中使用这样的组件,您必须在表单中填写行动属性,并可能为输入添加一个标签元素等。

步骤 2 – 添加 CSS 风格

我们将使用SCSS作为CSS预处理器,但我们正在使用的风格非常接近于简单的CSS,并且非常简单。

让我们从其他基本风格中定位形状面具元素开始:

 1// Position `form` and `canvas` full width and height
 2.upload, .upload__canvas {
 3  position: absolute;
 4  left: 0;
 5  top: 0;
 6  width: 100%;
 7  height: 100%;
 8}
 9
10// Position the `canvas` behind all other elements
11.upload__canvas {
12  z-index: -1;
13}
14
15// Hide the file `input`
16.upload__input {
17  display: none;
18}

现在让我们看看我们的形式所需的样式,无论是初始状态(隐藏)还是活动状态(用户正在拖动文件上传)。

 1// Styles for the upload `form`
 2.upload {
 3  z-index: 1; // should be the higher `z-index`
 4  // Styles for the `background`
 5  background-color: rgba(4, 72, 59, 0.8);
 6  background-image: radial-gradient(ellipse at 50% 120%, rgba(4, 72, 59, 1) 10%, rgba(4, 72, 59, 0) 40%);
 7  background-position: 0 300px;
 8  background-repeat: no-repeat;
 9  // Hide it by default
10  opacity: 0;
11  visibility: hidden;
12  // Transition
13  transition: 0.5s;
14
15  // Upload overlay, that prevent the event `drag-leave` to be triggered while dragging over inner elements
16  &:after {
17    position: absolute;
18    content: '';
19    left: 0;
20    top: 0;
21    width: 100%;
22    height: 100%;
23  }
24}
25
26// Styles applied while files are being dragging over the screen
27.upload--active {
28  // Translate the `radial-gradient`
29  background-position: 0 0;
30  // Show the upload component
31  opacity: 1;
32  visibility: visible;
33  // Only transition `opacity`, preventing issues with `visibility`
34  transition-property: opacity;
35}

最后,让我们看看我们已经应用于上传图标的简单风格:

 1// Styles for the icon
 2.upload__icon {
 3  position: relative;
 4  left: calc(50% - 40px);
 5  top: calc(50% - 40px);
 6  width: 80px;
 7  height: 80px;
 8  padding: 15px;
 9  border-radius: 100%;
10  background-color: #EBF2EA;
11
12  path {
13    fill: rgba(4, 72, 59, 0.8);
14  }
15}

现在我们的组件看起来像我们想要的,所以我们准备用JavaScript添加互动性。

步骤三:开发粒子系统

在实施功能之前,让我们看看如何实现粒子系统。

在我们的粒子系统中,每个粒子将是一个简单的JavaScript对象,具有基本参数来定义粒子应该如何行为,所有粒子将存储在一个阵列,在我们的代码中被称为粒子

然后,将新粒子添加到我们的系统是创建一个新的Javascrit对象,并将其添加到粒子阵列中。

 1// Create a new particle
 2function createParticle(options) {
 3    var o = options || {};
 4    particles.push({
 5        'x': o.x, // particle position in the `x` axis
 6        'y': o.y, // particle position in the `y` axis
 7        'vx': o.vx, // in every update (animation frame) the particle will be translated this amount of pixels in `x` axis
 8        'vy': o.vy, // in every update (animation frame) the particle will be translated this amount of pixels in `y` axis
 9        'life': 0, // in every update (animation frame) the life will increase
10        'death': o.death || Math.random() * 200, // consider the particle dead when the `life` reach this value
11        'size': o.size || Math.floor((Math.random() * 2) + 1) // size of the particle
12    });
13}

现在我们已经定义了我们粒子系统的基本结构,我们需要一个循环函数,这使我们能够添加新的粒子,更新它们,并在每个动画框中绘制它们的面板

1// Loop to redraw the particles on every frame
2function loop() {
3    addIconParticles(); // add new particles for the upload icon
4    updateParticles(); // update all particles
5    renderParticles(); // clear `canvas` and draw all particles
6    iconAnimationFrame = requestAnimationFrame(loop); // loop
7}

现在让我们看看我们如何定义了我们在循环中呼叫的所有函数,像往常一样,注意评论:

 1// Add new particles for the upload icon
 2function addIconParticles() {
 3    iconRect = uploadIcon.getBoundingClientRect(); // get icon dimensions
 4    var i = iconParticlesCount; // how many particles we should add?
 5    while (i--) {
 6        // Add a new particle
 7        createParticle({
 8            x: iconRect.left + iconRect.width / 2 + rand(iconRect.width - 10), // position the particle along the icon width in the `x` axis
 9            y: iconRect.top + iconRect.height / 2, // position the particle centered in the `y` axis
10            vx: 0, // the particle will not be moved in the `x` axis
11            vy: Math.random() * 2 * iconParticlesCount // value to move the particle in the `y` axis, greater is faster
12        });
13    }
14}
15
16// Update the particles, removing the dead ones
17function updateParticles() {
18    for (var i = 0; i < particles.length; i++) {
19        if (particles[i].life > particles[i].death) {
20            particles.splice(i, 1);
21        } else {
22            particles[i].x += particles[i].vx;
23            particles[i].y += particles[i].vy;
24            particles[i].life++;
25        }
26    }
27}
28
29// Clear the `canvas` and redraw every particle (rect)
30function renderParticles() {
31    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
32    for (var i = 0; i < particles.length; i++) {
33        ctx.fillStyle = 'rgba(255, 255, 255, ' + (1 - particles[i].life / particles[i].death) + ')';
34        ctx.fillRect(particles[i].x, particles[i].y, particles[i].size, particles[i].size);
35    }
36}

我们有我们的粒子系统准备好了,我们可以添加新的粒子来定义我们想要的选项,循环将负责执行动画。

添加上传图标的动画

现在让我们看看我们如何准备上传图标进行动画:

 1// Add 100 particles for the icon (without render), so the animation will not look empty at first
 2function initIconParticles() {
 3    var iconParticlesInitialLoop = 100;
 4    while (iconParticlesInitialLoop--) {
 5        addIconParticles();
 6        updateParticles();
 7    }
 8}
 9initIconParticles();
10
11// Alternating animation for the icon to translate in the `y` axis
12function initIconAnimation() {
13    iconAnimation = anime({
14        targets: uploadIcon,
15        translateY: -10,
16        duration: 800,
17        easing: 'easeInOutQuad',
18        direction: 'alternate',
19        loop: true,
20        autoplay: false // don't execute the animation yet, only on `drag` events (see later)
21    });
22}
23initIconAnimation();

在上面的代码中,我们只需要一些其他功能来暂停或恢复上传图标的动画,如有可能:

 1// Play the icon animation (`translateY` and particles)
 2function playIconAnimation() {
 3    if (!playingIconAnimation) {
 4        playingIconAnimation = true;
 5        iconAnimation.play();
 6        iconAnimationFrame = requestAnimationFrame(loop);
 7    }
 8}
 9
10// Pause the icon animation (`translateY` and particles)
11function pauseIconAnimation() {
12    if (playingIconAnimation) {
13        playingIconAnimation = false;
14        iconAnimation.pause();
15        cancelAnimationFrame(iconAnimationFrame);
16    }
17}

步骤 4 – 添加拖放功能

然后我们可以开始添加拖放拖放功能来上传文件。

1// Preventing the unwanted behaviours
2['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function (event) {
3    document.addEventListener(event, function (e) {
4        e.preventDefault();
5        e.stopPropagation();
6    });
7});

现在我们将处理类型的拖动事件,在那里我们将激活形式,以便显示它,我们将播放上传图标的动画:

1// Show the upload component on `dragover` and `dragenter` events
2['dragover', 'dragenter'].forEach(function (event) {
3    document.addEventListener(event, function () {
4        if (!animatingUpload) {
5            uploadForm.classList.add('upload--active');
6            playIconAnimation();
7        }
8    });
9});

如果用户离开放下区域,我们只会再次隐藏表格,并暂停上传图标的动画:

1// Hide the upload component on `dragleave` and `dragend` events
2['dragleave', 'dragend'].forEach(function (event) {
3    document.addEventListener(event, function () {
4        if (!animatingUpload) {
5            uploadForm.classList.remove('upload--active');
6            pauseIconAnimation();
7        }
8    });
9});

最后,我们必须处理的最重要的事件是下降事件,因为这将是我们获得用户丢失的文件的地方,我们将执行相应的动画,如果这是一个功能齐全的组件,我们会通过AJAX将文件上传到服务器。

 1// Handle the `drop` event
 2document.addEventListener('drop', function (e) {
 3    if (!animatingUpload) { // If no animation in progress
 4        droppedFiles = e.dataTransfer.files; // the files that were dropped
 5        filesCount = droppedFiles.length > 3 ? 3 : droppedFiles.length; // the number of files (1-3) to perform the animations
 6
 7        if (filesCount) {
 8            animatingUpload = true;
 9
10            // Add particles for every file loaded (max 3), also staggered (increasing delay)
11            var i = filesCount;
12            while (i--) {
13                addParticlesOnDrop(e.pageX + (i ? rand(100) : 0), e.pageY + (i ? rand(100) : 0), 200 * i);
14            }
15
16            // Hide the upload component after the animation
17            setTimeout(function () {
18                uploadForm.classList.remove('upload--active');
19            }, 1500 + filesCount * 150);
20
21            // Here is the right place to call something like:
22            // triggerFormSubmit();
23            // A function to actually upload the files to the server
24
25        } else { // If no files where dropped, just hide the upload component
26            uploadForm.classList.remove('upload--active');
27            pauseIconAnimation();
28        }
29    }
30});

在之前的代码片段中,我们看到称为addParticlesOnDrop的函数,该函数负责执行粒子动画,从那里丢掉了文件。

 1// Create a new particles on `drop` event
 2function addParticlesOnDrop(x, y, delay) {
 3    // Add a few particles when the `drop` event is triggered
 4    var i = delay ? 0 : 20; // Only add extra particles for the first item dropped (no `delay`)
 5    while (i--) {
 6        createParticle({
 7            x: x + rand(30),
 8            y: y + rand(30),
 9            vx: rand(2),
10            vy: rand(2),
11            death: 60
12        });
13    }
14
15    // Now add particles along the way where the user `drop` the files to the icon position
16    // Learn more about this kind of animation in the `anime.js` documentation
17    anime({
18        targets: {x: x, y: y},
19        x: iconRect.left + iconRect.width / 2,
20        y: iconRect.top + iconRect.height / 2,
21        duration: 500,
22        delay: delay || 0,
23        easing: 'easeInQuad',
24        run: function (anim) {
25            var target = anim.animatables[0].target;
26            var i = 10;
27            while (i--) {
28                createParticle({
29                    x: target.x + rand(30),
30                    y: target.y + rand(30),
31                    vx: rand(2),
32                    vy: rand(2),
33                    death: 60
34                });
35            }
36        },
37        complete: uploadIconAnimation // call the second part of the animation
38    });
39}

最后,当粒子达到图标的位置时,我们必须向上移动图标,产生文件正在上传的印象:

 1// Translate and scale the upload icon
 2function uploadIconAnimation() {
 3    iconParticlesCount += 2; // add more particles per frame, to get a speed up feeling
 4    anime.remove(uploadIcon); // stop current animations
 5    // Animate the icon using `translateY` and `scale`
 6    iconAnimation = anime({
 7        targets: uploadIcon,
 8        translateY: {
 9            value: -canvasHeight / 2 - iconRect.height,
10            duration: 1000,
11            easing: 'easeInBack'
12        },
13        scale: {
14            value: '+=0.1',
15            duration: 2000,
16            elasticity: 800
17        },
18        complete: function () {
19            // reset the icon and all animation variables to its initial state
20            setTimeout(resetAll, 0);
21        }
22    });
23}

最后,我们必须实施resetAll函数,该函数将图标和所有变量恢复到其初始状态,我们还必须更新面板大小,并在resize事件中重新设置组件,但为了不再使用本教程,我们没有包括这些和其他小细节,尽管您可以在Github存储中检查完整的代码(https://github.com/lmgonzalves/creative-upload)。

结论

最后,我们的组件已经完成了!让我们看看:

Creative Upload Interaction

你可以查看 直播演示,玩与 代码在 Codepen,或得到 完整的代码在Github

在整个教程中,我们看到如何创建一个简单的粒子系统,以及处理拖动下降事件,以实现令人眼花缭乱的文件上传组件。

请记住,此组件尚未准备好用于生产,如果您想完成实现以使其完全功能,我建议您检查此 优秀的CSS技巧教程

Published At
Categories with 技术
comments powered by Disqus