Резиновый ползунок или range slider
Range slider с эффектом резиновости реализованном с помощью gsap tweenmax.
HTML
<div class="box">
<h3>Price <span>Range</span></h3>
<div class="values">
<div>$<span id="first"></span></div> - <div>$<span id="second"></span></div>
</div>
<small>
Current Range:
<div>$<span id="third"></span></div>
</small>
<div class="slider" data-value-0="#first" data-value-1="#second" data-range="#third"></div>
</div>
SCSS
body {
&.ui-slider-active {
cursor: grabbing;
}
}
.box {
--primary: #275efe;
--headline: #3F4656;
--text: #99A3BA;
width: 100%;
max-width: 312px;
padding: 36px 32px 48px 32px;
background: #fff;
border-radius: 9px;
box-shadow: 0 1px 4px rgba(18, 22, 33, .12);
h3 {
font-family: inherit;
font-size: 32px;
font-weight: 700;
margin: 0 0 20px 0;
color: var(--headline);
span {
font-weight: 500;
}
}
.values,
small {
div {
display: inline-block;
vertical-align: top;
}
}
.values {
margin: 0;
font-weight: 500;
color: var(--primary);
& > div {
&:first-child {
margin-right: 2px;
}
&:last-child {
margin-left: 2px;
}
}
}
small {
color: var(--text);
display: block;
margin-top: 8px;
font-size: 14px;
}
.slider {
margin-top: 40px;
}
}
.slider {
--primary: #275efe;
--handle: #fff;
--handle-active: #{mix(white, #275efe, 70%)};
--handle-hover: #{mix(white, #275efe, 90%)};
--handle-border: 2px solid var(--primary);
--line: #cdd9ed;
--line-active: var(--primary);
height: 23px;
width: 100%;
position: relative;
pointer-events: none;
.ui-slider-handle {
--y: 0;
--background: var(--handle);
cursor: grab;
-webkit-tap-highlight-color: transparent;
top: 0;
width: 23px;
height: 23px;
transform: translateX(-50%);
position: absolute;
outline: none;
display: block;
pointer-events: auto;
div {
width: 23px;
height: 23px;
border-radius: 50%;
transition: background .4s ease;
transform: translateY(calc(var(--y) * 1px));
border: var(--handle-border);
background: var(--background);
}
&:hover {
--background: var(--handle-hover);
}
&:active {
--background: var(--handle-active);
cursor: grabbing;
}
}
svg {
--stroke: var(--line);
display: block;
height: 83px;
path {
fill: none;
stroke: var(--stroke);
stroke-width: 1;
}
}
.active,
& > svg {
position: absolute;
top: -30px;
height: 83px;
}
& > svg {
left: 0;
width: 100%;
}
.active {
position: absolute;
overflow: hidden;
left: calc(var(--l) * 1px);
right: calc(var(--r) * 1px);
svg {
--stroke: var(--line-active);
position: relative;
left: calc(var(--l) * -1px);
right: calc(var(--r) * -1px);
path {
stroke-width: 2;
}
}
}
}
html {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: inherit;
&:before,
&:after {
box-sizing: inherit;
}
}
JS
Необходимые библиотеки:https://code.jquery.com/jquery-3.4.1.min.js
https://code.jquery.com/ui/1.12.0/jquery-ui.min.js
https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js
скрипт$('.slider').each(function(e) {
var slider = $(this),
width = slider.width(),
handle,
handleObj;
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 ' + width + ' 83');
slider.html(svg);
slider.append($('<div>').addClass('active').html(svg.cloneNode(true)));
slider.slider({
range: true,
values: [1800, 7800],
min: 500,
step: 5,
minRange: 1000,
max: 12000,
create(event, ui) {
slider.find('.ui-slider-handle').append($('<div />'));
$(slider.data('value-0')).html(slider.slider('values', 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
$(slider.data('value-1')).html(slider.slider('values', 1).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
$(slider.data('range')).html((slider.slider('values', 1) - slider.slider('values', 0)).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
setCSSVars(slider);
},
start(event, ui) {
$('body').addClass('ui-slider-active');
handle = $(ui.handle).data('index', ui.handleIndex);
handleObj = slider.find('.ui-slider-handle');
},
change(event, ui) {
setCSSVars(slider);
},
slide(event, ui) {
let min = slider.slider('option', 'min'),
minRange = slider.slider('option', 'minRange'),
max = slider.slider('option', 'max');
if(ui.handleIndex == 0) {
if((ui.values[0] + minRange) >= ui.values[1]) {
slider.slider('values', 1, ui.values[0] + minRange);
}
if(ui.values[0] > max - minRange) {
return false;
}
} else if(ui.handleIndex == 1) {
if((ui.values[1] - minRange) <= ui.values[0]) {
slider.slider('values', 0, ui.values[1] - minRange);
}
if(ui.values[1] < min + minRange) {
return false;
}
}
$(slider.data('value-0')).html(ui.values[0].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
$(slider.data('value-1')).html(ui.values[1].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
$(slider.data('range')).html((slider.slider('values', 1) - slider.slider('values', 0)).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '));
setCSSVars(slider);
},
stop(event, ui) {
$('body').removeClass('ui-slider-active');
let duration = .6,
ease = Elastic.easeOut.config(1.08, .44);
TweenMax.to(handle, duration, {
'--y': 0,
ease: ease
});
TweenMax.to(svgPath, duration, {
y: 42,
ease: ease
});
handle = null;
}
});
var svgPath = new Proxy({
x: null,
y: null,
b: null,
a: null
}, {
set(target, key, value) {
target[key] = value;
if(target.x !== null && target.y !== null && target.b !== null && target.a !== null) {
slider.find('svg').html(getPath([target.x, target.y], target.b, target.a, width));
}
return true;
},
get(target, key) {
return target[key];
}
});
svgPath.x = width / 2;
svgPath.y = 42;
svgPath.b = 0;
svgPath.a = width;
$(document).on('mousemove touchmove', e => {
if(handle) {
let laziness = 4,
max = 24,
edge = 52,
other = handleObj.eq(handle.data('index') == 0 ? 1 : 0),
currentLeft = handle.position().left,
otherLeft = other.position().left,
handleWidth = handle.outerWidth(),
handleHalf = handleWidth / 2,
y = e.pageY - handle.offset().top - handle.outerHeight() / 2,
moveY = (y - laziness >= 0) ? y - laziness : (y + laziness <= 0) ? y + laziness : 0,
modify = 1;
moveY = (moveY > max) ? max : (moveY < -max) ? -max : moveY;
modify = handle.data('index') == 0 ? ((currentLeft + handleHalf <= edge ? (currentLeft + handleHalf) / edge : 1) * (otherLeft - currentLeft - handleWidth <= edge ? (otherLeft - currentLeft - handleWidth) / edge : 1)) : ((currentLeft - (otherLeft + handleHalf * 2) <= edge ? (currentLeft - (otherLeft + handleWidth)) / edge : 1) * (slider.outerWidth() - (currentLeft + handleHalf) <= edge ? (slider.outerWidth() - (currentLeft + handleHalf)) / edge : 1));
modify = modify > 1 ? 1 : modify < 0 ? 0 : modify;
if(handle.data('index') == 0) {
svgPath.b = currentLeft / 2 * modify;
svgPath.a = otherLeft;
} else {
svgPath.b = otherLeft + handleHalf;
svgPath.a = (slider.outerWidth() - currentLeft) / 2 + currentLeft + handleHalf + ((slider.outerWidth() - currentLeft) / 2) * (1 - modify);
}
svgPath.x = currentLeft + handleHalf;
svgPath.y = moveY * modify + 42;
handle.css('--y', moveY * modify);
}
});
});
function getPoint(point, i, a, smoothing) {
let cp = (current, previous, next, reverse) => {
let p = previous || current,
n = next || current,
o = {
length: Math.sqrt(Math.pow(n[0] - p[0], 2) + Math.pow(n[1] - p[1], 2)),
angle: Math.atan2(n[1] - p[1], n[0] - p[0])
},
angle = o.angle + (reverse ? Math.PI : 0),
length = o.length * smoothing;
return [current[0] + Math.cos(angle) * length, current[1] + Math.sin(angle) * length];
},
cps = cp(a[i - 1], a[i - 2], point, false),
cpe = cp(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
}
function getPath(update, before, after, width) {
let smoothing = .16,
points = [
[0, 42],
[before <= 0 ? 0 : before, 42],
update,
[after >= width ? width : after, 42],
[width, 42]
],
d = points.reduce((acc, point, i, a) => i === 0 ? `M ${point[0]},${point[1]}` : `${acc} ${getPoint(point, i, a, smoothing)}`, '');
return `<path d="${d}" />`;
}
function setCSSVars(slider) {
let handle = slider.find('.ui-slider-handle');
slider.css({
'--l': handle.eq(0).position().left + handle.eq(0).outerWidth() / 2,
'--r': slider.outerWidth() - (handle.eq(1).position().left + handle.eq(1).outerWidth() / 2)
});
}