如何使用SVG和anime.js构建弹性范围输入
介绍
在 HTML5 中,为 form 元素引入了许多新类型的 input 属性,例如 color、date、range 和还有很多。 尽管这些新类型的 input 在功能上有效,但它们往往不能满足 Web 应用程序的审美需求。
为了给这些 input 类型提供现代设计,可以使用这样的前端,本教程将模拟 range input 的行为,其中组件使用 SVG 绘制 [X176X ] 和 anime.js 来执行动画。
完成本教程后,您将了解创建 range input 设计的基本步骤,如下所示:
注意: 这个原创动画,我们以此为灵感,可以在 Stan Yakusevich 的 this dribble shot 中找到。
如果您想查看最终产品,请在 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>
正如我们所见,我们的组件包含一个 range 类型的实际 input,我们将使用 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 元素或者你不明白它们是如何工作的,你可以在 this tutorial from 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:自mousedown或touchstart事件后鼠标在y轴上移动的距离。ty:y轴上path必须平移的距离。
它还使用mouseX值将曲线绘制到x轴上的光标位置,并以String格式返回path:
// 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)
}
如您所见,需要实现两个连续的动画才能实现夸张的弹性效果,类似于原始动画。 这是因为使用 elasticOut 缓动函数的单个动画是不够的。
结论
在本教程中,我们开发了一个组件来模拟 range 类型的 input 的行为,但效果令人印象深刻,类似于原始动画: