Управление SVG-координатами `path` с помощью JavaScript

92
01 октября 2021, 08:10

У меня есть path SVG со следующими точками в атрибуте d.

Есть ли способ изменять только одно из координатных чисел в 'd', скажем, как 0 в "L25 0", чтобы манипулировать в сценарии JavaScript?
Если это возможно, то как бы выглядел этот синтаксис?

Я хочу получить доступ к одному из координатных чисел, чтобы я мог увеличить его, и тем самым анимировать path.

function NavHalf() {
  var arrow = document.getElementById("arrowUp");
  arrow.setAttribute('d', 'M0 25 L25 0 L50 25');
}
<svg height="25" width="50" onclick="NavHalf()">
   <path id="arrowUp" d="M0 0 L25 25 L50 0 " style="stroke:green;stroke-width:2; fill:none" />
</svg>

Свободный перевод вопроса Manipulating SVG Path Coordinate with Vanilla JavaScript от участника @Nhan Bui.

Answer 1

Часть 2

Стандартный JavaScript

Упростить работу с отдельными значениями координат path можно и на чистом JS, без зависимостей. Например, можно добавить недостающее DOM-свойство d, и перехватывать обращения к нему используя Proxy (ES6+).

Следующий пример демонстрирует визуализацию частот во время воспроизведения звукового файла. Отрисовка линии выполняется через path (130 точек):

function attributesSerializer(element, attributes) { 
  return new Proxy(element, { 
    get(el, prop) { 
      if (prop === '$attrs') return this.$attrs;  
      if (this.$attrs[prop]) return this.$attrs[prop];  
      const val = el[prop];  
      return (typeof val === 'function') ? val.bind(el) : val;  
    },  
    set(el, prop, val) { 
      if (prop in this.$attrs) return false;  
      return ((el[prop] = val), true);  
    },  
    $attrs: attributes.reduce((r, { attr, delim=' ' }) => { 
      r[attr] = new Proxy({}, { 
        get(obj, i) { 
          const values = element.getAttribute(attr).split(delim).map(v => v.trim());  
          return values[i]; 
        },  
        set(obj, i, newVal) { 
          const values = element.getAttribute(attr).split(delim).map(v => v.trim());  
          values[i] = newVal;  
          element.setAttribute(attr, values.join(delim));  
          return true; 
        } 
      });  
      return r;  
    }, {})  
  });  
} 
 
const path = attributesSerializer(document.querySelector('path'), [ 
  { attr: 'd', delim: '\n' } 
]);  
let analyser, bufferLen, freqData; 
 
document.querySelector('input').addEventListener('change', e => { 
  const audio = document.querySelector('audio'); 
  audio.src = URL.createObjectURL(e.target.files[0]); 
  audio.load(); 
  const ac = new AudioContext(), 
        src = ac.createMediaElementSource(audio); 
  analyser = ac.createAnalyser(); 
  src.connect(analyser); 
  analyser.connect(ac.destination); 
  bufferLen = (analyser.fftSize = 512) / 2; 
  freqData = new Uint8Array(bufferLen); 
  audio.play(); 
  loop(); 
}); 
 
function loop() { 
  analyser.getByteFrequencyData(freqData); 
  path.d[0] = `M0,100 0,${100 - freqData[0] / 2.55}`; 
  for (var i = 2; i < bufferLen; i += 2) 
    path.d[1 + i / 2] = `${100 * i / bufferLen},${100 - freqData[i] / 2.55}`; 
  path.d[i] = 'L100,100 Z'; 
  requestAnimationFrame(loop); 
}
body { display: flex; flex-flow: column nowrap; align-items: stretch; height: calc(100% - 32px); } 
svg { align-self: center; width: 100%; max-width: 400px; height: 100px; outline: 1px dashed #ccc; } 
input { margin: 0.5rem 0; } 
audio { width: 100%; outline: none; } 
input:invalid + audio { display: none; }
<svg viewBox="0 0 80 100" preserveAspectRatio="none"> 
  <path d="" fill="#def" stroke="#00f" stroke-width="0.1" /> 
</svg> 
<input type="file" accept="audio/*" required> 
<audio controls></audio>

Здесь функция attributesSerializer возвращает экземпляр Proxy, привязанный к DOM-объекту элемента path.
Этим прокси мы ловим только обращения к добавленному свойству d - которое так же отслеживается через прокси (вложенный), что позволяет считывать и присваивать значения отдельных точек в одноименном атрибуте через квадратноскобочную нотацию вида pathElement.d[индексТочки].
В качестве символа-разделителя для (де)сериализации атрибута используется символ \n.

Такой подход обладает рядом недостатков. Основными минусами реализации являются ее относительная сложность, низкое быстродействие, и то что она является "велосипедом".

Поэтому, для решения таких задач, всегда лучше использовать реактивность. Ведь инструменты которые ее реализуют, предоставляют также массу других возможностей, помимо работы с атрибутами SVG.

Answer 2

На самом деле нет никакого основанного на DOM способа прямого доступа и манипулирования только одной из точек в определении пути.

Манипулирование строками - это все, что вы можете сделать.

function NavHalf(x,y) {
  var arrow = document.getElementById("arrowUp");
  arrow.setAttribute('d', 'M0 0 L' + x + ',' + y + ' L50 0');
}
<button type="button" onclick="NavHalf(25,0)">Position 1</button>
<button type="button" onclick="NavHalf(20,15)">Position 2</button>
<button type="button" onclick="NavHalf(25,25)">Position 3</button>
<svg height="25" width="50">
   <path id="arrowUp" d="M0 0 L25 25 L50 0" style="stroke:green;stroke-width:2; fill:none" />
</svg>

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

Answer 3

Часть 1

Так как значение отдельной координаты здесь не является объектом/свойством DOM, прямые манипуляции с этим значением в JS невозможны - вместо этого мы манипулируем атрибутом, изменяя его строковое значение.
И, существует...

Типовой подход - реактивные библиотеки, фреймворки

(только критика, без примеров)

Сейчас уже мало кто пишет голый JS для решения подобных задач: распространена практика использования реактивных библиотек, чтобы упростить работу - и писать меньше кода, тем самым облегчая его поддержку.
Ну, так это должно работать...
Посмотрим как это выглядит для начинающего:

Для начала, нужно выбранную библиотеку подключить. Или две библиотеки, в случае реакта. Теперь страничка грузится чуть дольше. Хм, ну ладно, вроде мелочь - можно потерпеть ради преимуществ.
Далее надо написать бойлерплейт-код, в который поместим ту логику связи с DOM, которую мы хотим. Тут уже возникает смутное ощущение, что... а, неважно. Напишем. Мыжпрограммисты, в конце концов.
Но как-то это не очень удобно, в разметке набирать. Перенесем код в отдельный модуль - и бандлером будем все вместе собирать. Вебпак, то что нужно! Поглядим как его настраив... оооо... нет, НЕТ, мы не будем его настраивать. Все ж работает, да? Вот и хорошо, вот и славно.
Чтобы быстрее оправиться от психической травмы нанесенной вебпаком, изучим и применим TS - не надо будет сильно думать о логике выражений с типкастом. В самом деле, пусть машина думает, а нам и бойлерплейтов хватает... ведь мы уже по уши в фреймворке.

Вышло так, что в попытке упростить свою работу, мы ее усложняем.
Инструменты, призванные помочь сконцентрироваться на программировании, уводят все дальше от программирования: мы npm install половину задач, и зависимы от внутренней магии готовых решений.
Экономия времени и совершенствование библиотек это в целом хорошо, если бы разработчики не забывали язык(!) и не теряли гибкость ума, попадая в ловушку бездумного потребления.

Многобукв, но зачем? Просто этим подвожу к альтернативному варианту, который вдохновляет лично меня. К варианту, который хотя бы частично вернет мотивацию вникать, понимать, развивать фундаментальные навыки - и к отказу от "жизни в фреймворке", в пользу реализации реактивности на уровне языка.
Этот вариант существует больше на уровне идеи, но есть несколько первых попыток ее воплотить. Здесь упомяну только одну молодую реализацию, а именно...

Svelte

<svg height="25" width="50" on:click={toggle}>
   <path d="M0 0 L25 {$midY} L50 0" style="stroke:green; stroke-width:2; fill:none;" />
</svg>
<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';
  const midY = tweened(0, { duration: 250, easing: cubicOut });
  const toggle = () => midY.set($midY ? 0 : 25);
</script>

READ ALSO
Импорт модуля Node/JS, require/import, module/commonjs

Импорт модуля Node/JS, require/import, module/commonjs

Пытаюсь достучаться до импорта модулей

172
Как правильно в данном случае поступить с функцией, как при вызове функции пропускать параметры

Как правильно в данном случае поступить с функцией, как при вызове функции пропускать параметры

Есть задачаНаписать функцию, которая принимает время (часы, минуты, секунды) и выводит его на экран в формате «чч:мм:сс»

199
JS. Как обращаться к вложенным функциям?

JS. Как обращаться к вложенным функциям?

Как обратиться к вложенной функции и вложенной в нее функции?

180
Округление числа до следующего целого числа десятков

Округление числа до следующего целого числа десятков

Возможно ли в JavaScript округлять числа по следующей схеме:

131