Прикрепление стрелок к path дуги SVG

Я создаю датчик, используя SVG. Есть ли способ добавить стрелку в конце дуги анимированного индикатора, чтобы указать введенное значение?

Так как датчик отображается с помощью stroke-dasharray path, то поэтому нельзя применить маркер (стрелку), в соответствии со спецификацией svg.

введите сюда описание изображения

class arcGauge {
    constructor(targetEl) {
        this.el = targetEl;
        this.minmax = [];
        this.arcCoordinate = [];
        this.valueArcDataValue = 0;
        this.gaugeArc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    }

    // draw gauge
    init(data) {
        // set data
        this.viewBox = [0, 0, 110, 100];
        this.minmax = data.minmax || [];
        this.arcCoordinate = data.arcCoordinate || [120, 60];
        this.threshold = data.threshold || [];
        this.valueArcData = data.valueArcData;
        this.gaugeRadius = 42;
        this.gaugeStrokeWidth = 6;

        this._makeGauge();

    }

    _makeGauge() {
        const radius = this.gaugeRadius;

        let arcCoord = [];
        // coordinate
        this.arcCoordinate.forEach((ang) => {
            const radian = this._degreesToRadians(ang);
            const x = this.viewBox[2] / 2 + Math.cos(radian) * radius;
            const y = this.viewBox[2] / 2 + Math.sin(radian) * radius;
            arcCoord.push([x.toFixed(2), y.toFixed(2)]);
        });

        //arc
        this.gaugeArc.setAttribute('id', 'gaugeArc');
        this.gaugeArc.setAttribute('d', `M ${arcCoord[0][0]} ${arcCoord[0][1]} A ${radius} ${radius} 0 1 1  ${arcCoord[1][0]} ${arcCoord[1][1]}`);
        this.gaugeArc.setAttribute('fill', 'none');
        this.gaugeArc.setAttribute('stroke', this.valueArcData.color);
        this.gaugeArc.setAttribute('stroke-width', this.gaugeStrokeWidth);
        this.gaugeArc.setAttribute('transform', 'scale(-1, 1) translate(-110, 0)');

        let percentage = 0;
    percentage = this.valueArcData.value;

        this.gaugeArc.style.strokeDasharray = this._getArcLength(radius, 300, percentage);

        this.el.appendChild(this.gaugeArc);
    }

    // degree
    _degreesToRadians(degrees) {
        const pi = Math.PI;
        return degrees * (pi / 180);
    }
    // arc length
    _getArcLength(radius, degrees, val) {
        const radian = this._degreesToRadians(degrees);
        const arcLength = 2 * Math.PI * radius * (degrees / 360);
        const pathLength = arcLength * (val / 100);
        const dasharray = `${pathLength.toFixed(2)} ${arcLength.toFixed(2)}`;
        return dasharray;
    }



    // set gauge value
    setValue(v) {
            const baseValue = this.minmax[1] - this.minmax[0];
            const percentage = (v / baseValue) * 100;
            this.valueArcData.value = v;

            this.gaugeArc.style.strokeDasharray = this._getArcLength(this.gaugeRadius, 300, percentage);
            //gauge animation
            this.gaugeArc.style.transition = 'stroke-dasharray 1s ease-in-out';
    }

}


const arcGaugeData = {
  minmax: [0, 100],
  thresholdColor: ['#22b050', '#f4c141', '#e73621'],
  valueArcData: { type: 'arrow', color: '#3e3eff', value: 80 },
};
const arcGaugeIns = new arcGauge(document.querySelector('#chart'));
arcGaugeIns.init(arcGaugeData);

function changeArc(ipt) {
  arcGaugeIns.setValue(ipt.value);
}
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
<div class="container">
  <svg id="chart" xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100"></svg>
  <input type="range"  min="0" max="100" step="1" value="80" onchange="changeArc(this)" />
</div>

Если это невозможно, подскажите пожалуйста, есть ли альтернатива.

Свободный перевод вопроса Attaching arrows to SVG Arc path от участника @Th Yoon's user avatar Th Yoon.


Ответы (3 шт):

Автор решения: old-dev

Первое что приходит в голову добавить букву "▶" и позиционировать ее атрибутом startOffset по заданному path. Решение "костыльное" - но имеет место быть

class arcGauge {
    constructor(targetEl) {
        this.el = targetEl;
        this.endChar = targetEl.querySelector('textPath')
        this.animateChar = this.endChar.querySelector('animate')
        this.minmax = [];
        this.arcCoordinate = [];
        this.valueArcDataValue = 0;
        this.gaugeArc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    }

    // draw gauge
    init(data) {
        // set data
        this.viewBox = [0, 0, 110, 100];
        this.minmax = data.minmax || [];
        this.arcCoordinate = data.arcCoordinate || [120, 60];
        this.threshold = data.threshold || [];
        this.valueArcData = data.valueArcData;
        this.gaugeRadius = 42;
        this.gaugeStrokeWidth = 6;

        this._makeGauge();

    }

    _makeGauge() {
        const radius = this.gaugeRadius;

        let arcCoord = [];
        // coordinate
        this.arcCoordinate.forEach((ang) => {
            const radian = this._degreesToRadians(ang);
            const x = this.viewBox[2] / 2 + Math.cos(radian) * radius;
            const y = this.viewBox[2] / 2 + Math.sin(radian) * radius;
            arcCoord.push([x.toFixed(2), y.toFixed(2)]);
        });

        //arc
        this.gaugeArc.setAttribute('id', 'gaugeArc');
        this.gaugeArc.setAttribute('d', `M ${arcCoord[0][0]} ${arcCoord[0][1]} A ${radius} ${radius} 0 1 1  ${arcCoord[1][0]} ${arcCoord[1][1]}`);
        this.gaugeArc.setAttribute('fill', 'none');
        this.gaugeArc.setAttribute('stroke', this.valueArcData.color);
        this.gaugeArc.setAttribute('stroke-width', this.gaugeStrokeWidth);
        this.gaugeArc.setAttribute('transform', 'scale(-1, 1) translate(-110, 0)');
        
        this.endChar.setAttribute('fill', this.valueArcData.color);

        let percentage = 0;
    percentage = this.valueArcData.value;
        
        this.endChar.setAttribute('startOffset',percentage-4 + '%')
        
        this.gaugeArc.style.strokeDasharray = this._getArcLength(radius, 300, percentage);

        this.el.appendChild(this.gaugeArc);
    }

    // degree
    _degreesToRadians(degrees) {
        const pi = Math.PI;
        return degrees * (pi / 180);
    }
    // arc length
    _getArcLength(radius, degrees, val) {
        const radian = this._degreesToRadians(degrees);
        const arcLength = 2 * Math.PI * radius * (degrees / 360);
        const pathLength = arcLength * (val / 100);
        const dasharray = `${pathLength.toFixed(2)} ${arcLength.toFixed(2)}`;
        return dasharray;
    }



    // set gauge value
    setValue(v) {
            const baseValue = this.minmax[1] - this.minmax[0];
            const percentage = (v / baseValue) * 100;
            this.valueArcData.value = v;

            this.gaugeArc.style.strokeDasharray = this._getArcLength(this.gaugeRadius, 300, percentage);
            //gauge animation
            this.gaugeArc.style.transition = 'stroke-dasharray 1s ease-in-out';
            
        
        this.animateChar.setAttribute('from',this.endChar.getAttribute('startOffset'))
        this.animateChar.setAttribute('to',percentage-4 + '%')
        
        this.endChar.setAttribute('startOffset',percentage-4 + '%')
        this.animateChar.beginElement();
    }

}


const arcGaugeData = {
  minmax: [0, 100],
  thresholdColor: ['#22b050', '#f4c141', '#e73621'],
  valueArcData: { type: 'arrow', color: '#3e3eff', value: 80 },
};
const arcGaugeIns = new arcGauge(document.querySelector('#chart'));
arcGaugeIns.init(arcGaugeData);

function changeArc(ipt) {
  arcGaugeIns.setValue(ipt.value);
}
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
<div class="container">
  <svg id="chart" xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100">
  <text dy="6.4" style="font-size:20px">
    <textPath  startOffset="0%" href="#gaugeArc"> ▶
    <animate attributeName="startOffset" from="0%" to="0%" begin="indefinite" dur="1s" calcMode="spline" keySplines="0.42,0,0.58,1"/>
    </textPath>
    </text>
  </svg>
  <input type="range"  min="0" max="100" step="1" value="80" onchange="changeArc(this)" />
</div>

→ Ссылка
Автор решения: Alexandr_TT

Основная идея такова: вы вычисляете конечную точку шкалы с помощью getPointAtLength и перемещаете стрелку в эту точку.

Также вы рассчитываете угол пути в конечной точке, чтобы можно было соответствующим образом повернуть стрелку.

Для расчета угла вам понадобятся 2 точки:

точка1, т. е. конечная точка шкалы.

точка2, т. е. слегка смещенная точка (val + 0,1).

Это будет работать, если значение ползунка не равно 100. В этом случае я использую стрелку в качестве маркера.

let length = chart.getTotalLength();
chart.style.strokeDasharray = length;
chart.style.strokeDashoffset = length;

function gauge(rangeValue) {
  let val = length - (length * rangeValue) / 100;
  if (rangeValue < 100 && rangeValue > 0) {
    let point1 = chart.getPointAtLength(val);
    let point2 = chart.getPointAtLength(val + 0.1);
    let angle = Math.atan2(point2.y - point1.y, point2.x - point1.x);
    arrow.setAttribute(
      "transform",
      "translate(" +
        [point1.x, point1.y] +
        ")" +
        "rotate(" +
        (angle * 180) / Math.PI +
        ")"
    );
  } else {
    chart.setAttribute("marker", "url(#m)");
  }
  chart.style.strokeDashoffset = val;
}

gauge(Number(range.value));

range.addEventListener("input", () => {
  let rangeValue = Number(range.value);
  gauge(rangeValue);
});
svg{border:solid;}
<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100">

  <path id="chart" d="M 34.00 91.37 A 42 42 0 1 1  76.00 91.37" fill="none" stroke="#3e3eff" stroke-width="6" transform="scale(-1, 1) translate(-110, 0)"  marker="url(#m)"></path>
  <marker id="m">
    <path id="markerarrow" fill="red" d="M0,0L0,-10L-14,0L0,10" />
  </marker>
  <use href="#markerarrow" id="arrow"/>
</svg>
</br>
<p><input type="range" id="range" min="0" max="100" step="1" value="80" /></p>

Свободный перевод ответа от участника @enxaneta.

→ Ссылка
Автор решения: Alexandr_TT

В этом примере я просто использую простой многоугольник в качестве стрелки. Стрелка поворачивается от центральной точки.

var svg = document.getElementById('svg01');

document.forms.form01.range.addEventListener('change', e => {
  let rotate = 240 * e.target.value / 100;
  svg.querySelector('circle').setAttribute('stroke-dashoffset', rotate - 360);
  svg.querySelector('.polygon').setAttribute('transform', `rotate(-${rotate})`);
});
body {
  display: flex;
}

svg {
  border: solid;
  height: 90vh;
}
<svg id="svg01" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <g transform="translate(50 50) rotate(30)">
    <circle r="40" fill="none" stroke="#3e3eff" stroke-width="6" pathLength="360" stroke-dasharray="240 360" stroke-dashoffset="-200" />
    <g class="polygon" transform="rotate(-160)">
      <polygon transform="translate(40 0)" points="0,-7 10,7 -10,7 0,-7" fill="red" />
    </g>
  </g>
</svg>
<form name="form01">
  <input type="range" name="range" min="0" max="100" step="1" value="66" />
</form>

Свободный перевод ответа от участника @chrwahl.

→ Ссылка