【问题标题】:SVG arc slider for range input用于范围输入的 SVG 弧形滑块
【发布时间】:2021-04-15 15:26:26
【问题描述】:

我的目标是设计一个看起来像这样的弧形滑块

我的模板结构如下

<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 - 用户应该拖动以更改值的控件

track - 幻灯片的完整弧线

trackFill - 旋钮之前的滑块路径部分

是否可以让trackFill 在沿滑块曲线拖动时覆盖旋钮之前的滑块部分?如果是这样,哪些 API 或 CSS 规则将帮助我实现这样的结果?

【问题讨论】:

  • 你不需要动态改变trackfill路径。相反,您可以使用 stroke-dasharray 控制绘制的数量。
  • @PaulLeBeau 你能举个简单的例子吗?

标签: javascript css svg slider


【解决方案1】:

你追求的是这样的吗?

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>

为了清楚起见,我使这段代码保持简单,但以牺牲一些限制为代价。

假设每页只有一个滑块。如果您需要更多,则必须将滑块特定的值(例如,sliderValue 和 isDragging)分开。您可以为此使用数据属性。您还需要从通过 id 属性访问 SVG 元素切换到另一种方式(例如 class 属性),因为 id 属性在页面上必须是唯一的。

【讨论】:

  • 还有一件事,是否可以让这个滑块同时在移动设备和桌面设备上工作?简单地用触摸事件替换鼠标事件似乎不是正确的做法
  • 或者将其包装在一个 W3C 标准的 Web Component 中以在一页上使用多个实例
  • @MaxLiapkalo 触摸基本相同。但是对于touchstarttouchmove,您需要访问touches 属性。另一个复杂之处是触摸事件没有offsetX/Y 属性,因此您需要自己计算这些属性(请参阅stackoverflow.com/questions/17130940/…
【解决方案2】:

这是一个简单的例子:

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>

【讨论】:

  • 嗨,Michael,您能否解释一下 xy 坐标背后的数学原理以及为什么我们需要 X\Y 偏移值?
  • 当然。这是一个基本的三角函数。旋钮相对于滑块中心的位置为:x = cos(angle) * radius, y = sin(angle) * radius。角度为 180 * pos(以度为单位)或 PI * pos 以弧度为单位。中心坐标为x:offsetX + radius; y = offsetY + 半径
  • 抱歉,中心的 y = offsetY。其余的都是正确的:)
猜你喜欢
  • 2020-10-24
  • 2021-08-22
  • 1970-01-01
  • 1970-01-01
  • 2018-09-22
  • 1970-01-01
  • 2013-06-25
  • 2015-12-25
  • 2020-10-16
相关资源
最近更新 更多