Как сделать range slider градиентом
Здравствуйте, подскажите, пожалуйста, подробно как сделать такой градиентный ползунок с шагами по 4 пунктам, чтобы тягать за ползунок, был в конусе градиент и шаги
Ответы (3 шт):
Автор решения: Leonid
→ Ссылка
Остался только ползунок.
function init(){
let under = document.getElementById('under');
let ztop = document.getElementById('ztop');
let text = document.getElementById('text');
let ctx1 = under.getContext('2d');
let ctx2 = ztop.getContext('2d');
w = under.width = ztop.width = window.innerWidth - 40;
h = under.height = ztop.height = 80;
let gradient = ctx1.createLinearGradient(0,0, w,0);
gradient.addColorStop(0, 'lightblue');
gradient.addColorStop(1, 'darkblue');
ctx1.fillStyle = gradient;
ctx1.fill(new Path2D(`M 20 37 L ${w - 20} 26 L ${w - 20} 40 L 20 40 z`));
let points = [];
let parags = document.querySelectorAll('#text p');
for(let i=0; i < 3; i++){
points.push(i*(w-20)/4 + 20);
parags[i].style.cssText = `left: ${i*(w-20)/4 + 20}px; width: ${w/4 - 20}px;`;
}
points.push(w-20);
parags[3].style.cssText = `left: ${w - w/4 - 5}px; width: ${w/4 - 20}px; text-align: right;`;
ctx1.strokeStyle = 'lightgray';
let arcX = 20;
let arc;
let clipRect;
let movable = false;
let delta = 0;
ztop.addEventListener('mousedown', onDown);
ztop.addEventListener('touchstart', onDown);
ztop.addEventListener('mousemove', onMove);
ztop.addEventListener('touchmove', onMove);
document.addEventListener('mouseup', onUp);
document.addEventListener('touchend', onUp);
render();
function onDown(e){
let x = (!e.touches) ? e.offsetX : e.targetTouches[0].clientX - ztop.offsetLeft;
let y = (!e.touches) ? e.offsetY : e.targetTouches[0].clientY - ztop.offsetTop;
if(ctx1.isPointInPath(arc, x, y)){
movable = true;
delta = x - arcX;
}
}
function onMove(e){
let x = !e.targetTouches ? e.offsetX : e.targetTouches[0].clientX - ztop.offsetLeft;
if(movable){
arcX = x + delta;
render();
}
}
function onUp(e){
goTo(points.sort((a,b) => Math.abs(a - arcX) - Math.abs(b - arcX))[0]);
movable = false;
render();
}
function render(){
ctx2.clearRect(0,0,w,h);
ctx2.save();
clipRect = new Path2D();
clipRect.rect(arcX, 0, w, 40);
ctx2.clip(clipRect);
ctx2.fillStyle = 'lightgray';
ctx2.fill(new Path2D(`M 20 36.8 L ${w - 20} 25.4 L ${w - 20} 40 L 20 40 z`));
ctx2.restore();
ctx2.save();
ctx2.strokeStyle = '#ffffff';
points.forEach(i => {
ctx2.stroke(new Path2D(`M ${i} 10 l 0 30`));
})
ctx2.restore();
ctx2.save();
ctx2.strokeStyle = 'gray';
ctx2.setLineDash([2,2]);
points.forEach((p,i) => {
ctx2.stroke(new Path2D(`M ${p} 10 l 0 ${27 - p/w*11}`));
})
ctx2.restore();
arc = new Path2D();
arc.arc(arcX, 52, 9, 0, 2 * Math.PI);
ctx2.strokeStyle = 'lightgray';
ctx2.stroke(arc);
}
function goTo(x){
let dir = (x > arcX)? 1 : -1;
let anim = setInterval(() => {
arcX += dir*4;
if((arcX - x)*dir >= 0){
arcX = x;
clearInterval(anim);
}
render();
}, 20)
}
}
init();
.therange {
position: absolute;
top: 80px;
left: 20px;
}
#top {
z-index: 1;
}
#text p {
position: absolute;
bottom: -10px;
font-size: 12px;
}
<canvas id="under" class="therange"></canvas>
<canvas id="ztop" class="therange"></canvas>
<div id="text" class="therange">
<p>Не владею</p>
<p>Использую готовые решения</p>
<p>Использую готовые решения и умею переделывать</p>
<p>Пишу сложный JS с нуля</p>
</div>
Автор решения: hu-fo
→ Ссылка
var levels = document.querySelector('.levels')
var progress = document.querySelector('.progress')
var knob = document.querySelector('.knob')
var levelsBox, knobBox, points, min, max
knob.addEventListener('mousedown', function(e) {
e.preventDefault()
window.addEventListener('mousemove', dragging)
window.addEventListener('mouseup', drop)
knob.style.transition = '0s'
progress.style.transition = '0s'
var knobBox = knob.getBoundingClientRect()
// координата нажатия мыши относительно ручки
var shift = e.x - knobBox.left;
function dragging(e) {
var x = 0
// не даем выйти за границы
if (e.x < min) x = min
else if (e.x > max) x = max
else x = e.x
X = x - shift - levelsBox.left
knob.style.transform = `translateX(${X}px) rotate(-45deg)`
progress.style.width = X + knobBox.width / 2 + 'px'
}
function drop(e) {
// ближайшее число в массиве точек к текущей координате ручки
var closest = getClosest(X)
knob.style.transition = '0.5s'
progress.style.transition = '0.5s'
// перемащаем к полученной ближайшей точке
knob.style.transform = `
translateX(${closest - knobBox.width / 2.7}px)
rotate(-45deg)
`
progress.style.width = closest + 'px'
window.removeEventListener('mousemove', dragging)
window.removeEventListener('mousemove', drop)
}
})
function getClosest(v) {
return points.reduce((prev, curr) =>
(Math.abs(curr - v) < Math.abs(prev - v) ? curr : prev)
)
}
function init() {
levelsBox = levels.getBoundingClientRect()
knobBox = knob.getBoundingClientRect()
/* ширина промежутков */
points = [
0,
levelsBox.width * 0.2,
levelsBox.width * 0.45,
levelsBox.width
]
min = levelsBox.left,
max = levelsBox.right,
// тут я поленился и при изменении размера окна просто обнуляются размеры
knob.style.transform = `translateX(-${knobBox.width / 2.7}px) rotate(-45deg)`
progress.style.width = 0
}
// при изменении ширины окна пересчитываем
// некоторые размеры для корректного отображения
window.onresize = init
init()
* {
margin: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-family: sans-serif;
}
.levels {
position: relative;
width: 80%;
display: flex;
}
.level {
display: flex;
flex-direction: column;
}
.level p {
font-size: 1.8vw;
height: 3vw;
margin-bottom: 0.4vw;
display: flex;
align-items: flex-end;
}
/* черточка */
.level::after {
content: '';
height: 3vw;
width: 1px;
z-index: 1;
}
/* ширина промежутков */
.level:nth-child(1) {
width: 20%;
}
.level:nth-child(2) {
width: 25%;
}
.level:nth-child(3) {
width: 30%;
}
.level:nth-child(4) {
align-items: flex-end;
width: 25%;
}
/* градиент на черточках */
.level:nth-child(1)::after {
background: linear-gradient(0deg, white 0%, white 22%, #BDBDBD 22%, #BDBDBD 100%);
}
.level:nth-child(2)::after {
background: linear-gradient(0deg, white 0%, white 30%, #BDBDBD 30%, #BDBDBD 100%);
}
.level:nth-child(3)::after {
background: linear-gradient(0deg, white 0%, white 40%, #BDBDBD 40%, #BDBDBD 100%);
}
.level:nth-child(4)::after {
background: linear-gradient(0deg, white 0%, white 62%, #BDBDBD 62%, #BDBDBD 100%);
}
.slider {
position: absolute;
bottom: 0;
left: 0;
height: 2vw;
width: 100%;
background-color: #BDBDBD;
clip-path: polygon(0 65%, 100% 0, 100% 100%, 0% 100%);
}
.progress {
height: 100%;
background: linear-gradient(45deg, #D1C4E9 0%, #4527A0 100%);
}
.knob {
position: absolute;
bottom: -4vw;
left: 0;
width: 2.4vw;
height: 2.4vw;
border-radius: 50%;
background-color: #FAFAFA;
box-shadow: 0 0 0 1px #9E9E9E;
border-radius: 50% 10% 50% 50%;
z-index: 2;
}
<div class="levels">
<div class="level">
<p>не владею</p>
</div>
<div class="level">
<p>использую готовые <br> решения
</p>
</div>
<div class="level">
<p>использую готовые решения <br> и умею переделывать</p>
</div>
<div class="level">
<p>пишу сложный js с нуля</p>
</div>
<div class="slider">
<div class="progress"></div>
</div>
<div class="knob"></div>
</div>
Автор решения: UModeL
→ Ссылка
Эх, не рассмотрел вовремя, что в вопросе только одна метка - javascript... А у меня - HTML и CSS. Не пинайте сильно - в разметке есть строчка кода на JS (правда и она инлайн). Реализовано на основе стандартного input[type="range"]:
/* Центровка и фоны */
body { margin: 0; display: flex; align-items: center; justify-content: center; height: 100vh; width: 100vw; padding: 0; background-image: radial-gradient(at center, transparent, #ffc107), url("https://i.stack.imgur.com/m9NKc.png"); background-position: 0% 0%; background-repeat: no-repeat; background-size: auto; }
/* Главный контейнер */
.experience {
--level: 2;
position: relative;
min-width: 350px;
width: 98%;
box-sizing: border-box;
}
/* Контейнер с метками */
.labels {
position: relative;
top: 0;
left: 11px;
display: flex;
width: calc(100% - 22px);
}
/* Метки */
.labels>div {
position: relative;
display: inline-grid;
width: 25%;
font: 13px/1.5em "Arial";
align-items: self-end;
}
/* Пунктирные тики под метками */
.labels>div::after {
content: "";
position: absolute;
top: calc(100% + 4px);
left: 0;
height: 21px;
border-left: 1px dotted #b7b7b7;
}
/* Выравнивание текста вправо в крайней правой метке */
.labels>div:last-child {
text-align: right;
}
/* Выравнивание тика вправо для крайней правой метки */
.labels>div:last-child::after {
left: calc(100% - 2px);
}
/* Контейнер прогресса ползунка */
.progress {
position: relative;
top: 0;
left: 11px;
height: 25px;
width: calc(100% - 22px);
overflow: hidden;
box-sizing: border-box;
}
/* Общие свойства для частей прогресса */
.progress::before,
.progress::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 150%;
width: 100%;
background-repeat: no-repeat;
transform-origin: top left;
}
/* Цветная часть прогресса */
.progress::before {
background-image: linear-gradient(90deg, #ccb1f1, #221d8c);
background-position: 25% 0%, 50% 0%, 0% 0%;
background-size: auto;
transform: skewY(-0.8deg) translatey(22px);
}
/* Серая часть прогресса с белыми тиками */
.progress::after {
background-image: linear-gradient(90deg, transparent, #fff, #fff, transparent), linear-gradient(90deg, transparent, #fff, #fff, transparent), linear-gradient( 90deg, transparent calc(var(--level) * 25%), #e8e8e8 calc(var(--level) * 25%));
background-position: 25% 0%, 50% 0%, 0% 0%;
background-size: 1px 200%, 1px 200%, auto;
transform: skewY(-0.8deg) translatey(21px);
}
/* Ползунок (input[type=range]) */
.experience input[type="range"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
position: absolute;
bottom: 0;
left: 0;
margin: 0;
display: block;
width: 100%;
box-sizing: border-box;
background: none;
pointer-events: none;
outline: none;
}
/* Дорожка ползунка */
.experience input[type="range"]::-webkit-slider-runnable-track {
-webkit-appearance: none; appearance: none;
}
.experience input[type="range"]::-moz-range-track {
-moz-appearance: none; appearance: none;
}
/* Кнопка ползунка */
.experience input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
height: 22px;
width: 22px;
transform: translatey(30px) rotate(-45deg);
border-radius: 50% 10% 50% 50%;
border: 1px solid #b5b5b5;
background-color: rgba(255,255,255,.8);
pointer-events: auto;
}
.experience input[type="range"]::-moz-range-thumb {
-moz-appearance: none; appearance: none;
height: 22px;
width: 22px;
transform: translatey(30px) rotate(-45deg);
border-radius: 50% 10% 50% 50%;
border: 1px solid #b5b5b5;
background-color: rgba(255,255,255,.8);
pointer-events: auto;
}
<div class="experience">
<div class="labels">
<div>Не владею</div>
<div>Использую готовые<br>решения</div>
<div>Использую готовые решения<br>и умею и переделывать</div>
<div>Пишу сложный JS с нуля</div>
</div>
<div class="progress"></div>
<input type="range" step="1" min="0" max="4" value="2" oninput="this.step = (this.value >= 2) ? 2 : 1; this.parentElement.style.setProperty('--level', this.value);">
</div>
Даже немного адаптивности добавил. В актуальном Chrome ничего не разъезжается.
UPD: Добавил поддержку Firefox.
