SVG Как генерировать случайные пути?

Есть вот такой простой пример.

const btn = document.getElementById('generator_btn');
const textarea = document.getElementById('textarea_generator');
const generatorField = document.getElementById('generator_field');
const generatorColor = document.getElementById('generator_color');

// Массив возможных путей для фигур ... это надо переделать, но не знаю как. 
const paths = [
  '<path class="st1" d="M1.8 23s15.2-7.7 24-7.9 14.4-.5 23.7 7.5c9.3 8 3.6 17.9-1 20-2.7 1.3-7.9.6-6.2-5.5 1.6-5.6 7.7-9.1 16.6-12.4 8.9-3.3 28.9-8.5 32.9-12.9 2.3-2.6 2.8-6-.3-7-2-.6-5.8.6-8.1 2.9C81.3 10 78.3 15.3 79 39c.5 19-20 22.2-25.1 17.8-6.1-5.2 1.5-15.4 8.7-18.7 7.2-3.3 14.7-5.9 21.1-3.1 6.4 2.8 10.8 5 13.5 5.3 2.7.2 9.3 0 15.4-6.9"/><path class="st1" d="m110.9 40.6 4.4-9.8-10.5 3.6"/>',
  '<path class="st1" d="M1.8 40.1s4.9-10 10.8-14.8c5.9-4.8 14.4-6.3 17.6-5.8 5.9.8 12.7 6.2 19.3 12.7 6.7 6.5 14.8 14.2 28.7 6.1 7.2-4.2 15.3-17.1 7.2-25.5-6.4-6.7-18 2.9-21.7 11.7-1.9 4.6-5 20 2.7 25.7 7.8 5.8 17.2 3.1 27.4-4.4 10.2-7.6 22.6-24 24-27"/><path class="st1" d="m112.6 21.1 5.6-3.9.5 8.7"/>',
  '<path class="st1" d="M.8 41.8s14-21 28.3-14.3c14.4 6.7 7.2 20.3 1.7 18.5-5.9-1.9-7-20 9.4-21.8s23.6 5 32.7 8.5c9.1 3.5 33.8 6.6 44.3-10.2"/><path class="st1" d="m111.2 25.9 6.2-4-.8 6"/>'
];

generatorColor.addEventListener('input', function() {
  includeSvg();
});

btn.addEventListener('click', function() {
  includeSvg();
});

function includeSvg() {
  let currentColor = generatorColor.value;
  // 
  let randomPath = paths[Math.floor(Math.random() * paths.length)];
  let svgCode = generateSVG(currentColor, randomPath);
  textarea.value = svgCode;
  generatorField.innerHTML = svgCode;
}

function generateSVG(currentColor, path) {
  let startFigure = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" height="60" x="0" y="0" viewBox="0 0 120 64"><style>.st1{fill:none;stroke:' + currentColor + ';stroke-width:2;stroke-linecap:round;stroke-linejoin:round}</style>';
  let endFigure = '</svg>';
  return startFigure + path + endFigure;
}
body {
  margin: 0;
}

#generator_btn {
  margin: 0;
  display: inline-block;
  padding: 10px 15px;
  font-weight: 700;
  vertical-align: top;
}

#generator_color {
  height: 40px;
}

#textarea_generator {
  display: block;
  width: 100%;
  max-width: 90vw;
  margin: 10px 0;

}
<div class="gen_block">
  <button id="generator_btn">Generator SVG</button>
  <input type="color" id="generator_color" name="generator_color" value="#FE6C3B" />
</div>
<textarea id="textarea_generator" name="generator_svg" rows="4"></textarea>
<div id="generator_field"></div>

Идея состоит в том, что б сделать по, нажатию на кнопку, генератор случайных вот таких путей.... кракозябр, но я не могу понять как это реализовать. По сути svg элемент состоит из двух путей В первом - каким-то образом запутаная кривая, (но там должен быть плавный переход), а во втором стрелочка которая заканчиваеться в этом пути. Ну и динамическая разукрашка сгенерированного пути, что б можно было назначить цвет svg элементу перед сохранением. Подскажите, пожалуйста как создавать эти случайные пути в приделах этой области элемента ?


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

Автор решения: Serg Bakay

попробуйте воспользоваться подобными библиотеками: kute.js

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

Генератор случайных путей благодаря варианту @XelaNimed получилось сделать. Но качество меня не устраивает... Не получаеться с петлями, только рендомные прямые у путей. Может кто-то знает, как улучшить что б оно смотрелось более плавно и не вылазило за приделы области SVG?

const btn = document.getElementById('generator_btn');
const textarea = document.getElementById('textarea_generator');
const generatorField = document.getElementById('generator_field');
const generatorColor = document.getElementById('generator_color');

const W = 120;
const H = 64;
const MIN_ANGLE = 60;
const MIN_DISTANCE = (Math.min(W, H)) / 20;
const MAX_DISTANCE = (Math.min(W, H)) / 4;
let lastTwoPoints = [];

generatorColor.addEventListener('input', function() {
  let currentSvg = textarea.value;
  let strokeValue = /stroke:[^;]+;/.exec(currentSvg)[0];
  let updatedSvg = currentSvg.replace(strokeValue, 'stroke:' + generatorColor.value + ';');
  textarea.value = updatedSvg;
  generatorField.innerHTML = updatedSvg;
});

btn.addEventListener('click', function() {
  includeSvg();
});

function includeSvg() {
  let currentColor = generatorColor.value;
  let randomPath = generateRandomPath();
  let svgCode = generateSVG(currentColor, randomPath);
  textarea.value = svgCode;
  generatorField.innerHTML = svgCode;
}

function generateRandomPath() {
  const startX = getRandomNumber(W * 0.6) + W * 0.2;
  const startY = getRandomNumber(H * 0.6) + H * 0.2;
  let pathString = `<path d="M${startX} ${startY} `;

  for (let i = 0; i < 10; i++) {
    const controlX1 = getRandomNumber(W * 0.6) + W * 0.2;
    const controlY1 = getRandomNumber(H * 0.6) + H * 0.2;
    const controlX2 = getRandomNumber(W * 0.6) + W * 0.2;
    const controlY2 = getRandomNumber(H * 0.6) + H * 0.2;
    const endX = getRandomNumber(W * 0.6) + W * 0.2;
    const endY = getRandomNumber(H * 0.6) + H * 0.2;

    pathString += `${controlX1} ${controlY1} ${controlX2} ${controlY2} ${endX} ${endY}`;
  }

  return pathString + '"/>';
}

function getDistance(pointA, pointB) {
  return Math.sqrt((pointA[0] - pointB[0]) ** 2 + (pointA[1] - pointB[1]) ** 2);
}


function getAngle(pointA, pointB, pointC) {
  let a = getDistance(pointA, pointB);
  let b = getDistance(pointB, pointC);
  let c = getDistance(pointC, pointA);
  return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * (180 / Math.PI);
}


function getRandomPoint() {
  let x = getRandomNumber(W * 0.6) + W * 0.2;
  let y = getRandomNumber(H * 0.6) + H * 0.2;

  let point = [x, y];

  if (lastTwoPoints.length < 2) {
    lastTwoPoints.push(point);
  } else {
    if (getAngle(...lastTwoPoints, point) < MIN_ANGLE ||
      getDistance(lastTwoPoints[1], point) < MIN_DISTANCE ||
      getDistance(lastTwoPoints[1], point) > MAX_DISTANCE) {
      point = getRandomPoint();
    } else {
      lastTwoPoints.shift();
      lastTwoPoints.push(point);
    }
  }

  return `${point[0]} ${point[1]}`;
}

function generateSVG(currentColor, path) {
  let startFigure = '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="128" viewBox="0 0 120 64"><style>path{fill:none;stroke:' + currentColor + ';stroke-width:2;stroke-linecap:round;stroke-linejoin:round}</style>';
  let endFigure = '</svg>';
  return startFigure + path + endFigure;
}

function getRandomNumber(max) {
  return Math.floor(Math.random() * max);
}
body {
  margin: 0;
}

#generator_btn {
  margin: 0;
  display: inline-block;
  padding: 10px 15px;
  font-weight: 700;
  vertical-align: top;
}

#generator_color {
  height: 40px;
}

#textarea_generator {
  display: block;
  width: 100%;
  max-width: 90vw;
  margin: 10px 0;
}
<div class="gen_block">
  <button id="generator_btn">Generator SVG</button>
  <input type="color" id="generator_color" name="generator_color" value="#FE6C3B" />
</div>
<textarea id="textarea_generator" name="generator_svg" rows="10"></textarea>
<div id="generator_field"></div>

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

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

https://developer.mozilla.org/ru/docs/Web/SVG/Tutorial/Paths

остальная инфа впрочем тоже не помешает, но это главное в рамках данного примера.

p.s. дополнял фактически одну строку и то методом тыка, потому и результат такой...)))

// за основу взят ваш же ответ


пометка для прохожих:

если хотите изобрести себе подпись для паспорта, то что вышло идеально подходит :3

UPD 15 01 2024 / 19:25

и вьюбокс растянул, и кракозябру увеличил, и рандом сделал повеселее, ну и заспойлерил чтоб не мешалось при скроле :3

UPD 16 01 2024 / 23:35

добавил стрелку - маркер

источник https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker

p.s. в сниппете из-за увеличения смотреть бестолку, разворачивайте на весь экран

const btn = document.getElementById('generator_btn');
const textarea = document.getElementById('textarea_generator');
const generatorField = document.getElementById('generator_field');
const generatorColor = document.getElementById('generator_color');

const W = 150;
const H = 125; /*!!!!!!!!!!*/
const MIN_ANGLE = 60;
const MIN_DISTANCE = (Math.min(W, H)) / 20;
const MAX_DISTANCE = (Math.min(W, H)) / 4;
let lastTwoPoints = [];

generatorColor.addEventListener('input', function() {
  let currentSvg = textarea.value;
  let strokeValue = /stroke:[^;]+;/.exec(currentSvg)[0];
  let updatedSvg = currentSvg.replace(strokeValue, 'stroke:' + generatorColor.value + ';');
  textarea.value = updatedSvg;
  generatorField.innerHTML = updatedSvg;
});

btn.addEventListener('click', function() {
  includeSvg();
});

function includeSvg() {
  let currentColor = generatorColor.value;
  let randomPath = generateRandomPath();
  let svgCode = generateSVG(currentColor, randomPath);
  textarea.value = svgCode;
  generatorField.innerHTML = svgCode;
}

function generateRandomPath() {
  const startX = getRandomNumber(H * 0.8);
  const startY = getRandomNumber(H * 0.8);
  let pathString = `M${startX} ${startY} `;
var t=document.getElementById('t');

let x1, x2, x3, x4, y1, y2, y3, y4;
let q = 1.3; // коэфицент отклонения контрольной точки
  for (let i = 0; i < 2; i++) {
x1 = getRandomNumber(H * 0.35 + ((i+i)*15));
y1 = getRandomNumber(H * 0.35);
x2 = getRandomNumber(H * 0.75);
y2 = getRandomNumber(H * 0.35);
x3 = getRandomNumber(H * 0.75);
y3 = getRandomNumber(H * 0.75);
x4 = getRandomNumber(H * 0.35);
y4 = getRandomNumber(H * 0.75);

pathString += '\n' + (i == 0 ? 'Q' : 'T') +
` ${x1} ${y1}, ${x1/q} ${y1/q} 
T ${x2} ${y2}, ${x2*q} ${y2/q}
T ${x3} ${y3}, ${x3*q} ${y3*q}
T ${x4} ${y4}, ${x4/q} ${y4*q}`;
  }
  t.value=pathString;
  return '<path marker-end="url(#arrow)" d="' + pathString + '"/>';
}

function getDistance(pointA, pointB) {
  return Math.sqrt((pointA[0] - pointB[0]) ** 2 + (pointA[1] - pointB[1]) ** 2);
}


function getAngle(pointA, pointB, pointC) {
  let a = getDistance(pointA, pointB);
  let b = getDistance(pointB, pointC);
  let c = getDistance(pointC, pointA);
  return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * (180 / Math.PI);
}


function getRandomPoint() {
  let x = getRandomNumber(W * 0.6) + W * 0.2;
  let y = getRandomNumber(H * 0.6) + H * 0.2;

  let point = [x, y];

  if (lastTwoPoints.length < 2) {
    lastTwoPoints.push(point);
  } else {
    if (getAngle(...lastTwoPoints, point) < MIN_ANGLE ||
      getDistance(lastTwoPoints[1], point) < MIN_DISTANCE ||
      getDistance(lastTwoPoints[1], point) > MAX_DISTANCE) {
      point = getRandomPoint();
    } else {
      lastTwoPoints.shift();
      lastTwoPoints.push(point);
    }
  }

  return `${point[0]} ${point[1]}`;
}

function generateSVG(currentColor, path) {
  let startFigure = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500"><style>path{fill:none;stroke:' + currentColor + ';stroke-width:2;stroke-linecap:round;stroke-linejoin:round}</style>';
  let endFigure = '</svg>';
  let arrow = ` <defs>
    <!-- A marker to be used as an arrowhead -->
    <marker
      id="arrow"
      viewBox="0 0 10 10"
      refX="5"
      refY="5"
      markerWidth="6"
      markerHeight="6"
      orient="auto-start-reverse">
      <path d="M 0 0 L 10 5 L 0 10 z" />
    </marker>
  </defs>`;
  /********************************************/
  return startFigure + path + arrow + endFigure;
}

function getRandomNumber(max) {
  return (Math.floor(Math.random() * max)) + 225;/*!!!!!!*/
}
body {margin: 0;}
#generator_color {height: 40px;}
#textarea_generator, #t{width: 47%;}

#generator_btn {
  margin: 0;
  display: inline-block;
  padding: 10px 15px;
  font-weight: 700;
  vertical-align: top;
}
<div class="gen_block">
<button id="generator_btn">Generator SVG</button>
<input type="color" id="generator_color" name="generator_color" value="#FE6C3B" />
</div>
<textarea id='t'></textarea>
<textarea id="textarea_generator" name="generator_svg"></textarea>

<div id="generator_field"></div>

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

В код добавлена кнопка показывающая контрольные отрезки, по которым строится кривая. Эти отрезки не полностью случайные: первый отрезок начинается на левом краю рисунка, затем несколько отрезков опираются на верхний и нижний края рисунка, последний отрезок заканчивается на правом краю.

Концы всех контрольных отрезков лежат внутри или на краю рисунка. Это гарантирует что кривая тоже целиком внутри.

const scale = (v, f) => {
    const [x, y] = v;
    return [f * x, f * y];
};

const normalize = v => scale(v, 1 / Math.hypot(...v));

const rotate = (v, a) => {
    const [x, y] = v;
    const c = Math.cos(a);
    const s = Math.sin(a);
    return [c * x - s * y, s * x + c * y];
};

const round = (v, f) => {
    const [x, y] = v;
    return [Math.round(f * x) / f, Math.round(f * y) / f];
};

const curveD = (segments, withArrow) => {
    const n = segments.length;
    let d = '';

    const [x1, y1] = segments[0][0];
    d += `M ${x1} ${y1}`;

    for (let i = 1; i < n - 1; ++i) {
        let [x2, y2] = segments[i - 1][1];
        let [[x3, y3], [x4, y4]] = segments[i];
        d += ` C ${x2} ${y2}, ${x3} ${y3}, ${(x3 + x4) / 2} ${(y3 + y4) / 2}`;
    }

    let [x2, y2] = segments[n - 2][1];
    let [[x3, y3], [x4, y4]] = segments[n - 1];
    d += ` C ${x2} ${y2}, ${x3} ${y3}, ${x4} ${y4}`;

    if (withArrow) {
        const v = scale(normalize([x3 - x4, y3 - y4]), 10);
        const [rx, ry] = round(rotate(v, Math.PI / 8), 100);
        const [lx, ly] = round(rotate(v, -Math.PI / 8), 100);

        d += ` l ${rx} ${ry} m ${-rx} ${-ry} l ${lx} ${ly}`;
    }

    return d;
};

const controlsD = segments => {
    const n = segments.length;
    let d = '';
    const r = 2;

    const drawPoint = (x, y) => {
        d += ` M ${x + r} ${y} a ${r} ${r} 0 0 0 ${-2 * r} 0`;
        d += ` M ${x - r} ${y} a ${r} ${r} 0 0 0 ${2 * r} 0`;
    };

    for (let i = 0; i < n; ++i) {
        const [[x1, y1], [x2, y2]] = segments[i];
        d += ` M ${x1} ${y1} L ${x2} ${y2}`;
        drawPoint(x1, y1);
        if (0 < i && i < n - 1) {
            drawPoint((x1 + x2) / 2, (y1 + y2) / 2);
        }
        drawPoint(x2, y2);
    }
    return d;
};

const drawRandomCurve = (svg, curve, controls, textarea) => {
    const width = svg.getAttribute('width');
    const height = svg.getAttribute('height');

    const randomX = () => Math.floor(width * Math.random());
    const randomY = () => Math.floor(height * Math.random());

    const randomControlSegments = n => {
        const segments = [];

        segments.push([
            [0, randomY()],
            [width - 1 - Math.floor(0.8 * randomX()), randomY()]
        ]);

        let y = (height - 1) * Math.floor(2 * Math.random());
        for (let i = 1; i < n - 1; ++i) {
            const p1 = [randomX(), y];
            y = height - 1 - y;
            const p2 = [randomX(), y];
            segments.push([p1, p2]);
        }

        segments.push([
            [Math.floor(0.8 * randomX()), randomY()],
            [width - 1, randomY()]
        ]);

        return segments;
    };

    const segments = randomControlSegments(5);
    const d = curveD(segments, true);

    curve.setAttribute('d', d);
    textarea.value = d;

    controls.setAttribute('d', controlsD(segments));
};


const doDraw = () => drawRandomCurve(
    document.getElementById('board'),
    document.getElementById('curve'),
    document.getElementById('controls'),
    document.getElementById('text')
);
document.getElementById('draw!').onclick = doDraw;
doDraw();

const showControls = () => {
    const show = document.getElementById('show_controls').checked;
    document.getElementById('controls').setAttribute(
        'visibility',
        (show) ? 'visible' : 'hidden'
    );
};
document.getElementById('show_controls').onclick = showControls;
showControls();
<button id="draw!">Обновить кривую</button>
<br>
<textarea id="text" cols="60", rows="3"></textarea>
<br>
<svg id="board" width="120" height="60" style="border: 1px solid gray;">
<path id="curve" stroke="black" stroke-width="2" fill="transparent"></path>
<path id="controls" stroke="red" stroke-width="1" fill="transparent" visibility="hidden"></path>
<br>
<input type="checkbox" id="show_controls" name="show_controls" />
<label for="show_controls">показать контрольные отрезки</label>
</svg>

→ Ссылка