Как добавить узловые точки в генераторе кривых

158
14 января 2020, 19:10

Вопрос инициирован отличным ответом на вопрос по созданию полукруглых надписей в SVG.

Ниже авторский код генератора кривых:

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> 
<input value="Quick brown fox jumps over the lazy dog."  
       onkeyup="d3.select('textPath').html(this.value)"> 
<input id="result"> 
<svg> 
 <text> 
    <textPath href="#path"> 
      Quick brown fox jumps over the lazy dog. 
    </textPath> 
  </text> 
</svg> 
 
<script> 
let points = [[50,50],[300,100],[550,50]]; 
let dragged = null; 
let selected = points[points.length-1]; 
let line = d3.line().curve(d3.curveCardinal); 
let svg = d3.select("svg"); 
let path = svg.append("path").datum(points).attr('id', 'path'); 
 
svg.on("mousemove", mousemove).on("mousedown", mousedown) 
d3.select(window).on("mouseup", mouseup).on("resize", adjustSize); 
window.oncontextmenu = () => false; 
adjustSize(); 
redraw(); 
 
function adjustSize() { 
    let w = window.innerWidth; 
    let h = window.innerHeight; 
    svg.attr("width", w).attr("height", h) 
        .attr("viewBox", `0 0 ${w} ${h}`); 
} 
 
function redraw() { 
     
    svg.select("path").attr("d", line); 
    d3.select('input#result').attr('value', svg.select("path").attr('d')) 
    var circle = svg.selectAll("circle.knob") 
        .data(points, d => d); 
         
    circle.exit().remove(); 
     
    let newNodes = circle.enter() 
        .append("circle") 
        .classed('knob', true) 
        .attr("r", 1e-6) 
        .on("dblclick", deletePoint) 
        .on("mousedown", d => redraw(selected = dragged = d)) 
        .transition() 
        .duration(250) 
        .attr("r", 6.5); 
 
    circle.merge(newNodes) 
        .classed("selected", d => d === selected) 
        .attr("cx", d => d[0]) 
        .attr("cy", d => d[1]); 
         
    if (d3.event) { 
        d3.event.preventDefault(); 
        d3.event.stopPropagation(); 
    } 
} 
 
function mousemove() { 
    if (!dragged) return; 
    let m = d3.mouse(svg.node()); 
    dragged[0] = m[0]; 
    dragged[1] = m[1]; 
    redraw(); 
} 
 
function mouseup() { 
    if (!dragged) return; 
    mousemove(); 
    dragged = null; 
} 
 
function deletePoint(d) { 
    if (!selected) 
        return; 
    let i = points.indexOf(selected); 
    points.splice(i, 1); 
    selected = points.length ?  
        points[i > 0 ? i - 1 : 0] : null; 
    redraw(); 
} 
 
function mousedown() { 
    if (d3.event.button !== 0) 
        return; 
    points.push(selected = dragged =  
                d3.mouse(svg.node())); 
    redraw(); 
} 
 
</script> 
 
<style> 
body, svg { 
    position: absolute; 
    margin: 0; 
    width: 100vw; 
    height: 100vh; 
    overflow: hidden; 
    user-select: none; 
} 
path { 
    fill: none; 
    stroke: red; 
} 
circle { 
    stroke: red; 
    fill: #fff; 
    fill-opacity: .4; 
} 
.selected {fill: #ff7f0e} 
text {font-size:30px} 
input {width:100%} 
</style>

  • Генератор позволяет добавлять текст и создавать разнообразные формы кривых с помощью перетаскивания узловых точек.

  • В textarea динамически выводится формула path

  • Есть возможность добавлять новые узловые точки

    Можно ли внести некоторые улучшения в функционал генератора?

    1. Добавить возможность выбора количества знаков после запятой в формуле path
    2. Изменить порядок добавления новых узловых точек,- в любое место кривой, а не только в конец её.

Добавляем точку, как бы в середину кривой

Но новая точка добавилась в конец кривой

  1. Приложение не работает в Firefox на локальном ПК
Answer 1

Я сделялъ :)

Кликами точки можно добавлять, а даблкликами - удалять.

Тут есть несколько фокусов:

  1. Определение ближайшей точки к мышке, которая лежит на линии, осуществляется за счет того, что каждый раз когда <path> меняется - я строю диаграмму Вороного d3.voronoi() для точек лежащих на получившемся пути с фиксированным шагом.

Такое разбиение можно увидеть если убрать вот этот стиль

path.voronoi {
  fill: transparent;
  stroke: none;
}

и вот в этой строчке поставить 20 а не 2

.data(voronoi.polygons(sample(path.node(), 2)))

Проверка попадания точки в ячейку диаграммы делает браузер за счет механизма mouseover.

  1. Второй фокус - определение между какими опорными точками вставлять вновь добавляемую - решил кое как на коленке, вероятно над этой частью еще стоит подумать. Т.к. придуманные мною костыли не работают для аппроксимаций линий, которые не проходят через опорные точки d3.curveBasis например, по-этому другие типы линий пока что убраны из этой поделки.

Сейчас для генерации кривой по опорным точкам используется алгоритм d3.curveCardinal.

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> 
 
<input value="Quick brown fox jumps over the lazy dog." onkeyup="changeText(this.value)"> 
 
<input id="result"> 
 
<select style="width:20%;display:none" onchange="changeProperties()"> 
    <option value="curveCardinal">cardinal</option> 
    <option value="curveCardinalClosed">cardinal closed</option> 
    <option value="curveBasis">basis</option> 
    <option value="curveBasisClosed">basis closed</option> 
</select> 
 
<input id="tension" type="range" style="width:20%" min="-100" max="100" value="50" onchange="changeProperties()"> 
 
<br> 
 
<svg> 
    <text><textPath href="#path"></textPath></text> 
    <g class="voronoi"></g> 
    <g class="path"><path id="path"></path></g> 
    <g class="anchors"></g> 
    <g class="sticky"><circle r="3" cx="-100" cy="-100"></circle></g> 
</svg> 
 
<style> 
  body, svg { 
      position: absolute; 
      margin: 0; 
      width: 100vw; 
      height: 100vh; 
      overflow: hidden; 
      user-select: none; 
  } 
  path { 
      fill: none; 
      stroke: red; 
  } 
  path.voronoi { 
      fill: transparent; 
      stroke: none; 
  } 
  circle { 
      stroke: red; 
      fill: #fff; 
      fill-opacity: .4; 
      cursor:pointer; 
  } 
  g.sticky circle { 
      fill: none; 
      pointer-events:none; 
  } 
  .selected { 
      fill: #ff7f0e; 
  } 
  text { 
      font-size: 30px; 
  } 
  input { 
      width: 100%; 
  } 
  input[type=range] { 
      height:6px; 
  } 
  input[type=range]::-webkit-slider-runnable-track { 
      height:23px; 
  } 
</style> 
 
<script> 
var voronoi, dragged; 
 
let points = [[50, 50], [300, 100], [550, 50]]; 
 
let selected = points[points.length - 1]; 
 
let line = d3.line() 
             .curve(d3.curveCardinal); 
 
let svg = d3.select("svg"); 
 
let path = svg.select("path#path") 
              .datum(points); 
 
let stick = d3.select('g.sticky circle'); 
 
svg.on("mousemove", mousemove) 
   .on("mousedown", mousedown); 
 
d3.select(window) 
  .on("mouseup", mouseup) 
  .on("resize", adjustSize); 
 
window.oncontextmenu = () => false; 
adjustSize();  
redraw();  
resample(); 
changeText(); 
 
function adjustSize() { 
 
    let w = window.innerWidth, h = window.innerHeight; 
     
    svg.attr("width", w) 
       .attr("height", h) 
       .attr("viewBox", `0 0 ${w} ${h}`); 
        
    voronoi = d3.voronoi() 
                .x(d => d.x) 
                .y(d => d.y) 
                .size([w, h]) 
    resample(); 
} 
 
function redraw() { 
 
    let datum = path.attr("d", line).attr('d'); 
 
    datum = datum.replace(/\d+\.\d+/g, s => parseFloat(s).toFixed()) 
 
    path.attr("d", datum) 
 
    d3.select('input#result') 
      .attr('value', datum); 
     
    var circle = svg.select("g.anchors") 
                    .selectAll("circle.knob") 
                    .data(points, d => d); 
 
    circle.exit().remove(); 
 
    let newNodes = circle.enter() 
        .append("circle") 
        .classed('knob', true) 
        .attr("r", 1e-6) 
        .on("dblclick", deletePoint) 
        .on("mousedown", d => redraw(selected = dragged = d)) 
        .on("mousemove", () => stick.attr('opacity', 0)) 
        .transition() 
        .duration(250) 
        .attr("r", 6.5); 
 
    circle.merge(newNodes) 
        .classed("selected", d => d === selected) 
        .attr("cx", d => d[0]) 
        .attr("cy", d => d[1]); 
 
    if (d3.event) { 
        d3.event.preventDefault(); 
        d3.event.stopPropagation(); 
    } 
} 
 
function mousemove() { 
    if (!dragged) return; 
    let m = d3.mouse(svg.node()); 
    dragged[0] = m[0]; 
    dragged[1] = m[1]; 
    redraw(); 
} 
 
function mouseup() { 
    if (!dragged) return; 
    mousemove(); 
    dragged = null; 
    resample(); 
} 
 
function resample() { 
    try{ 
      let samples = sample(path.node(), 2); 
 
      svg.selectAll("path.voronoi").remove() 
 
      svg.select('g.voronoi') 
         .selectAll("path.voronoi") 
         .data(voronoi.polygons(samples)) 
         .enter() 
         .append("path") 
         .attr('class', 'voronoi') 
         .attr("d", d =>`M${d.join('L')}Z`) 
         .on('mouseover', function(d) { 
              let m = d3.mouse(svg.node()); 
              stick.attr('opacity', dist(m, d.data) < 5 ? 1 : 0) 
                .datum({x: d.data.x, y: d.data.y}) 
                .attr('cx', d => d.x) 
                .attr('cy', d => d.y) 
         }); 
     } catch {} 
} 
 
function deletePoint(d) { 
    if (!selected) return; 
    let i = points.indexOf(selected); 
    points.splice(i, 1); 
    selected = points.length ? points[i > 0 ? i - 1 : 0] : null; 
    redraw(); 
    resample(); 
} 
 
function mousedown() { 
    if (d3.event.button !== 0) return; 
    let pt = [stick.datum().x, stick.datum().y]; 
    let m = d3.mouse(svg.node()); 
    if (dist(m, pt)<5) {    
        points.splice(calcIndex(pt), 0, selected = dragged = pt) 
    } else { 
        points.push(selected = dragged = m);     
    } 
    redraw(); 
    resample(); 
    setTimeout(()=>stick.attr('opacity', 0),100) 
} 
 
function dist(p1, p2){ 
    let x = (p1.x || p1[0]) - (p2.x || p2[0]); 
    let y = (p1.y || p1[1]) - (p2.y || p2[1]); 
    return Math.sqrt(x*x + y*y); 
} 
 
function calcIndex(pt) { 
  let pathNode = path.node(); 
  let total = pathNode.getTotalLength(); 
  let index = 1; 
  let minDistToAnchorPoint = Number.MAX_VALUE; 
  let minDistToStickPoint = Number.MAX_VALUE; 
  for (let l = 0; l <= total; l++) { 
    let p = pathNode.getPointAtLength(l); 
    let distToCurrentAnchorPoint = dist(p, points[index]); 
    if (distToCurrentAnchorPoint < minDistToAnchorPoint)  { 
        minDistToAnchorPoint = distToCurrentAnchorPoint; 
    } else { 
        index++ 
        minDistToAnchorPoint = Number.MAX_VALUE; 
    } 
    if (dist(p, pt)<2)   
        break; 
  } 
  return index 
} 
 
function changeProperties(){ 
    let type = document.querySelector('select').value; 
    let v = document.querySelector('#tension').value/100; 
    type = d3[type]; 
    type.tension && (type = type.tension(v)) 
    line = d3.line().curve(type); 
    redraw(); 
    resample(); 
} 
 
function sample(pathNode, precision) { 
  let total = pathNode.getTotalLength(); 
  let samples = []; 
  for (let l = 0; l <= total; l += precision) { 
    samples.push(pathNode.getPointAtLength(l)); 
  } 
  return samples; 
} 
 
function changeText(text) { 
  d3.select('textPath').html(text) 
} 
</script>

READ ALSO
Смена css темы на сайте

Смена css темы на сайте

Подскажите способы, как можно реализовать смену темы на сайтеНапример есть светлая и темная тема

157
JS. Курсы валют

JS. Курсы валют

Господа, помогите новичку в первых шагахХочу научится отображать курсы валют/нефти/крипты и прочего

291
Плавное перемещение блоков на jquery

Плавное перемещение блоков на jquery

Есть код для фиксированной шапки, в первом положении блок находится за контейнером, после прокрутки страницы на 100px добавляется класс где...

145
Запрет адаптивности Vue + Vuetify

Запрет адаптивности Vue + Vuetify

Необходимо сделать две версии сайта на поддоменах (mapp

133