如何使用 SVG 和 anime.js 创建弹性范围输入法

简介

在HTML5中,为form元素引入了许多新类型的input属性,例如ColorDaterange等等。虽然从功能上讲,这些新类型的输入作品并不能满足网络应用程序的审美需求。

为了给这些input类型一个现代的设计,可以使用这样的正面,本教程将使用一个组件来模拟range``input的行为,使用svg绘制路径Anime.js来执行动画。

学习本教程后,您将了解创建如下range``input设计的基本步骤:

斯坦Yakusevich设计的范围输入

<$>[备注] 注: 这是我们用来作为灵感的原创动画,可以在斯坦·雅库塞维奇的This Drobble shotby Stan Yakusevich]上找到。 <$>

如果您想看到最终产品,请在CodePen.上查看

第一步-使用Html和SVG编码标记

在第一步中,我们将看到我们将使用的主要HTML结构。请阅读评论,这样你就不会错过任何细节:

1<!-- Wrapper for the range input slider -->
2<div class="range__wrapper">
3    <!-- The real input will be hidden, but updated properly with Javascript -->
4    <!-- For a production usage, you may want to add a label and also put it inside a form -->
5    <input class="range__input" type="range" min="30" max="70" value="64"/>
6
7    <!-- All the other elements will go here -->
8</div>

正如我们所看到的,我们的组件包含一个实际的input,类型为range,我们将使用Java脚本正确地更新它。将这个input元素和我们的组件放在一个通用的HTML表单中允许我们将input的值(以及其他表单数据)发送到submit上的服务器。

现在让我们来看看我们需要的SVG元素,为了更好地理解,我们进行了注释:

 1<!-- SVG elements -->
 2<svg class="range__slider" width="320px" height="480px" viewBox="0 0 320 480">
 3    <defs>
 4        <!-- Range marks symbol. It will be reused below -->
 5        <symbol id="range__marks" shape-rendering="crispEdges">
 6            <path class="range__marks__path" d="M 257 30 l 33 0"></path>
 7            <path class="range__marks__path" d="M 268 60 l 22 0"></path>
 8            <path class="range__marks__path" d="M 278 90 l 12 0"></path>
 9            <path class="range__marks__path" d="M 278 120 l 12 0"></path>
10            <path class="range__marks__path" d="M 278 150 l 12 0"></path>
11            <path class="range__marks__path" d="M 278 180 l 12 0"></path>
12            <path class="range__marks__path" d="M 278 210 l 12 0"></path>
13            <path class="range__marks__path" d="M 278 240 l 12 0"></path>
14            <path class="range__marks__path" d="M 278 270 l 12 0"></path>
15            <path class="range__marks__path" d="M 278 300 l 12 0"></path>
16            <path class="range__marks__path" d="M 278 330 l 12 0"></path>
17            <path class="range__marks__path" d="M 278 360 l 12 0"></path>
18            <path class="range__marks__path" d="M 278 390 l 12 0"></path>
19            <path class="range__marks__path" d="M 268 420 l 22 0"></path>
20            <path class="range__marks__path" d="M 257 450 l 33 0"></path>
21        </symbol>
22        <!-- This clipPath element will allow us to hide/show the white marks properly -->
23        <!-- The `path` used here is an exact copy of the `path` used for the slider below -->
24        <clipPath id="range__slider__clip-path">
25            <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
26        </clipPath>
27    </defs>
28    <!-- Pink marks -->
29    <use xlink:href="#range__marks" class="range__marks__pink"></use>
30    <!-- Slider `path`, that will be morphed properly on user interaction -->
31    <path class="range__slider__path" d="M 0 480 l 320 0 l 0 480 l -320 0 Z"></path>
32    <!-- Clipped white marks -->
33    <use xlink:href="#range__marks" class="range__marks__white" clip-path="url(#range__slider__clip-path)"></use>
34</svg>

<$>[备注] 注意: 如果这是您第一次使用svgpath元素,或者您不了解它们的工作原理,您可以在Mozilla.的本教程》中了解更多信息 <$>

最后,我们需要另一段代码来显示原始动画中出现的值和文本:

 1<!-- Range values -->
 2<div class="range__values">
 3    <div class="range__value range__value--top">
 4        <!-- This element will be updated in the way: `100 - inputValue` -->
 5        <span class="range__value__number range__value__number--top"></span>
 6        <!-- Some text for the `top` value -->
 7        <span class="range__value__text range__value__text--top">
 8            <span>Points</span>
 9            <span>You Need</span>
10        </span>
11    </div>
12    <div class="range__value range__value--bottom">
13        <!-- This element will be updated with the `inputValue` -->
14        <span class="range__value__number range__value__number--bottom"></span>
15        <!-- Some text for the `bottom` value -->
16        <span class="range__value__text range__value__text--bottom">
17            <span>Points</span>
18            <span>You Have</span>
19        </span>
20    </div>
21</div>

现在让我们来看看这些款式。

第二步-添加样式

我们将开始设计wrapper元素的样式:

1.range__wrapper {
2  user-select: none; // disable user selection, for better drag & drop
3
4  // More code for basic styling and centering...
5}

正如您所看到的,除了实现适当外观和居中元素的基本样式外,我们还禁用了用户在组件中选择任何内容的功能。这一点很重要,因为我们将实现拖放类型的交互,因此,如果我们允许选择功能,我们可能会获得意外的行为。

接下来我们将隐藏实际的input元素,并正确定位svg.range__slider)元素:

 1// Hide the `input`
 2.range__input {
 3  display: none;
 4}
 5
 6// Position the SVG root element
 7.range__slider {
 8  position: absolute;
 9  left: 0;
10  top: 0;
11}

为了给SVG元素上色,我们使用以下代码:

 1// Slider color
 2.range__slider__path {
 3  fill: #FF4B81;
 4}
 5
 6// Styles for marks
 7.range__marks__path {
 8  fill: none;
 9  stroke: inherit;
10  stroke-width: 1px;
11}
12
13// Stroke color for the `pink` marks
14.range__marks__pink {
15  stroke: #FF4B81;
16}
17
18// Stroke color for the `white` marks
19.range__marks__white {
20  stroke: white;
21}

现在让我们看看用于值的主要样式。在这里,transform-origin属性在保持数字与文本以所需方式对齐方面起着重要作用,就像在原始动画中一样。

 1// Positioning the container for values; it will be translated with Javascript
 2.range__values {
 3  position: absolute;
 4  left: 0;
 5  top: 0;
 6  width: 100%;
 7}
 8
 9// These `transform-origin` values will keep the numbers in the desired position as they are scaled
10.range__value__number--top {
11  transform-origin: 100% 100%; // bottom-right corner
12}
13.range__value__number--bottom {
14  transform-origin: 100% 0; // top-right corner
15}
16
17// More basic styles for the values...

第三步-添加与JavaScript的交互

现在是添加交互并开始制作动画的时候了。

首先,让我们看看模拟拖放功能、监听相应事件、执行数学运算和执行动画所需的代码。请注意,我们没有包括整个代码,而只是理解行为的基本部分。

 1// Handle `mousedown` and `touchstart` events, saving data about mouse position
 2function mouseDown(e) {
 3    mouseY = mouseInitialY = e.targetTouches ? e.targetTouches[0].pageY : e.pageY;
 4    rangeWrapperLeft = rangeWrapper.getBoundingClientRect().left;
 5}
 6
 7// Handle `mousemove` and `touchmove` events, calculating values to morph the slider `path` and translate values properly
 8function mouseMove(e) {
 9    if (mouseY) {
10        // ... Some code for maths ...
11        // After doing maths, update the value
12        updateValue();
13    }
14}
15
16// Handle `mouseup`, `mouseleave`, and `touchend` events
17function mouseUp() {
18    // Trigger elastic animation in case `y` value has changed
19    if (mouseDy) {
20        elasticRelease();
21    }
22    // Reset values
23    mouseY = mouseDy = 0;
24}
25
26// Events listeners
27rangeWrapper.addEventListener('mousedown', mouseDown);
28rangeWrapper.addEventListener('touchstart', mouseDown);
29rangeWrapper.addEventListener('mousemove', mouseMove);
30rangeWrapper.addEventListener('touchmove', mouseMove);
31rangeWrapper.addEventListener('mouseup', mouseUp);
32rangeWrapper.addEventListener('mouseleave', mouseUp);
33rangeWrapper.addEventListener('touchend', mouseUp);

现在我们来看一下updateValue函数。此功能负责更新组件值并根据光标位置移动滑块。为了更好地理解,我们对其中的每一个部分都进行了详尽的评论:

 1// Function to update the slider value
 2function updateValue() {
 3    // Clear animations if are still running
 4    anime.remove([rangeValues, rangeSliderPaths[0], rangeSliderPaths[1]]);
 5
 6    // Calculate the `input` value using the current `y`
 7    rangeValue = parseInt(currentY * max / rangeHeight);
 8    // Calculate `scale` value for numbers
 9    scale = (rangeValue - rangeMin) / (rangeMax - rangeMin) * scaleMax;
10    // Update `input` value
11    rangeInput.value = rangeValue;
12    // Update numbers values
13    rangeValueNumberTop.innerText = max - rangeValue;
14    rangeValueNumberBottom.innerText = rangeValue;
15    // Translate range values
16    rangeValues.style.transform = 'translateY(' + (rangeHeight - currentY) + 'px)';
17    // Apply corresponding `scale` to numbers
18    rangeValueNumberTop.style.transform = 'scale(' + (1 - scale) + ')';
19    rangeValueNumberBottom.style.transform = 'scale(' + (1 - (scaleMax - scale)) + ')';
20
21    // Some math calculations
22    if (Math.abs(mouseDy) < mouseDyLimit) {
23        lastMouseDy = mouseDy;
24    } else {
25        lastMouseDy = mouseDy < 0 ? -mouseDyLimit : mouseDyLimit;
26    }
27
28    // Calculate the `newSliderY` value to build the slider `path`
29    newSliderY = currentY + lastMouseDy / mouseDyFactor;
30    if (newSliderY < rangeMinY || newSliderY > rangeMaxY) {
31        newSliderY = newSliderY < rangeMinY ? rangeMinY : rangeMaxY;
32    }
33
34    // Build `path` string and update `path` elements
35    newPath = buildPath(lastMouseDy, rangeHeight - newSliderY);
36    rangeSliderPaths[0].setAttribute('d', newPath);
37    rangeSliderPaths[1].setAttribute('d', newPath);
38}

正如我们已经看到的,在前面的函数中有一个对BuildPath‘函数的调用,该函数是我们的组件中的一个基本部分。此函数将允许我们构建滑块的路径`,给定以下参数:

  • dy:自MousedownTouchstart事件以来,鼠标在`y‘轴上移动的距离。
  • ty路径必须平移的y轴距离。

它还使用MouseX值将曲线绘制到x轴上的光标位置,并以String格式返回path

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

最后,让我们来看看如何实现有趣的弹性效果:

 1// Function to simulate the elastic behavior
 2function elasticRelease() {
 3    // Morph the paths to the opposite direction, to simulate a strong elasticity
 4    anime({
 5        targets: rangeSliderPaths,
 6        d: buildPath(-lastMouseDy * 1.3, rangeHeight - (currentY - lastMouseDy / mouseDyFactor)),
 7        duration: 150,
 8        easing: 'linear',
 9        complete: function () {
10            // Morph the paths to the normal state, using the `elasticOut` easing function (default)
11            anime({
12                targets: rangeSliderPaths,
13                d: buildPath(0, rangeHeight - currentY),
14                duration: 4000,
15                elasticity: 880
16            });
17        }
18    });
19
20    // Here will go a similar code to:
21    // - Translate the values to the opposite direction, to simulate a strong elasticity
22    // - Then, translate the values to the right position, using the `elasticOut` easing function (default)
23}

正如您所看到的,需要实现两个连续的动画来实现夸张的弹性效果,类似于原始动画。这是因为使用elasticOut缓动函数的单个动画是不够的。

结论

在本教程中,我们开发了一个组件来模拟range类型的input的行为,但效果令人印象深刻,类似于原始动画:

斯坦·Yakusevich运球

您可以使用result,上的代码或获取Codepen,上的完整代码]来查看最终Github.实演

Published At
Categories with 技术
Tagged with
comments powered by Disqus