在 JavaScript 中创建随机 SVG 曲线,同时避免急转弯

问题描述

下面的代码段在文档上绘制了一定次数随机连接曲线:

function createPath() {
  const 
    dimensions = getwindowDimensions(),svg = document.querySelector( `svg` ),path = document.createElementNS( `http://www.w3.org/2000/svg`,`path` );
  
  dimensions[0] = dimensions[0]; dimensions[1] = dimensions[1];
  svg.appendChild( path );
  
  path.setAttribute(
    `d`,`M ` +
    `${getRandomNumber(dimensions[0])} `+`${getRandomNumber(dimensions[1])} `+
    `C `+ 
    `${getRandomNumber(dimensions[0])} `+`${getRandomNumber( dimensions[1])},`+  
    
    `${getRandomNumber(dimensions[0])} `+`${getRandomNumber( dimensions[1])},`+
    
    `${getRandomNumber(dimensions[0])} `+`${getRandomNumber( dimensions[1])} `
  )
  
  for( let i = 0; i < 100; i++  ) {
    path.setAttribute(
      `d`,path.getAttribute( `d` ) + 
      `S `+`${getRandomNumber(dimensions[0])} `+`${getRandomNumber(dimensions[1])},`+
      
      `${getRandomNumber(dimensions[0])} `+`${getRandomNumber(dimensions[1])} `
    )
  }
}

setInterval( setSVGDimensions,10 ); setInterval( positionPath,10 );
createPath();
* { Box-sizing: border-Box; margin: 0; padding: 0; }
html,body { height: 100%; }
body {
  display: flex; justify-content: center; align-items: center;
}
svg {
  border-radius: 1rem; background-color: rgba( 95%,95%,0.5 );
  filter: blur( 1rem );

  animation-name: blur; animation-duration: 1s;
  animation-timing-function: ease-in-out; animation-fill-mode: forwards;

  stroke-linecap: round; stroke-linejoin: round;
  stroke-width: 0.25rem; fill: none;
}

@keyframes blur {
  100% { filter: blur( 0rem ); }
}

path {
  animation-name: grow;
  animation-duration: 2500s;
  animation-timing-function: cubic-bezier( 0.75,0.25,1 );

  stroke-dasharray: 1000000;
  stroke-dashoffset: 1000000;  
  stroke: rgba( 0%,100%,75%,0.75 );
}

@keyframes grow {
  100% { stroke-dashoffset: 0; }
}
<svg></svg>
<script>
  function getRandomNumber( max ) { return Math.floor( Math.random() * max ); }

  function getwindowDimensions() {
    const 
      dimensions = [],windowWidth = document.documentElement.clientWidth,windowHeight = document.documentElement.clientHeight;

    dimensions.push( windowWidth,windowHeight );
    return dimensions;
  }
  
  function setSVGDimensions() {
    const 
      dimensions = getwindowDimensions(),svg = document.querySelector( `svg` );

    svg.style.width = dimensions[0] - 10; svg.style.height = dimensions[1] - 10;
  }  
  
  function positionPath() {
    const
      dimensions = getwindowDimensions(),path = document.querySelector( `path` );

      path.setAttribute( 
        `transform`,`
          scale( 0.5 ) translate( ${ dimensions[0] / 2 },${ dimensions[1] / 3 } )
        ` 
      )
  }  
</script>

除了某些曲线的锐度之外,这是所需的行为。半径太小,角度太锐。我们想要更宽更平滑的曲线。例如,在此屏幕截图中,问题区域被圈出。

在下图中,请注意红色圆圈具有非常尖锐的曲线,而绿色圆圈则是更宽更平滑的曲线:

enter image description here

有没有办法可以使用 JavaScript 来防止创建尖锐曲线(红色圆圈)并让算法只创建更宽的曲线(绿色圆圈) )?

解决方法

我添加了一些函数来检查最后两点和下一个点之间的角度是否不小于 MIN_ANGLE。现在是 60 度,但可以更宽以获得更大的曲线半径。

我还添加了 MIN_DISTANCE,因为两点之间的距离太短也会提供尖锐的曲线。

let lastTwoPoints = [];
const MIN_ANGLE = 60;
const MIN_DISTANCE = (Math.min(...getWindowDimensions()))/10;

function getPoint(){
    let point = [getRandomNumber(getWindowDimensions()[0]),getRandomNumber(getWindowDimensions()[1])];

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

function pointString(){
    let point = getPoint();
    return `${point[0]} ${point[1]} `;
}

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

function getAngle(pointA,pointB,pointC){ // angle to pointB
    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 createPath() {
    const 
    dimensions = getWindowDimensions(),svg = document.querySelector( `svg` ),path = document.createElementNS( `http://www.w3.org/2000/svg`,`path` );

    dimensions[0] = dimensions[0]; dimensions[1] = dimensions[1];
    svg.appendChild( path );

    path.setAttribute(
    `d`,`M ` +
    `${pointString()}`+
    `C `+ 
    `${pointString()}`+  

    `${pointString()}`+

    `${pointString()}`
    )


    for( let i = 0; i < 100; i++  ) {
    path.setAttribute(
    `d`,path.getAttribute( `d` ) + 
    `S `+`${pointString()}`+

    `${pointString()}`
    )
    }
}

setInterval( setSVGDimensions,10 ); setInterval( positionPath,10 );
createPath();

function getRandomNumber( max ) { return Math.floor( Math.random() * max ); }

function getWindowDimensions() {
    const 
    dimensions = [],windowWidth = document.documentElement.clientWidth,windowHeight = document.documentElement.clientHeight;

    dimensions.push( windowWidth,windowHeight );
    return dimensions;
}

function setSVGDimensions() {
    const 
    dimensions = getWindowDimensions(),svg = document.querySelector( `svg` );

    svg.style.width = dimensions[0] - 10; svg.style.height = dimensions[1] - 10;
}  

function positionPath() {
    const
    dimensions = getWindowDimensions(),path = document.querySelector( `path` );

    path.setAttribute( 
    `transform`,`
    scale( 0.5 ) translate( ${ dimensions[0] / 2 },${ dimensions[1] / 3 } )
    ` 
    )
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html,body { height: 100%; }
body {
  display: flex; justify-content: center; align-items: center;
}
svg {
  border-radius: 1rem; background-color: rgba( 95%,95%,0.5 );
  filter: blur( 1rem );

  animation-name: blur; animation-duration: 1s;
  animation-timing-function: ease-in-out; animation-fill-mode: forwards;

  stroke-linecap: round; stroke-linejoin: round;
  stroke-width: 0.25rem; fill: none;
}

@keyframes blur {
  100% { filter: blur( 0rem ); }
}

path {
  animation-name: grow;
  animation-duration: 2500s;
  animation-timing-function: cubic-bezier( 0.75,0.25,1 );

  stroke-dasharray: 1000000;
  stroke-dashoffset: 1000000;  
  stroke: rgba( 0%,100%,75%,0.75 );
}

@keyframes grow {
  100% { stroke-dashoffset: 0; }
}
<svg></svg>

我已经清理了代码,添加了 MAX_DISTANCE 来检查:

let lastTwoPoints = [];

const W = document.documentElement.clientWidth;
const H = document.documentElement.clientHeight;

const MIN_ANGLE = 60;
const MIN_DISTANCE = (Math.min(W,H))/20;
const MAX_DISTANCE = (Math.min(W,H))/4;

let svg = document.querySelector('svg');
let path = document.querySelector('path');

svg.style.width = W;
svg.style.height = H;

createPath();

function getPoint(){
        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 = getPoint();
        } else {
            lastTwoPoints.shift();
            lastTwoPoints.push(point);
        }
    }
    return point;
}

function pointString(){
    let point = getPoint();
    return `${point[0]} ${point[1]} `;
}

function getDistance(pointA,pointA);
    return Math.acos((a*a + b*b - c*c)/(2*a*b))*(180/Math.PI);
}

function createPath() {

       let path_string = `M ${pointString()} C ${pointString()} ${pointString()} ${pointString()}`;

    for( let i = 0; i < 100; i++  ) {
      path_string += `S ${pointString()} ${pointString()} `
    }
    
    path.setAttribute('d',path_string);
}


function getRandomNumber(max) { return Math.floor( Math.random() * max ); }
<svg fill="none" stroke="black">
    <path d=""/>
</svg>