如何使用JavaScript和Canvas开发交互式文件上传器
介绍
我们可以在网站或 Web 应用程序上进行交互有多好或多有趣? 事实是,大多数人可能比我们今天做得更好。 例如,谁不想使用这样的应用程序:
在本教程中,我们将看到如何使用 Jakub Antalík 之前的 动画作为灵感来实现一个创意组件来上传文件。 这个想法是围绕文件被删除后发生的情况带来更好的视觉反馈。
我们将只专注于实现 drag
和 drop
交互和一些动画,而不会实际实现将文件实际上传到服务器并在生产中使用组件的所有必要逻辑。
这就是我们的组件的样子:
你可以看live demo或者玩Codepen中的代码。 但是,如果您还想知道它是如何工作的,请继续阅读。
在本教程中,我们将看到两个主要方面:
- 我们将学习如何使用 Javascript 和 Canvas 实现一个简单的粒子系统。
- 我们将实现处理
drag
和drop
事件所需的一切。
除了通常的技术(HTML、CSS、Javascript),我们将使用轻量级动画库 anime.js 来编写我们的组件。
第 1 步 - 创建 HTML 结构
在这种情况下,我们的 HTML 结构将非常基本:
<!-- 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>
如您所见,我们只需要一个 form
元素和一个 file
类型 input
来允许将文件上传到服务器。 在我们的组件中,我们还需要一个 canvas
元素来绘制粒子和一个 SVG 图标。
请记住,要在生产中使用这样的组件,您必须在表单中填写 action
属性,并且可能添加一个 label
元素作为输入等。
第 2 步 — 添加 CSS 样式
我们将使用 SCSS 作为 CSS 预处理器,但我们使用的样式非常接近纯 CSS,而且非常简单。
让我们首先定位 form
和 canvas
元素,以及其他基本样式:
// 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; }
现在让我们看看我们的 form
所需的样式,包括初始状态(隐藏)和活动时(用户正在拖动文件上传)。 为了更好地理解,代码已被详尽地注释:
// 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; }
最后,让我们看看我们应用于上传图标的简单样式:
// 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); } }
现在我们的组件看起来像我们想要的那样,所以我们准备添加与 Javascript 的交互性。
第三步——开发粒子系统
在实现 drag
和 drop
功能之前,让我们看看如何实现粒子系统。
在我们的粒子系统中,每个粒子都是一个简单的 Javascript Object
,带有基本参数来定义粒子的行为方式。 所有的粒子都将存储在 Array
中,在我们的代码中称为 particles
。
然后,向我们的系统添加一个新粒子就是创建一个新的 Javascrit Object
并将其添加到 particles
数组中。 检查评论,以便您了解每个属性的用途:
// 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 }); }
现在我们已经定义了粒子系统的基本结构,我们需要一个循环函数,它允许我们添加新粒子,更新它们并在每个动画帧的 canvas
上绘制它们。 像这样的东西:
// 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 }
现在让我们看看我们是如何定义在循环中调用的所有函数的。 与往常一样,请注意评论:
// 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); } }
我们已经准备好我们的粒子系统,我们可以添加新的粒子来定义我们想要的选项,并且循环将负责执行动画。
为上传图标添加动画
现在让我们看看我们如何准备要动画的上传图标:
// 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();
使用前面的代码,我们只需要几个其他函数来暂停或恢复上传图标的动画,视情况而定:
// 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); } }
第 4 步 — 添加拖放功能
然后我们可以开始添加 drag
和 drop
功能来上传文件。 让我们从防止每个相关事件出现不需要的行为开始:
// Preventing the unwanted behaviours ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function (event) { document.addEventListener(event, function (e) { e.preventDefault(); e.stopPropagation(); }); });
现在我们将处理 drag
类型的事件,我们将在其中激活 form
以使其显示,我们将播放上传图标的动画:
// 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(); } }); });
如果用户离开 drop
区域,我们只需再次隐藏 form
并暂停上传图标的动画:
// 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(); } }); });
最后,我们必须处理的最重要的事件是 drop
事件,因为我们将在其中获取用户放置的文件,我们将执行相应的动画,如果这是一个完整的功能我们将通过 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(); } } });
在前面的代码片段中,我们看到函数 addParticlesOnDrop
被调用,它负责执行从文件被丢弃的地方的粒子动画。 让我们看看我们如何实现这个功能:
// 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 }); }
最后,当粒子到达图标的位置时,我们必须向上移动图标,给人一种文件正在上传的感觉:
// 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); } }); }
最后,我们必须实现 resetAll
函数,它将图标和所有变量重置为其初始状态。 我们还必须更新 canvas
大小并在 resize
事件上重置组件。 但为了不再制作本教程,我们没有包含这些和其他小细节,尽管您可以在 Github 存储库 中查看完整的代码。
结论
最后我们的组件就完成了! 让我们来看看:
您可以查看现场演示,在Codepen上玩代码,或在Github上获取完整代码。
在整个教程中,我们看到了如何创建一个简单的粒子系统,以及处理 drag
和 drop
事件以实现一个引人注目的文件上传组件。
请记住,此组件尚未准备好在生产中使用。 如果您想完成实现以使其功能齐全,我建议您查看 CSS Tricks 中的 excellent tutorial。