Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SVG arc slider for range input

My goal is to design an arc slider which looks something like that

enter image description here

I have the following structure of the template

<svg width="500" height="300">
  <path id="track" stroke="lightgrey" fill="transparent" stroke-width="20" d="
     M 50 50
     A 90 90 0 0 0 300 50
  "/>
  <path id="trackFill" fill="cyan" stroke-width="20" d="
     M 50 50
     A 90 90 0 0 0 [some dynamic value?] [some dynamic value?]
  "/>
  <circle id="knob" fill="lightblue"  cx="[dynamic, initial - 50]" cy="[dynamic, initial - 50]" r="25"/>
</svg>

knob - the control which user is supposed to drag in order to change the value

track - the full arc of the slide

trackFill - the portion of the slider path before the knob

Is it possible to make trackFill cover the portion of the slider before the knob as it is being dragged along the slider curve? If so which APIs or CSS rules will help me to achieve such a result?

like image 882
Max Liapkalo Avatar asked Oct 21 '25 23:10

Max Liapkalo


2 Answers

Is it something like this you are after?

let svg = document.getElementById("slider");
let trackFill = document.getElementById("trackFill");
let knob = document.getElementById("knob");
let isDragging = false;
let sliderDragOffset = {dx: 0, dy: 0};
let ARC_CENTRE = {x: 175, y: 50};
let ARC_RADIUS = 125;

let sliderValue = 0;
setSliderValue(sliderValue);


function setSliderValue(value)
{
  // Limit value to (0..sliderMax)
  let sliderMax = track.getTotalLength();
  sliderValue = Math.max(0, Math.min(value, sliderMax));
  // Calculate new position of knob
  let knobRotation = sliderValue * Math.PI / sliderMax;
  let knobX = ARC_CENTRE.x - Math.cos(knobRotation) * ARC_RADIUS;
  let knobY = ARC_CENTRE.y + Math.sin(knobRotation) * ARC_RADIUS;
  // Adjust trackFill dash patter to only draw the portion up to the knob position
  trackFill.setAttribute("stroke-dasharray", sliderValue + " " + sliderMax);
  // Update the knob position
  knob.setAttribute("cx", knobX);
  knob.setAttribute("cy", knobY);
}


knob.addEventListener("mousedown", evt => {
  isDragging = true;
  // Remember where we clicked on knob in order to allow accurate dragging
  sliderDragOffset.dx = evt.offsetX - knob.cx.baseVal.value;
  sliderDragOffset.dy = evt.offsetY - knob.cy.baseVal.value;
  // Attach move event to svg, so that it works if you move outside knob circle
  svg.addEventListener("mousemove", knobMove);
  // Attach move event to window, so that it works if you move outside svg
  window.addEventListener("mouseup", knobRelease);
});


function knobMove(evt)
{
  // Calculate adjusted drag position
  let x = evt.offsetX + sliderDragOffset.dx;
  let y = evt.offsetY + sliderDragOffset.dy;
  // Position relative to centre of slider arc
  x -= ARC_CENTRE.x;
  y -= ARC_CENTRE.y;
  // Get angle of drag position relative to slider centre
  let angle = Math.atan2(y, -x);
  // Positions above arc centre will be negative, so handle them gracefully
  // by clamping angle to the nearest end of the arc
  angle = (angle < -Math.PI / 2) ? Math.PI : (angle < 0) ? 0 : angle;
  // Calculate new slider value from this angle (sliderMaxLength * angle / 180deg)
  setSliderValue(angle * track.getTotalLength() / Math.PI);
}


function knobRelease(evt)
{
  // Cancel event handlers
  svg.removeEventListener("mousemove", knobMove);
  window.removeEventListener("mouseup", knobRelease);
  isDragging = false;
}
<svg id="slider" width="500" height="300">
  <g stroke="lightgrey">
    <path id="track" fill="transparent" stroke-width="20" d="
       M 50 50
       A 125 125 0 0 0 300 50
    "/>
  </g>
  <use id="trackFill" xlink:href="#track" stroke="cyan"/>
  <circle id="knob" fill="lightblue" cx="50" cy="50" r="25"/>
</svg>

I've kept this code simple for clarity, but at the expense of some limitations.

It assumes there is only one slider per page. If you need more than that, you will have to keep the slider-specific values (eg sliderValue and, isDragging) separate. You could use data attributes for that. You would also need to switch from accessing the SVG elements via id attributes to another way (eg. class attributes), because id attributes must be unique on the page.

like image 86
Paul LeBeau Avatar answered Oct 24 '25 14:10

Paul LeBeau


Here is a simple example:

const radius = 50;
const offsetX = 10;
const offsetY = 10;

// 0 <= pos <= 1
const setSliderPos = (svg, pos) => {
  const angle = Math.PI * pos;
  const x = offsetX + radius - Math.cos(angle) * radius;
  const y = offsetY + Math.sin(angle) * radius;

  svg.select('.knob').attr('cx', x).attr('cy', y);
  svg.select('.first').attr('d', `M ${offsetX},${offsetY} A ${radius},${radius} 0 0 0 ${x},${y}`);
  svg.select('.second').attr('d', `M ${x},${y} A ${radius},${radius} 0 0 0 ${offsetX + radius * 2},${offsetY}`);
}

setSliderPos(d3.select('#svg-1'), 0.3);
setSliderPos(d3.select('#svg-2'), 0.6);
setSliderPos(d3.select('#svg-3'), 1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="svg-1" width="150" height="80">
  <path class="first" stroke-width="5" stroke="lightblue" fill="none"/> 
  <path class="second" stroke-width="5" stroke="cyan" fill="none"/> 
  <circle class="knob" r="10" fill="lightblue"/>
</svg>
   
<svg id="svg-2" width="150" height="80">
  <path class="first" stroke-width="5" stroke="lightblue" fill="none"/> 
  <path class="second" stroke-width="5" stroke="cyan" fill="none"/> 
  <circle class="knob" r="10" fill="lightblue"/>
</svg>

<svg id="svg-3" width="150" height="80">
  <path class="first" stroke-width="5" stroke="lightblue" fill="none"/> 
  <path class="second" stroke-width="5" stroke="cyan" fill="none"/> 
  <circle class="knob" r="10" fill="lightblue"/>
</svg>
like image 43
Michael Rovinsky Avatar answered Oct 24 '25 14:10

Michael Rovinsky



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!