146 votes

comment dessiner une courbe lisse à travers N points en utilisant javascript HTML5 canvas ?

Pour une application de dessin, j'enregistre les coordonnées du mouvement de la souris dans un tableau, puis je les dessine avec lineTo. La ligne qui en résulte n'est pas lisse. Comment puis-je produire une courbe unique entre tous les points rassemblés ?

J'ai fait des recherches sur Internet mais je n'ai trouvé que 3 fonctions pour dessiner des lignes : Pour 2 points d'échantillonnage, il suffit d'utiliser lineTo . Pour 3 points d'échantillonnage quadraticCurveTo pour 4 points d'échantillonnage, bezierCurveTo .

(J'ai essayé de dessiner un bezierCurveTo pour chaque 4 points du tableau, mais cela conduit à des coudes tous les 4 points de l'échantillon, au lieu d'une courbe continue et lisse).

Comment écrire une fonction pour dessiner une courbe lisse avec 5 points d'échantillonnage et au-delà ?

5 votes

Qu'entendez-vous par "lisse" ? Infiniment différentiable ? Deux fois différentiable ? Les splines cubiques ("courbes de Bézier") ont de nombreuses propriétés intéressantes et sont deux fois différentiables, et assez faciles à calculer.

8 votes

@Kerrek SB, par "lisse", je veux dire qu'à l'œil nu, on ne peut pas détecter de coins ou de bosses, etc.

0 votes

@sketchfemme, faites-vous le rendu des lignes en temps réel, ou retardez-vous le rendu jusqu'à ce que vous ayez collecté un certain nombre de points ?

138voto

sketchfemme Points 3377

Le problème de la jonction de points d'échantillonnage successifs avec des fonctions de type "curveTo" disjointes est que l'endroit où les courbes se rencontrent n'est pas lisse. Cela est dû au fait que les deux courbes partagent un point final mais sont influencées par des points de contrôle complètement disjoints. Une solution consiste à "courber vers" les points médians entre les deux points d'échantillonnage suivants. La jonction des courbes à l'aide de ces nouveaux points interpolés donne une transition lisse aux points d'extrémité (ce qui est un point d'extrémité pour une itération devient un point d'extrémité pour une autre itération). point de contrôle pour l'itération suivante). En d'autres termes, les deux courbes disjointes ont maintenant beaucoup plus en commun.

Cette solution a été extraite du livre "Foundation ActionScript 3.0 Animation : Making things move". p.95 - techniques de rendu : création de courbes multiples.

Note : cette solution ne dessine pas réellement à travers chacun des points, ce qui était le titre de ma question (elle approxime plutôt la courbe à travers les points d'échantillonnage mais ne passe jamais par les points d'échantillonnage), mais pour mes besoins (une application de dessin), c'est suffisant pour moi et visuellement vous ne pouvez pas faire la différence. Voici es une solution permettant de passer par tous les points d'échantillonnage, mais elle est beaucoup plus compliquée (cf. http://www.cartogrammar.com/blog/actionscript-curves-update/ )

Voici le code de dessin pour la méthode d'approximation :

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

   for (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);

0 votes

+1 Cela a bien fonctionné pour un projet JavaScript/canvas sur lequel je travaille.

1 votes

Heureux d'avoir pu vous aider. Pour votre information, j'ai lancé un bloc de dessin html5 canvas open source qui est un plugin jQuery. Il devrait être un point de départ utile. github.com/homanchou/sketchyPad

4 votes

C'est bien, mais comment faire pour que la courbe passe par tous les points ?

119voto

Ken Fyrstenberg Points 38115

Un peu tard, mais pour mémoire.

Vous pouvez obtenir des lignes douces en utilisant splines cardinales (alias spline canonique) pour dessiner des courbes lisses qui passent par les points.

J'ai créé cette fonction pour canvas - elle est divisée en trois fonctions pour augmenter la versatilité. La fonction wrapper principale ressemble à ceci :

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

    showPoints  = showPoints ? showPoints : false;

    ctx.beginPath();

    drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));

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

Pour dessiner une courbe, il faut avoir un tableau avec des points x, y dans l'ordre : x1,y1, x2,y2, ...xn,yn .

Utilisez-le comme ça :

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);

La fonction ci-dessus appelle deux sous-fonctions, une pour calculer les points lissés. Elle renvoie un tableau avec de nouveaux points - c'est la fonction principale qui calcule les points lissés :

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;
}

Et pour dessiner réellement les points sous forme de courbe lissée (ou toute autre ligne segmentée tant que vous avez un tableau 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>

Il en résulte ceci :

Example pix

Vous pouvez facilement étendre le canevas pour l'appeler comme ceci à la place :

ctx.drawCurve(myPoints);

Ajoutez ce qui suit au javascript :

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

Vous pouvez trouver une version plus optimisée de ceci sur NPM ( npm i cardinal-spline-js ) ou sur GitLab .

5 votes

Tout d'abord : C'est magnifique. :-) Mais en regardant cette image, ne donne-t-elle pas l'impression (trompeuse) que les valeurs sont en fait passées sous la valeur #10 en chemin entre #9 et #10 ? (Je compte à partir des points que je peux voir, donc le #1 serait celui qui se trouve en haut de la trajectoire descendante initiale, le #2 celui qui se trouve tout en bas [point le plus bas du graphique], et ainsi de suite...)

0 votes

@T.J.Crowder c'est le comportement correct car la courbe est interpolée. Vous pouvez régler cela en ajustant la valeur de la tension. La tension est affectée par ambos les points précédent et suivant et, en raison de l'angle raide vers le haut pour le point suivant, le point précédent est obligé de s'arrondir plus tôt. C'est ainsi que fonctionne une spline cardinale :-)

0 votes

Merci. Mais encore une fois, le graphique ne devrait-il pas éviter d'être trompeur ? Peut-être en faisant en sorte que la spline cible un point légèrement plus haut sur la courbe ascendante, de sorte que le point le plus bas soit celui qui est représenté sur le graphique ? Je vois des profanes (comme moi, lorsqu'il s'agit de graphiques complexes) interpréter cela très, très facilement.

30voto

Daniel Howard Points 579

Ce type décrit une façon de dessiner des courbes lisses qui passent réellement par un ensemble de N points, ce qui était exactement ce que la question originale voulait, et ce dont j'avais besoin pour mon projet :

http://scaledinnovation.com/analytics/splines/aboutSplines.html

12voto

dpatru Points 353

Comme Daniel Howard souligne Rob Spencer décrit ce que vous voulez à http://scaledinnovation.com/analytics/splines/aboutSplines.html .

Voici une démo interactive : http://jsbin.com/ApitIxo/2/

Le voici sous forme d'extrait au cas où jsbin serait en panne.

<!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>

6voto

Eric Rowell Points 3940

Essayez KineticJS - vous pouvez définir une Spline avec un tableau de points. Voici un exemple :

Ancienne url : http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

Voir l'url de l'archive : https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

0 votes

Une librairie étonnante ! La meilleure pour cette tâche !

0 votes

Oui ! J'avais besoin de la fonction blob() pour créer une forme fermée qui passe par tous les points.

7 votes

404. Page non trouvée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X