序章

HTML5では、多くの新しいタイプの input 属性が導入されました form などの要素 color, date, range、 などなど。 機能的にはこれらの新しいタイプですが input 動作しますが、Webアプリケーションの美的ニーズを満たさないことがよくあります。

これらを与えるために input モダンなデザインを入力すると、そのようなフロントを使用できます。このチュートリアルでは、 range input SVGを使用して描画するコンポーネントを使用する path およびanime.jsを使用してアニメーションを実行します。

このチュートリアルに従うと、作成するための基本的な手順がわかります。 range input 次のようなデザイン:

注:インスピレーションとして使用したこのオリジナルのアニメーションは、StanYakusevichによるこのドリブルショットにあります。

最終製品をご覧になりたい場合は、CodePenでチェックしてください。

ステップ1—HTMLとSVGを使用したマークアップのコーディング

この最初のステップでは、使用する主要なHTML構造を確認します。 コメントを読んで、詳細を見逃さないようにしてください。

<!-- Wrapper for the range input slider -->
<div class="range__wrapper">
    <!-- The real input will be hidden, but updated properly with Javascript -->
    <!-- For a production usage, you may want to add a label and also put it inside a form -->
    <input class="range__input" type="range" min="30" max="70" value="64"/>

    <!-- All the other elements will go here -->
</div>

ご覧のとおり、コンポーネントには実際のコンポーネントが含まれています input タイプの range、Javascriptで適切に更新します。 これを持っている input 共通のHTMLフォームの要素とコンポーネントを使用すると、 input (他のフォームデータと一緒に)上のサーバーに submit.

次に、理解を深めるためにコメントした、必要なSVG要素を見てみましょう。

<!-- SVG elements -->
<svg class="range__slider" width="320px" height="480px" viewBox="0 0 320 480">
    <defs>
        <!-- Range marks symbol. It will be reused below -->
        <symbol id="range__marks" shape-rendering="crispEdges">
            <path class="range__marks__path" d="M 257 30 l 33 0"></path>
            <path class="range__marks__path" d="M 268 60 l 22 0"></path>
            <path class="range__marks__path" d="M 278 90 l 12 0"></path>
            <path class="range__marks__path" d="M 278 120 l 12 0"></path>
            <path class="range__marks__path" d="M 278 150 l 12 0"></path>
            <path class="range__marks__path" d="M 278 180 l 12 0"></path>
            <path class="range__marks__path" d="M 278 210 l 12 0"></path>
            <path class="range__marks__path" d="M 278 240 l 12 0"></path>
            <path class="range__marks__path" d="M 278 270 l 12 0"></path>
            <path class="range__marks__path" d="M 278 300 l 12 0"></path>
            <path class="range__marks__path" d="M 278 330 l 12 0"></path>
            <path class="range__marks__path" d="M 278 360 l 12 0"></path>
            <path class="range__marks__path" d="M 278 390 l 12 0"></path>
            <path class="range__marks__path" d="M 268 420 l 22 0"></path>
            <path class="range__marks__path" d="M 257 450 l 33 0"></path>
        </symbol>
        <!-- This clipPath element will allow us to hide/show the white marks properly -->
        <!-- The `path` used here is an exact copy of the `path` used for the slider below -->
        <clipPath id="range__slider__clip-path">
            <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
        </clipPath>
    </defs>
    <!-- Pink marks -->
    <use xlink:href="#range__marks" class="range__marks__pink"></use>
    <!-- Slider `path`, that will be morphed properly on user interaction -->
    <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
    <!-- Clipped white marks -->
    <use xlink:href="#range__marks" class="range__marks__white" clip-path="url(#range__slider__clip-path)"></use>
</svg>

注:SVGを初めて使用する場合 path 要素またはそれらがどのように機能するかを理解していない場合は、Mozillaこのチュートリアルで詳細を学ぶことができます。

最後に、元のアニメーションに表示される値とテキストを表示するための別のコードが必要です。

<!-- Range values -->
<div class="range__values">
    <div class="range__value range__value--top">
        <!-- This element will be updated in the way: `100 - inputValue` -->
        <span class="range__value__number range__value__number--top"></span>
        <!-- Some text for the `top` value -->
        <span class="range__value__text range__value__text--top">
            <span>Points</span>
            <span>You Need</span>
        </span>
    </div>
    <div class="range__value range__value--bottom">
        <!-- This element will be updated with the `inputValue` -->
        <span class="range__value__number range__value__number--bottom"></span>
        <!-- Some text for the `bottom` value -->
        <span class="range__value__text range__value__text--bottom">
            <span>Points</span>
            <span>You Have</span>
        </span>
    </div>
</div>

それでは、スタイルを見てみましょう。

ステップ2—スタイルを追加する

スタイリングを開始します wrapper エレメント:

.range__wrapper {
  user-select: none; // disable user selection, for better drag & drop

  // More code for basic styling and centering...
}

ご覧のとおり、適切な外観を実現し、要素を中央に配置するための基本的なスタイルとは別に、コンポーネント内でユーザーが何かを選択する機能を無効にしました。 これは重要です。「ドラッグアンドドロップ」タイプのインタラクションを実装するため、「選択」機能を許可すると、予期しない動作が発生する可能性があります。

次に、実際のを非表示にします input 要素、および位置 svg (.range__slider)適切な要素:

// Hide the `input`
.range__input {
  display: none;
}

// Position the SVG root element
.range__slider {
  position: absolute;
  left: 0;
  top: 0;
}

SVG要素に色を付けるには、次のコードを使用します。

// Slider color
.range__slider__path {
  fill: #FF4B81;
}

// Styles for marks
.range__marks__path {
  fill: none;
  stroke: inherit;
  stroke-width: 1px;
}

// Stroke color for the `pink` marks
.range__marks__pink {
  stroke: #FF4B81;
}

// Stroke color for the `white` marks
.range__marks__white {
  stroke: white;
}

次に、値に使用される主なスタイルを見てみましょう。 ここに transform-origin プロパティは、元のアニメーションのように、数字をテキストに合わせて目的の方法で維持するために重要な役割を果たします。

// Positioning the container for values; it will be translated with Javascript
.range__values {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}

// These `transform-origin` values will keep the numbers in the desired position as they are scaled
.range__value__number--top {
  transform-origin: 100% 100%; // bottom-right corner
}
.range__value__number--bottom {
  transform-origin: 100% 0; // top-right corner
}

// More basic styles for the values...

ステップ3—JavaScriptとのインタラクションを追加する

次に、インタラクションを追加して、アニメーションを開始します。

まず、ドラッグアンドドロップ機能のシミュレーション、対応するイベントのリッスン、数学作業の実行、およびアニメーションの実行に必要なコードを見てみましょう。 コード全体ではなく、動作を理解するための基本的な部分のみが含まれていることに注意してください。

// Handle `mousedown` and `touchstart` events, saving data about mouse position
function mouseDown(e) {
    mouseY = mouseInitialY = e.targetTouches ? e.targetTouches[0].pageY : e.pageY;
    rangeWrapperLeft = rangeWrapper.getBoundingClientRect().left;
}

// Handle `mousemove` and `touchmove` events, calculating values to morph the slider `path` and translate values properly
function mouseMove(e) {
    if (mouseY) {
        // ... Some code for maths ...
        // After doing maths, update the value
        updateValue();
    }
}

// Handle `mouseup`, `mouseleave`, and `touchend` events
function mouseUp() {
    // Trigger elastic animation in case `y` value has changed
    if (mouseDy) {
        elasticRelease();
    }
    // Reset values
    mouseY = mouseDy = 0;
}

// Events listeners
rangeWrapper.addEventListener('mousedown', mouseDown);
rangeWrapper.addEventListener('touchstart', mouseDown);
rangeWrapper.addEventListener('mousemove', mouseMove);
rangeWrapper.addEventListener('touchmove', mouseMove);
rangeWrapper.addEventListener('mouseup', mouseUp);
rangeWrapper.addEventListener('mouseleave', mouseUp);
rangeWrapper.addEventListener('touchend', mouseUp);

今、私たちは見ることができます updateValue 関数。 この関数は、コンポーネント値を更新し、カーソル位置に対応してスライダーを移動する役割を果たします。 理解を深めるために、そのすべての部分について徹底的にコメントしました。

// Function to update the slider value
function updateValue() {
    // Clear animations if are still running
    anime.remove([rangeValues, rangeSliderPaths[0], rangeSliderPaths[1]]);

    // Calculate the `input` value using the current `y`
    rangeValue = parseInt(currentY * max / rangeHeight);
    // Calculate `scale` value for numbers
    scale = (rangeValue - rangeMin) / (rangeMax - rangeMin) * scaleMax;
    // Update `input` value
    rangeInput.value = rangeValue;
    // Update numbers values
    rangeValueNumberTop.innerText = max - rangeValue;
    rangeValueNumberBottom.innerText = rangeValue;
    // Translate range values
    rangeValues.style.transform = 'translateY(' + (rangeHeight - currentY) + 'px)';
    // Apply corresponding `scale` to numbers
    rangeValueNumberTop.style.transform = 'scale(' + (1 - scale) + ')';
    rangeValueNumberBottom.style.transform = 'scale(' + (1 - (scaleMax - scale)) + ')';

    // Some math calculations
    if (Math.abs(mouseDy) < mouseDyLimit) {
        lastMouseDy = mouseDy;
    } else {
        lastMouseDy = mouseDy < 0 ? -mouseDyLimit : mouseDyLimit;
    }

    // Calculate the `newSliderY` value to build the slider `path`
    newSliderY = currentY + lastMouseDy / mouseDyFactor;
    if (newSliderY < rangeMinY || newSliderY > rangeMaxY) {
        newSliderY = newSliderY < rangeMinY ? rangeMinY : rangeMaxY;
    }

    // Build `path` string and update `path` elements
    newPath = buildPath(lastMouseDy, rangeHeight - newSliderY);
    rangeSliderPaths[0].setAttribute('d', newPath);
    rangeSliderPaths[1].setAttribute('d', newPath);
}

これまで見てきたように、前の関数内で、 buildPath 関数。これは、コンポーネントの重要な部分です。 この関数を使用すると、 path スライダーの場合、次のパラメーターが与えられます。

  • dy:距離 y 以来、マウスが動かされた軸 mousedown また touchstart イベント。
  • ty:距離 y その軸 path 翻訳する必要があります。

また、 mouseX 上のカーソル位置に曲線を描画する値 x 軸、およびを返します pathString フォーマット:

// Function to build the slider `path`, using the given `dy` and `ty` values
function buildPath(dy, ty) {
    return 'M 0 ' + ty + ' q ' + mouseX + ' ' + dy + ' 320 0 l 0 480 l -320 0 Z';
}

最後に、興味深い弾性効果を実現する方法を見てみましょう。

// Function to simulate the elastic behavior
function elasticRelease() {
    // Morph the paths to the opposite direction, to simulate a strong elasticity
    anime({
        targets: rangeSliderPaths,
        d: buildPath(-lastMouseDy * 1.3, rangeHeight - (currentY - lastMouseDy / mouseDyFactor)),
        duration: 150,
        easing: 'linear',
        complete: function () {
            // Morph the paths to the normal state, using the `elasticOut` easing function (default)
            anime({
                targets: rangeSliderPaths,
                d: buildPath(0, rangeHeight - currentY),
                duration: 4000,
                elasticity: 880
            });
        }
    });

    // Here will go a similar code to:
    // - Translate the values to the opposite direction, to simulate a strong elasticity
    // - Then, translate the values to the right position, using the `elasticOut` easing function (default)
}

ご覧のとおり、元のアニメーションと同様に、誇張された弾性効果を実現するには、2つの連続したアニメーションを実装する必要がありました。 これは、を使用する単一のアニメーションが elasticOut イージング機能だけでは不十分です。

結論

このチュートリアルでは、の動作をシミュレートするコンポーネントを開発しました input タイプの range、ただし、元のアニメーションと同様の印象的な効果があります。

最終結果を確認したり、Codepen コードを試してみたり、Github完全なコードを取得したりできます。