programing

자바스크립트 HTML5 캔버스를 이용하여 N개의 포인트를 통해 매끄러운 곡선을 그리는 방법?

css3 2023. 11. 1. 22:33

자바스크립트 HTML5 캔버스를 이용하여 N개의 포인트를 통해 매끄러운 곡선을 그리는 방법?

그리기 응용프로그램의 경우 마우스 이동 좌표를 배열에 저장한 다음 선으로 그립니다.결과 선이 매끄럽지 않습니다.수집된 모든 점 사이에서 단일 곡선을 생성하려면 어떻게 해야 합니까?

구글 검색을 해봤지만 선 긋기 기능은 3가지 밖에 없습니다.2개의 샘플 포인트의 경우, 간단히 사용할 수 있습니다.lineTo. 3개의 샘플 포인트의 경우quadraticCurveTo, 4개의 샘플 포인트에 대해,bezierCurveTo.

(나는 그림을 그리려고 했습니다.bezierCurveTo배열의 모든 4개의 점에 대해 연속적인 매끄러운 곡선 대신 4개의 샘플 점마다 꼬임이 발생합니다.)

샘플 포인트 5개 이상으로 매끄러운 곡선을 그리는 함수를 작성하려면 어떻게 해야 합니까?

이후의 표본 점을 분리된 "곡선 To" 유형의 함수와 결합할 때의 문제는 곡선이 만나는 곳이 매끄럽지 않다는 것입니다.이는 두 곡선이 끝점을 공유하지만 완전히 분리된 관리점의 영향을 받기 때문입니다.한 가지 해결책은 다음 두 표본점 사이의 중간점을 "곡선"으로 만드는 것입니다.이 새 보간 점을 사용하여 곡선을 결합하면 끝점에서 매끄러운 전환이 이루어집니다(한 반복의 끝점이 되는 것은 다음 반복의 제어점이 됩니다).즉, 지금은 서로 연결된 두 곡선이 훨씬 더 많은 공통점을 가지고 있습니다.

이 솔루션은 "Foundation ActionScript 3.0 Animation:"이라는 책에서 발췌되었습니다.사물을 움직이게 한다", p.95 - 렌더링 기법: 여러 곡선을 만듭니다.

참고: 이 솔루션은 제 질문의 제목인 각 점을 실제로 그리는 것이 아니라(오히려 표본점을 통해 곡선을 근사화하지만 표본점을 통과하지 않음), 제 목적(그림 그리기 어플리케이션)에서는 충분합니다.샘플 포인트를 모두 조사할 수 있는 솔루션이 있지만 훨씬 복잡합니다(http://www.cartogrammar.com/blog/actionscript-curves-update/) 참조).

근사 방법의 도면 코드는 다음과 같습니다.

// move to the first point
   ctx.moveTo(points[0].x, points[0].y);


   for (var i = 1; i < points.length - 2; i++)
   {
      var xc = (points[i].x + points[i + 1].x) / 2;
      var yc = (points[i].y + points[i + 1].y) / 2;
      ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
   }
 // curve through the last two points
 ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);

실행 가능한 토막글:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const points = [
  {x: 50, y: 50},
  {x: 180, y: 100},
  {x: 75, y: 120},
  {x: 40, y: 40},
];

// move to the first point
ctx.moveTo(points[0].x, points[0].y);

for (var i = 1; i < points.length - 2; i++) {
  var xc = (points[i].x + points[i + 1].x) / 2;
  var yc = (points[i].y + points[i + 1].y) / 2;
  ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}

// curve through the last two points
ctx.quadraticCurveTo(
  points[i].x,
  points[i].y,
  points[i + 1].x,
  points[i + 1].y
);
ctx.stroke();
<canvas width="600" height="600"></canvas>

조금 늦었지만, 참고로.

기본 스플라인(정규 스플라인이라고도 함)을 사용하여 점을 통과하는 부드러운 곡선을 그리면을 얻을 수 있습니다.

캔버스용으로 만든 기능입니다. 다용도성을 높이기 위해 3가지 기능으로 나누어져 있습니다.메인 래퍼 기능은 다음과 같습니다.

function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {

  ctx.beginPath();

  drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
  
  if (showPoints) {
    ctx.beginPath();
    for(var i=0;i<ptsa.length-1;i+=2) 
      ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
  }

  ctx.stroke();
}

곡선을 그리려면 x, y 점을 순서대로 배열합니다.x1,y1, x2,y2, ...xn,yn.

다음과 같이 사용합니다.

var myPoints = [10,10, 40,30, 100,10]; //minimum two points
var tension = 1;

drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);

위의 함수는 두 개의 하위 함수를 호출하며, 하나는 평활점을 계산합니다.새 점이 있는 배열을 반환합니다. 매끄러운 점을 계산하는 핵심 함수입니다.

function getCurvePoints(pts, tension, isClosed, numOfSegments) {

    // use input value if provided, or use a default value   
    tension = (typeof tension != 'undefined') ? tension : 0.5;
    isClosed = isClosed ? isClosed : false;
    numOfSegments = numOfSegments ? numOfSegments : 16;

    var _pts = [], res = [],    // clone array
        x, y,           // our x,y coords
        t1x, t2x, t1y, t2y, // tension vectors
        c1, c2, c3, c4,     // cardinal points
        st, t, i;       // steps based on num. of segments

    // clone array so we don't change the original
    //
    _pts = pts.slice(0);

    // The algorithm require a previous and next point to the actual point array.
    // Check if we will draw closed or open curve.
    // If closed, copy end points to beginning and first points to end
    // If open, duplicate first points to befinning, end points to end
    if (isClosed) {
        _pts.unshift(pts[pts.length - 1]);
        _pts.unshift(pts[pts.length - 2]);
        _pts.unshift(pts[pts.length - 1]);
        _pts.unshift(pts[pts.length - 2]);
        _pts.push(pts[0]);
        _pts.push(pts[1]);
    }
    else {
        _pts.unshift(pts[1]);   //copy 1. point and insert at beginning
        _pts.unshift(pts[0]);
        _pts.push(pts[pts.length - 2]); //copy last point and append
        _pts.push(pts[pts.length - 1]);
    }

    // ok, lets start..
    
    // 1. loop goes through point array
    // 2. loop goes through each segment between the 2 pts + 1e point before and after
    for (i=2; i < (_pts.length - 4); i+=2) {
        for (t=0; t <= numOfSegments; t++) {

            // calc tension vectors
            t1x = (_pts[i+2] - _pts[i-2]) * tension;
            t2x = (_pts[i+4] - _pts[i]) * tension;
    
            t1y = (_pts[i+3] - _pts[i-1]) * tension;
            t2y = (_pts[i+5] - _pts[i+1]) * tension;

            // calc step
            st = t / numOfSegments;
        
            // calc cardinals
            c1 =   2 * Math.pow(st, 3)  - 3 * Math.pow(st, 2) + 1; 
            c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); 
            c3 =       Math.pow(st, 3)  - 2 * Math.pow(st, 2) + st; 
            c4 =       Math.pow(st, 3)  -     Math.pow(st, 2);

            // calc x and y cords with common control vectors
            x = c1 * _pts[i]    + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
            y = c1 * _pts[i+1]  + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;
        
            //store points in array
            res.push(x);
            res.push(y);

        }
    }
    
    return res;
}

점을 매끄러운 곡선(또는 x,y 배열이 있는 경우 다른 분할된 선)으로 실제로 그리려면 다음과 같이 하십시오.

function drawLines(ctx, pts) {
    ctx.moveTo(pts[0], pts[1]);
    for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}

var ctx = document.getElementById("c").getContext("2d");


function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {

  ctx.beginPath();

  drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
  
  if (showPoints) {
    ctx.beginPath();
    for(var i=0;i<ptsa.length-1;i+=2) 
      ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
  }

  ctx.stroke();
}


var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points
var tension = 1;

drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);


function getCurvePoints(pts, tension, isClosed, numOfSegments) {

  // use input value if provided, or use a default value     
  tension = (typeof tension != 'undefined') ? tension : 0.5;
  isClosed = isClosed ? isClosed : false;
  numOfSegments = numOfSegments ? numOfSegments : 16;

  var _pts = [], res = [],  // clone array
      x, y,         // our x,y coords
      t1x, t2x, t1y, t2y,   // tension vectors
      c1, c2, c3, c4,       // cardinal points
      st, t, i;     // steps based on num. of segments

  // clone array so we don't change the original
  //
  _pts = pts.slice(0);

  // The algorithm require a previous and next point to the actual point array.
  // Check if we will draw closed or open curve.
  // If closed, copy end points to beginning and first points to end
  // If open, duplicate first points to befinning, end points to end
  if (isClosed) {
    _pts.unshift(pts[pts.length - 1]);
    _pts.unshift(pts[pts.length - 2]);
    _pts.unshift(pts[pts.length - 1]);
    _pts.unshift(pts[pts.length - 2]);
    _pts.push(pts[0]);
    _pts.push(pts[1]);
  }
  else {
    _pts.unshift(pts[1]);   //copy 1. point and insert at beginning
    _pts.unshift(pts[0]);
    _pts.push(pts[pts.length - 2]); //copy last point and append
    _pts.push(pts[pts.length - 1]);
  }

  // ok, lets start..

  // 1. loop goes through point array
  // 2. loop goes through each segment between the 2 pts + 1e point before and after
  for (i=2; i < (_pts.length - 4); i+=2) {
    for (t=0; t <= numOfSegments; t++) {

      // calc tension vectors
      t1x = (_pts[i+2] - _pts[i-2]) * tension;
      t2x = (_pts[i+4] - _pts[i]) * tension;

      t1y = (_pts[i+3] - _pts[i-1]) * tension;
      t2y = (_pts[i+5] - _pts[i+1]) * tension;

      // calc step
      st = t / numOfSegments;

      // calc cardinals
      c1 =   2 * Math.pow(st, 3)    - 3 * Math.pow(st, 2) + 1; 
      c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); 
      c3 =     Math.pow(st, 3)  - 2 * Math.pow(st, 2) + st; 
      c4 =     Math.pow(st, 3)  -     Math.pow(st, 2);

      // calc x and y cords with common control vectors
      x = c1 * _pts[i]  + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
      y = c1 * _pts[i+1]    + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;

      //store points in array
      res.push(x);
      res.push(y);

    }
  }

  return res;
}

function drawLines(ctx, pts) {
  ctx.moveTo(pts[0], pts[1]);
  for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
canvas { border: 1px solid red; }
<canvas id="c"><canvas>

결과는 다음과 같습니다.

Example pix

캔버스를 쉽게 확장할 수 있으므로 대신 이렇게 부를 수 있습니다.

ctx.drawCurve(myPoints);

다음을 javascript에 추가합니다.

if (CanvasRenderingContext2D != 'undefined') {
    CanvasRenderingContext2D.prototype.drawCurve = 
        function(pts, tension, isClosed, numOfSegments, showPoints) {
       drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}
}

이에 대한 보다 최적화된 버전은 NPM에서 확인하실 수 있습니다 (npm i cardinal-spline-js또는 GitLab에서.

첫 번째 답은 모든 점을 통과하지 못할 것입니다.이 그래프는 모든 점을 정확하게 통과하며 점이 [x:,y:}] n개인 완벽한 곡선이 됩니다.

var points = [{x:1,y:1},{x:2,y:3},{x:3,y:4},{x:4,y:2},{x:5,y:6}] //took 5 example points
ctx.moveTo((points[0].x), points[0].y);

for(var i = 0; i < points.length-1; i ++)
{

  var x_mid = (points[i].x + points[i+1].x) / 2;
  var y_mid = (points[i].y + points[i+1].y) / 2;
  var cp_x1 = (x_mid + points[i].x) / 2;
  var cp_x2 = (x_mid + points[i+1].x) / 2;
  ctx.quadraticCurveTo(cp_x1,points[i].y ,x_mid, y_mid);
  ctx.quadraticCurveTo(cp_x2,points[i+1].y ,points[i+1].x,points[i+1].y);
}

다른 게시물에 제 솔루션을 게시하는 것보다 추가하기로 결정했습니다.아래는 제가 구축한 솔루션이고, 완벽하지 않을 수도 있지만, 현재까지는 생산량이 양호합니다.

중요: 모든 지점을 통과할 것입니다.

더 좋은 생각이 있으면 공유해 주세요.감사해요.

다음은 이전과 이후의 비교입니다.

enter image description here

이 코드를 HTML에 저장하여 테스트합니다.

    <!DOCTYPE html>
    <html>
    <body>
    	<canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas>
    	<script>
    		var cv = document.getElementById("myCanvas");
    		var ctx = cv.getContext("2d");
    
    		function gradient(a, b) {
    			return (b.y-a.y)/(b.x-a.x);
    		}
    
    		function bzCurve(points, f, t) {
    			//f = 0, will be straight line
    			//t suppose to be 1, but changing the value can control the smoothness too
    			if (typeof(f) == 'undefined') f = 0.3;
    			if (typeof(t) == 'undefined') t = 0.6;
    
    			ctx.beginPath();
    			ctx.moveTo(points[0].x, points[0].y);
    
    			var m = 0;
    			var dx1 = 0;
    			var dy1 = 0;
    
    			var preP = points[0];
    			for (var i = 1; i < points.length; i++) {
    				var curP = points[i];
    				nexP = points[i + 1];
    				if (nexP) {
    					m = gradient(preP, nexP);
    					dx2 = (nexP.x - curP.x) * -f;
    					dy2 = dx2 * m * t;
    				} else {
    					dx2 = 0;
    					dy2 = 0;
    				}
    				ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y);
    				dx1 = dx2;
    				dy1 = dy2;
    				preP = curP;
    			}
    			ctx.stroke();
    		}
    
    		// Generate random data
    		var lines = [];
    		var X = 10;
    		var t = 40; //to control width of X
    		for (var i = 0; i < 100; i++ ) {
    			Y = Math.floor((Math.random() * 300) + 50);
    			p = { x: X, y: Y };
    			lines.push(p);
    			X = X + t;
    		}
    
    		//draw straight line
    		ctx.beginPath();
    		ctx.setLineDash([5]);
    		ctx.lineWidth = 1;
    		bzCurve(lines, 0, 1);
    
    		//draw smooth line
    		ctx.setLineDash([0]);
    		ctx.lineWidth = 2;
    		ctx.strokeStyle = "blue";
    		bzCurve(lines, 0.3, 1);
    	</script>
    </body>
    </html>

Daniel Howard가 지적한 바와 같이, Rob Spencer는 http://scaledinnovation.com/analytics/splines/aboutSplines.html 에서 여러분이 원하는 것을 설명합니다.

대화형 데모는 다음과 같습니다. http://jsbin.com/ApitIxo/2/

jsbin이 다운된 경우를 대비하여 토막글로 제공합니다.

<!DOCTYPE html>
    <html>
      <head>
        <meta charset=utf-8 />
        <title>Demo smooth connection</title>
      </head>
      <body>
        <div id="display">
          Click to build a smooth path. 
          (See Rob Spencer's <a href="http://scaledinnovation.com/analytics/splines/aboutSplines.html">article</a>)
          <br><label><input type="checkbox" id="showPoints" checked> Show points</label>
          <br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label>
          <br>
          <label>
            <input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span>
          </label>
        <div id="mouse"></div>
        </div>
        <canvas id="canvas"></canvas>
        <style>
          html { position: relative; height: 100%; width: 100%; }
          body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; } 
          canvas { outline: 1px solid red; }
          #display { position: fixed; margin: 8px; background: white; z-index: 1; }
        </style>
        <script>
          function update() {
            $("tensionvalue").innerHTML="("+$("tension").value+")";
            drawSplines();
          }
          $("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update;
      
          // utility function
          function $(id){ return document.getElementById(id); }
          var canvas=$("canvas"), ctx=canvas.getContext("2d");

          function setCanvasSize() {
            canvas.width = parseInt(window.getComputedStyle(document.body).width);
            canvas.height = parseInt(window.getComputedStyle(document.body).height);
          }
          window.onload = window.onresize = setCanvasSize();
      
          function mousePositionOnCanvas(e) {
            var el=e.target, c=el;
            var scaleX = c.width/c.offsetWidth || 1;
            var scaleY = c.height/c.offsetHeight || 1;
          
            if (!isNaN(e.offsetX)) 
              return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };
          
            var x=e.pageX, y=e.pageY;
            do {
              x -= el.offsetLeft;
              y -= el.offsetTop;
              el = el.offsetParent;
            } while (el);
            return { x: x*scaleX, y: y*scaleY };
          }
      
          canvas.onclick = function(e){
            var p = mousePositionOnCanvas(e);
            addSplinePoint(p.x, p.y);
          };
      
          function drawPoint(x,y,color){
            ctx.save();
            ctx.fillStyle=color;
            ctx.beginPath();
            ctx.arc(x,y,3,0,2*Math.PI);
            ctx.fill()
            ctx.restore();
          }
          canvas.onmousemove = function(e) {
            var p = mousePositionOnCanvas(e);
            $("mouse").innerHTML = p.x+","+p.y;
          };
      
          var pts=[]; // a list of x and ys

          // given an array of x,y's, return distance between any two,
          // note that i and j are indexes to the points, not directly into the array.
          function dista(arr, i, j) {
            return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2));
          }

          // return vector from i to j where i and j are indexes pointing into an array of points.
          function va(arr, i, j){
            return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]]
          }
      
          function ctlpts(x1,y1,x2,y2,x3,y3) {
            var t = $("tension").value;
            var v = va(arguments, 0, 2);
            var d01 = dista(arguments, 0, 1);
            var d12 = dista(arguments, 1, 2);
            var d012 = d01 + d12;
            return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012,
                    x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ];
          }

          function addSplinePoint(x, y){
            pts.push(x); pts.push(y);
            drawSplines();
          }
          function drawSplines() {
            clear();
            cps = []; // There will be two control points for each "middle" point, 1 ... len-2e
            for (var i = 0; i < pts.length - 2; i += 1) {
              cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1], 
                                      pts[2*i+2], pts[2*i+3], 
                                      pts[2*i+4], pts[2*i+5]));
            }
            if ($("showControlLines").checked) drawControlPoints(cps);
            if ($("showPoints").checked) drawPoints(pts);
    
            drawCurvedPath(cps, pts);
 
          }
          function drawControlPoints(cps) {
            for (var i = 0; i < cps.length; i += 4) {
              showPt(cps[i], cps[i+1], "pink");
              showPt(cps[i+2], cps[i+3], "pink");
              drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink");
            } 
          }
      
          function drawPoints(pts) {
            for (var i = 0; i < pts.length; i += 2) {
              showPt(pts[i], pts[i+1], "black");
            } 
          }
      
          function drawCurvedPath(cps, pts){
            var len = pts.length / 2; // number of points
            if (len < 2) return;
            if (len == 2) {
              ctx.beginPath();
              ctx.moveTo(pts[0], pts[1]);
              ctx.lineTo(pts[2], pts[3]);
              ctx.stroke();
            }
            else {
              ctx.beginPath();
              ctx.moveTo(pts[0], pts[1]);
              // from point 0 to point 1 is a quadratic
              ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);
              // for all middle points, connect with bezier
              for (var i = 2; i < len-1; i += 1) {
                // console.log("to", pts[2*i], pts[2*i+1]);
                ctx.bezierCurveTo(
                  cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
                  cps[(2*(i-1))*2], cps[(2*(i-1))*2+1],
                  pts[i*2], pts[i*2+1]);
              }
              ctx.quadraticCurveTo(
                cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
                pts[i*2], pts[i*2+1]);
              ctx.stroke();
            }
          }
          function clear() {
            ctx.save();
            // use alpha to fade out
            ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen
            ctx.fillRect(0,0,canvas.width,canvas.height);
            ctx.restore();
          }
      
          function showPt(x,y,fillStyle) {
            ctx.save();
            ctx.beginPath();
            if (fillStyle) {
              ctx.fillStyle = fillStyle;
            }
            ctx.arc(x, y, 5, 0, 2*Math.PI);
            ctx.fill();
            ctx.restore();
          }

          function drawLine(x1, y1, x2, y2, strokeStyle){
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            if (strokeStyle) {
              ctx.save();
              ctx.strokeStyle = strokeStyle;
              ctx.stroke();
              ctx.restore();
            }
            else {
              ctx.save();
              ctx.strokeStyle = "pink";
              ctx.stroke();
              ctx.restore();
            }
          }

        </script>


      </body>
    </html>

잘 작동한다는 걸 알았습니다.

function drawCurve(points, tension) {
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);

    var t = (tension != null) ? tension : 1;
    for (var i = 0; i < points.length - 1; i++) {
        var p0 = (i > 0) ? points[i - 1] : points[0];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = (i != points.length - 2) ? points[i + 2] : p2;

        var cp1x = p1.x + (p2.x - p0.x) / 6 * t;
        var cp1y = p1.y + (p2.y - p0.y) / 6 * t;

        var cp2x = p2.x - (p3.x - p1.x) / 6 * t;
        var cp2y = p2.y - (p3.y - p1.y) / 6 * t;

        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
    }
    ctx.stroke();
}

KineticJ를 시도해 보십시오. 점 배열로 스플라인을 정의할 수 있습니다.예는 다음과 같습니다.

이전 URL: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

아카이브 URL 참조: www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/ ://https://web.archive.org/web/20141204030628/http

봉쥬르

나는 user1693593의 해결책에 감사합니다: 헤르마이트 다항식은 그려질 것을 제어하는 가장 좋은 방법인 것 같고, 수학적인 관점에서 가장 만족스럽습니다.그 과목은 오랫동안 닫혀있는 것처럼 보이지만, 저 같은 후발주자들은 여전히 관심이 있을 수도 있습니다.곡선을 저장하고 다른 곳에서 다시 사용할 수 있는 무료 대화형 플롯 작성기를 찾아봤지만, 웹에서 이런 것을 찾지 못했습니다: 그래서 사용자 1693593에서 언급한 위키백과 소스에서 제가 직접 만들었습니다.여기서는 어떻게 작동하는지 설명하기가 어려우며, 가치가 있는지 알 수 있는 가장 좋은 방법은 https://sites.google.com/view/divertissements/accueil/splines 을 보는 것입니다.

믿을 수 없을 정도로 늦었지만 호만의 훌륭하고 간단한 대답에 영감을 받아 좀 더 일반적인 해결책(호만의 해결책이 3개 미만의 정점을 가진 점 배열에서 충돌한다는 의미에서 일반적)을 게시할 수 있습니다.

function smooth(ctx, points)
{
    if(points == undefined || points.length == 0)
    {
        return true;
    }
    if(points.length == 1)
    {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[0].x, points[0].y);
        return true;
    }
    if(points.length == 2)
    {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[1].x, points[1].y);
        return true;
    }
    ctx.moveTo(points[0].x, points[0].y);
    for (var i = 1; i < points.length - 2; i ++)
    {
        var xc = (points[i].x + points[i + 1].x) / 2;
        var yc = (points[i].y + points[i + 1].y) / 2;
        ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
    }
    ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}

이 코드는 제게 딱 맞습니다.

this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
  this.context.bezierCurveTo(
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i - 1].y,
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i].y,
    data[i].x,
    data[i].y);
}

올바른 매끄러운 선과 올바른 끝점 NOTEST! (y = "canvas 높이" - y);

원래 질문에 대한 약간 다른 답변.

도형을 그리려는 사람이 있는 경우:

  • 그것은 일련의 점들로 설명됩니다.
  • 선이 점들에 작은 곡선을 가지고 있는 곳
  • 선은 반드시 점들을 통과할 필요는 없습니다(즉, 점들 중 약간 "inside"를 통과합니다).

그렇다면 아래의 제 기능이 도움이 되기를 바랍니다.

<!DOCTYPE html>
<html>

<body>
<canvas id="myCanvas" width="1200" height="700" style="border: 1px solid #d3d3d3">Your browser does not support the
    HTML5 canvas tag.</canvas>
<script>
    var cv = document.getElementById("myCanvas");
    var ctx = cv.getContext("2d");

    const drawPointsWithCurvedCorners = (points, ctx) => {
        for (let n = 0; n <= points.length - 1; n++) {
            let pointA = points[n];
            let pointB = points[(n + 1) % points.length];
            let pointC = points[(n + 2) % points.length];

            const midPointAB = {
                x: pointA.x + (pointB.x - pointA.x) / 2,
                y: pointA.y + (pointB.y - pointA.y) / 2,
            };
            const midPointBC = {
                x: pointB.x + (pointC.x - pointB.x) / 2,
                y: pointB.y + (pointC.y - pointB.y) / 2,
            };
            ctx.moveTo(midPointAB.x, midPointAB.y);
            ctx.arcTo(
                pointB.x,
                pointB.y,
                midPointBC.x,
                midPointBC.y,
                radii[pointB.r]
            );
            ctx.lineTo(midPointBC.x, midPointBC.y);
        }
    };

    const shapeWidth = 200;
    const shapeHeight = 150;

    const topInsetDepth = 35;
    const topInsetSideWidth = 20;
    const topInsetHorizOffset = shapeWidth * 0.25;

    const radii = {
        small: 15,
        large: 30,
    };

    const points = [
        {
            // TOP-LEFT
            x: 0,
            y: 0,
            r: "large",
        },
        {
            x: topInsetHorizOffset,
            y: 0,
            r: "small",
        },
        {
            x: topInsetHorizOffset + topInsetSideWidth,
            y: topInsetDepth,
            r: "small",
        },
        {
            x: shapeWidth - (topInsetHorizOffset + topInsetSideWidth),
            y: topInsetDepth,
            r: "small",
        },
        {
            x: shapeWidth - topInsetHorizOffset,
            y: 0,
            r: "small",
        },
        {
            // TOP-RIGHT
            x: shapeWidth,
            y: 0,
            r: "large",
        },
        {
            // BOTTOM-RIGHT
            x: shapeWidth,
            y: shapeHeight,
            r: "large",
        },
        {
            // BOTTOM-LEFT
            x: 0,
            y: shapeHeight,
            r: "large",
        },
    ];

    // ACTUAL DRAWING OF POINTS
    ctx.beginPath();
    drawPointsWithCurvedCorners(points, ctx);
    ctx.stroke();
</script>
</body>

</html>

K3N의 기본 스플라인 방식에 추가하고 오해의 소지가 있는 곳에서 곡선 '디핑'에 대한 T. J. Crowder의 우려를 해결하기 위해 다음 코드를 삽입했습니다.getCurvePoints()functionres.push(x);

if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) {
    y = (_pts[i+1] + _pts[i+3]) / 2;
}
if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) {
    x = (_pts[i] + _pts[i+2]) / 2;
}

이렇게 하면 각 연속적인 점 쌍 사이에 (보이지 않는) 경계 상자가 효과적으로 생성되고 곡선이 이 경계 상자 내에 유지됩니다. 즉, 곡선의 한 점이 두 점의 위/아래/좌/우에 있으면 그 위치가 상자 내에 있도록 변경됩니다.여기서는 중간점을 사용하지만, 선형 보간을 사용하여 이를 개선할 수 있습니다.

만약 당신이 n개의 점을 통해 곡선의 방정식을 결정하고 싶다면, 다음 코드는 n-1차 다항식의 계수들을 당신에게 주고 이 계수들을 다음에 저장할 것입니다.coefficients[]배열(상수 항에서 시작).x 좌표가 순서대로 있을 필요는 없습니다.이것은 라그랑주 다항식의 예입니다.

var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
    for (var m=0; m<xPoints.length; m++) {
        var newCoefficients=[];
        for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
        if (m>0) {
            newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
            newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
    } else {
        newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
        newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
    }
    var startIndex=1; 
    if (m==0) startIndex=2; 
    for (var n=startIndex; n<xPoints.length; n++) {
        if (m==n) continue;
        for (var nc=xPoints.length-1; nc>=1; nc--) {
        newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
        }
        newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
    }    
    for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}

저는 어떻게든 2차 베젤만을 사용하는 방법이 필요합니다.이 방법은 제 방법이며 3D로 확장할 수 있습니다.

쿼드베지어 곡선의 공식은

b(t) = (1-t)^2A + 2(1-t)tB + t^2*C

t = 0 또는 1일 때 곡선은 점 A 또는 C를 통과할 수 있지만 B를 통과하는 것은 보장되지 않습니다.

그것의 1계 파생상품은

b'(t) = 2(t-1)A + 2(1-2t)B + 2tC

두 개의 쿼드 베저 곡선으로 P0,P1,P2 점을 통과하는 곡선을 구성하려면 p1에서 두 베저 곡선의 기울기가 같아야 합니다.

b'α(t) = 2(t-1)P0 + 2(1-2t)M1 + 2tP1

b'β(t) = 2(t-1)P1 + 2(1-2t)M2 + 2tP2

b'α(1) = b'β(0)

이것은.

(M1 + M2) / 2 = P1

그래서 이렇게 3점까지 곡선을 그릴 수 있습니다.

bezier(p0, m1, p1);
bezier(p1, m2, p2);

어디에m1p1 = p1m2. 방향은.m1m2중요하지 않습니다, 에 의해 발견할 수 있습니다.p2 - p1.

4개 이상의 점을 통과하는 곡선의 경우

bezier(p0, m1, p1);
bezier(p1, m2, (m2 + m3) / 2);
bezier((m2 + m3) / 2, m3, p2);
bezier(p2, m4, p3);

어디에m1p1 = p1m2그리고.m3p2 = p2m4.

function drawCurve(ctx: CanvasRenderingContext2D, points: { x: number, y: number }[], tension = 2) {
    if (points.length < 2) {
        return;
    }
    ctx.beginPath();
    if (points.length === 2) {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[1].x, points[1].y);
        ctx.stroke();
        return;
    }
    let prevM2x = 0;
    let prevM2y = 0;
    for (let i = 1, len = points.length; i < len - 1; ++i) {
        const p0 = points[i - 1];
        const p1 = points[i];
        const p2 = points[i + 1];
        let tx = p2.x - (i === 1 ? p0.x : prevM2x);
        let ty = p2.y - (i === 1 ? p0.y : prevM2y);
        const tLen = Math.sqrt(tx ** 2 + ty ** 2);
        if (tLen > 1e-8) {
            const inv = 1 / tLen;
            tx *= inv;
            ty *= inv;
        } else {
            tx = 0;
            ty = 0;
        }
        const det = Math.sqrt(Math.min(
            (p0.x - p1.x) ** 2 + (p0.y - p1.y) ** 2,
            (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
        )) / (2 * tension);
        const m1x = p1.x - tx * det;
        const m1y = p1.y - ty * det;
        const m2x = p1.x + tx * det;
        const m2y = p1.y + ty * det;
        if (i === 1) {
            ctx.moveTo(p0.x, p0.y);
            ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
        } else {
            const mx = (prevM2x + m1x) / 2;
            const my = (prevM2y + m1y) / 2;
            ctx.quadraticCurveTo(prevM2x, prevM2y, mx, my);
            ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
        }
        if (i === len - 2) {
            ctx.quadraticCurveTo(m2x, m2y, p2.x, p2.y);
        }
        prevM2x = m2x;
        prevM2y = m2y;
    }
    ctx.stroke();
}

언급URL : https://stackoverflow.com/questions/7054272/how-to-draw-smooth-curve-through-n-points-using-javascript-html5-canvas