Как сделать 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.

→ Ссылка