JavaScriptとCanvasを使用してインタラクティブなファイルアップローダーを開発する方法
序章
WebサイトまたはWebアプリケーションで対話を行うことができるのはどれほど素晴らしいか楽しいですか? 真実は、ほとんどが今日よりも良くなる可能性があるということです。 たとえば、次のようなアプリケーションを使用したくない人は次のようになります。
クレジット: Jakub Antalik on dribble
このチュートリアルでは、JakubAntalíkによる以前のアニメーションをインスピレーションとして使用して、ファイルをアップロードするためのクリエイティブコンポーネントを実装する方法を説明します。 アイデアは、ドロップされた後にファイルで何が起こるかについて、より良い視覚的フィードバックをもたらすことです。
drag
とdrop
のインタラクションと一部のアニメーションの実装にのみ焦点を当て、実際にファイルをサーバーにアップロードして本番環境でコンポーネントを使用するために必要なすべてのロジックを実装しません。
これは、コンポーネントがどのように見えるかです。
ライブデモを表示するか、Codepenのコードで遊ぶことができます。 しかし、それがどのように機能するかについても知りたい場合は、読み続けてください。
チュートリアルでは、2つの主要な側面を確認します。
- 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スタイルを追加する
CSSプリプロセッサとしてSCSSを使用しますが、使用しているスタイルはプレーン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との対話機能を追加する準備が整いました。
ステップ3—パーティクルシステムの開発
drag
およびdrop
機能を実装する前に、パーティクルシステムを実装する方法を見てみましょう。
私たちのパーティクルシステムでは、各パーティクルは、パーティクルの動作を定義する基本的なパラメータを備えた単純なJavascriptObject
になります。 そして、すべてのパーティクルは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
イベントを処理して人目を引くファイルアップロードコンポーネントを実装する方法を見てきました。
このコンポーネントは本番環境で使用する準備ができていないことに注意してください。 実装を完了して完全に機能させる場合は、CSSTricksでこの優れたチュートリアルを確認することをお勧めします。