Continuous gradient along a HTML5 canvas path

前端 未结 2 396
北海茫月
北海茫月 2020-12-19 19:01

I am trying to draw a continous gradient along a path of points, where each point has a it\'s own color, using the HTML5 canvas API.

See http://bl.ocks.org/rveciana/

相关标签:
2条回答
  • 2020-12-19 19:16

    Here's a slight modification of your original idea that makes the joins blend nicely.

    enter image description here

    Original: Draw a gradient line from the start to end of a line segment.

    This causes the line joins to overlap and produces a noticeable & undesired transition.

    enter image description here

    Modification: Draw a gradient line that doesn't extend to the start / endpoints.

    With this modification, the line joins will always be solid colors rather than be partially gradiented. As a result, the line joins will transition nicely between line segments.

    enter image description here

    Here's example code and a Demo:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    
    var lines = [
      {x:100, y:050,color:'red'},
      {x:150, y:100,color:'green'},
      {x:200, y:050,color:'gold'},
      {x:275, y:150,color:'blue'}
    ];
    var linewidth=20;
    
    ctx.lineCap='round';
    ctx.lineJoint='round';
    
    for(var i=1;i<lines.length;i++){
    
      // calculate the smaller part of the line segment over
      //     which the gradient will run
      var p0=lines[i-1];
      var p1=lines[i];
      var dx=p1.x-p0.x;
      var dy=p1.y-p0.y;
      var angle=Math.atan2(dy,dx);
      var p0x=p0.x+linewidth*Math.cos(angle);
      var p0y=p0.y+linewidth*Math.sin(angle);
      var p1x=p1.x+linewidth*Math.cos(angle+Math.PI);
      var p1y=p1.y+linewidth*Math.sin(angle+Math.PI);
    
      // determine where the gradient starts and ends
      if(i==1){
        var g=ctx.createLinearGradient(p0.x,p0.y,p1x,p1y);   
      }else if(i==lines.length-1){
        var g=ctx.createLinearGradient(p0x,p0y,p1.x,p1.y);
      }else{
        var g=ctx.createLinearGradient(p0x,p0y,p1x,p1y);
      }
    
      // add the gradient color stops
      // and draw the gradient line from p0 to p1
      g.addColorStop(0,p0.color);
      g.addColorStop(1,p1.color);
      ctx.beginPath();
      ctx.moveTo(p0.x,p0.y);
      ctx.lineTo(p1.x,p1.y);
      ctx.strokeStyle=g;
      ctx.lineWidth=linewidth;
      ctx.stroke();
    }
    #canvas{border:1px solid red; margin:0 auto; }
    <canvas id="canvas" width=350 height=200></canvas>

    0 讨论(0)
  • 2020-12-19 19:21

    You can do a simple approach interpolating two colors along a line. If you need smooth/shared gradients where two lines joins at steeper angles, you would need to calculate and basically implement a line drawing algorithm from (almost) scratch. This would be out of scope for SO, so here is a simpler approach.

    That being said - the example in the link is not actually a line but several plots of squares of different colors. The issues it would have too is "hidden" by its subtle variations.

    Example

    snapshot

    This approach requires two main functions:

    1. Line interpolate function which draws each segment in a line from previous mouse position to current position

    2. Color interpolate function which takes an array of colors and interpolate between two current colors depending on length, position and segment size.

    Tweak parameters such as segment size, number of colors in the array etc. to get the optimal result.

    Line interpolate function

    function plotLine(ctx, x1, y1, x2, y2) {
    
      var diffX = Math.abs(x2 - x1),      // get line length
          diffY = Math.abs(y2 - y1),
          dist = Math.sqrt(diffX * diffX + diffY * diffY),
          step = dist / 10,               // define some resolution
          i = 0, t, b, x, y;
    
      while (i <= dist) {                 // render circles along the line
        t = Math.min(1, i / dist);
    
        x = x1 + (x2 - x1) * t;
        y = y1 + (y2 - y1) * t;
    
        ctx.fillStyle = getColor();       // get current color
        ctx.beginPath();
        ctx.arc(x, y, 10, 0, Math.PI*2);
        ctx.fill();
        i += step;
      }
    

    Color interpolate function

      function getColor() {
    
        var r, g, b, t, c1, c2;
    
        c1 = colors[cIndex];                           // get current color from array
        c2 = colors[(cIndex + 1) % maxColors];         // get next color
        t = Math.min(1, total / segment);              // calculate t
    
        if (++total > segment) {                       // rotate segment
          total = 0;
          if (++cIndex >= maxColors) cIndex = 0;       // rotate color array
        }
    
        r = c1.r + (c2.r - c1.r) * t;                  // interpolate color
        g = c1.g + (c2.g - c1.g) * t;
        b = c1.b + (c2.b - c1.b) * t;
    
        return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
      }
    

    Demo

    Putting it all together will allow you to draw gradient lines. If you don't want to draw them manually simply call the plotLine() function whenever needed.

    // Some setup code
    var c = document.querySelector("canvas"),
        ctx = c.getContext("2d"),
        colors = [
          {r: 255, g: 0, b: 0},
          {r: 255, g: 255, b: 0},
          {r: 0, g: 255, b: 0},
          {r: 0, g: 255, b: 255},
          {r: 0, g: 0, b: 255},
          {r: 255, g: 0, b: 255},
          {r: 0, g: 255, b: 255},
          {r: 0, g: 255, b: 0},
          {r: 255, g: 255, b: 0},
        ],
        cIndex = 0, maxColors = colors.length,
        total = 0, segment = 500,
        isDown = false, px, py;
    
    setSize();
          
    c.onmousedown = c.ontouchstart = function(e) {
      isDown = true;
      var pos = getPos(e);
      px = pos.x;
      py = pos.y;
    };
    
    window.onmousemove = window.ontouchmove = function(e) {if (isDown) plot(e)};
    window.onmouseup = window.ontouchend = function(e) {
      e.preventDefault();
      isDown = false
    };
    
    function getPos(e) {
      e.preventDefault();
      if (e.touches) e = e.touches[0];
      var r = c.getBoundingClientRect();
      return {
        x: e.clientX - r.left,
        y: e.clientY - r.top
      }
    }
    
    function plot(e) {
      var pos = getPos(e);
      plotLine(ctx, px, py, pos.x, pos.y);
      px = pos.x;
      py = pos.y;
    }
    
    function plotLine(ctx, x1, y1, x2, y2) {
    
      var diffX = Math.abs(x2 - x1),
          diffY = Math.abs(y2 - y1),
          dist = Math.sqrt(diffX * diffX + diffY * diffY),
          step = dist / 50,
          i = 0,
          t, b, x, y;
      
      while (i <= dist) {
        t = Math.min(1, i / dist);
    
        x = x1 + (x2 - x1) * t;
        y = y1 + (y2 - y1) * t;
    
        ctx.fillStyle = getColor();
        ctx.beginPath();
        ctx.arc(x, y, 10, 0, Math.PI*2);
        ctx.fill();
        i += step;
      }
      
      function getColor() {
      
        var r, g, b, t, c1, c2;
        
        c1 = colors[cIndex];
        c2 = colors[(cIndex + 1) % maxColors];
        t = Math.min(1, total / segment);
        
        if (++total > segment) {
          total = 0;
          if (++cIndex >= maxColors) cIndex = 0;
        }
      
        r = c1.r + (c2.r - c1.r) * t;
        g = c1.g + (c2.g - c1.g) * t;
        b = c1.b + (c2.b - c1.b) * t;
      
        return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
      }
    }
    
    window.onresize = setSize;
    function setSize() {
      c.width = window.innerWidth;
      c.height = window.innerHeight;
    }
    document.querySelector("button").onclick = function() {
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
    };
    html, body {background:#777; margin:0; overflow:hidden}
    canvas {position:fixed;left:0;top:0;background: #333}
    button {position:fixed;left:10px;top:10px}
    <canvas></canvas>
    <button>Clear</button>

    TIPS:

    • The gradient values can be pre-populated / cached beforehand
    • The step for position in gradient can be bound to length to get even spread independent of draw speed
    • You can easily replace the brush with other path/figures/shapes, even combine image based brushes which is composited with current color
    0 讨论(0)
提交回复
热议问题