序章

WebサイトまたはWebアプリケーションで対話を行うことができるのはどれほど素晴らしいか楽しいですか? 真実は、ほとんどが今日よりも良くなる可能性があるということです。 たとえば、次のようなアプリケーションを使用したくない人は次のようになります。

Dribble Shot by Jakub Antalík
クレジット: Jakub Antalik on dribble

このチュートリアルでは、JakubAntalíkによる以前のアニメーションをインスピレーションとして使用して、ファイルをアップロードするためのクリエイティブコンポーネントを実装する方法を説明します。 アイデアは、ドロップされた後にファイルで何が起こるかについて、より良い視覚的フィードバックをもたらすことです。

dragdropのインタラクションと一部のアニメーションの実装にのみ焦点を当て、実際にファイルをサーバーにアップロードして本番環境でコンポーネントを使用するために必要なすべてのロジックを実装しません。

これは、コンポーネントがどのように見えるかです。

Creative Upload Interaction

ライブデモを表示するか、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リポジトリで完全なコードを確認できますが、これらおよびその他のマイナーな詳細は含まれていません。

結論

そして最後に、コンポーネントが完成しました。 見てみましょう:

Creative Upload Interaction

ライブデモを確認したり、Codepen コードを試してみたり、Githubフルコードを入手したりできます。

チュートリアル全体を通して、単純なパーティクルシステムを作成する方法と、dragおよびdropイベントを処理して人目を引くファイルアップロードコンポーネントを実装する方法を見てきました。

このコンポーネントは本番環境で使用する準備ができていないことに注意してください。 実装を完了して完全に機能させる場合は、CSSTricksでこの優れたチュートリアルを確認することをお勧めします。