Search code examples
javascriptcanvaspaperjs

Filling a shape procedurally with more than one color


I'm making an app with node,js,etc. I'd like to fill custom shapes I can create either via data points or another format with different, layered colors. For example, I have a triangle. I want to fill the bottom 1/3 with red, the middle 1/3 with blue, and the top 1/3 with green. How would I go about this?

I'm looking at Paper.js and the basic canvas, but they seem to only have single color fills.

Thanks for any advice!


Solution

  • I am aware of that an answer has been accepted, but I wanted to present a very simple approach for future readers. Which, as a bonus, automatically calculates the height of each part and is fast, using a linear gradient -

    Result will be

    snap

    Code and demo

    var ctx = document.querySelector("canvas").getContext("2d"),
        grad = ctx.createLinearGradient(0, 0, 0, 150);
    
    grad.addColorStop(0, "red");     // start of red
    grad.addColorStop(1/3, "red");   // end of red at 1/3
    
    grad.addColorStop(1/3, "gold");  // start of gold at 1/3
    grad.addColorStop(2/3, "gold");  // end of gold at 2/3
    
    grad.addColorStop(2/3, "blue");  // start of blue at 2/3
    grad.addColorStop(1, "blue");    // end of blue at 3/3
    
    // Fill a triangle:
    ctx.moveTo(75, 0); ctx.lineTo(150, 150); ctx.lineTo(0, 150);
    ctx.fillStyle = grad;
    ctx.fill();
    <canvas/>

    Animated version using compositing technique

    var ctx = document.querySelector("canvas").getContext("2d"),
        grad = ctx.createLinearGradient(0, 0, 0, 150),
        step = grad.addColorStop.bind(grad), // function reference to simplify
        dlt = -3, y = 150;
    
    step(0, "red");     // start of red
    step(1/3, "red");   // end of red at 1/3
    step(1/3, "gold");  // start of gold at 1/3
    step(2/3, "gold");  // end of gold at 2/3
    step(2/3, "blue");  // start of blue at 2/3
    step(1, "blue");    // end of blue at 3/3
    
    // store a triangle path - we'll reuse this for the demo loop
    ctx.moveTo(75, 0); ctx.lineTo(150, 150); ctx.lineTo(0, 150);
    
    (function loop() {
      ctx.globalCompositeOperation = "copy";  // will clear canvas with next draw
    
      // Fill the previously defined triangle path with any color:
      ctx.fillStyle = "#000";  // fill some solid color for performance
      ctx.fill();
      
      // draw a rectangle to clip the top using the following comp mode:
      ctx.globalCompositeOperation = "destination-in";
      ctx.fillRect(0, y, 150, 150 - y);
    
      // now that we have the shape we want, just replace it with the gradient:
      // to do that we use a new comp. mode
      ctx.globalCompositeOperation = "source-in";
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, 150, 150);
      
      y += dlt; if (y <= 0 || y >= 150) dlt = -dlt;  
      requestAnimationFrame(loop);
    })();
    <canvas/>

    Cached gradient image for animation (recommended)

    var ctx = document.querySelector("canvas").getContext("2d"),
        tcanvas = document.createElement("canvas"),    // to cache triangle
        tctx = tcanvas.getContext("2d"),
        grad = tctx.createLinearGradient(0, 0, 0, 150),
        step = grad.addColorStop.bind(grad), // function reference to simplify
        dlt = -3, y = 150;
    
    step(0, "red");     // start of red
    step(1/3, "red");   // end of red at 1/3
    step(1/3, "gold");  // start of gold at 1/3
    step(2/3, "gold");  // end of gold at 2/3
    step(2/3, "blue");  // start of blue at 2/3
    step(1, "blue");    // end of blue at 3/3
    
    // draw triangle to off-screen canvas once.
    tctx.moveTo(75, 0); tctx.lineTo(150, 150); tctx.lineTo(0, 150);
    tctx.fillStyle = grad; tctx.fill();
    
    (function loop() {
      ctx.clearRect(0, 0, 150, 150);
    
      // draw clipped version of the cached triangle image
      if (150-y) ctx.drawImage(tcanvas, 0, y, 150, 150 - y, 0, y, 150, 150 - y);
    
      y += dlt; if (y <= 0 || y >= 150) dlt = -dlt;  
      requestAnimationFrame(loop);
    })();
    <canvas/>

    You can change direction using the gradient line, which dictates the angle of the gradient.

    // vertical 
    ctx.createLinearGradient(0, 0, 0, 150); // x1, y1, x2, y2
    
    // hortizontal
    ctx.createLinearGradient(0, 0, 150, 0); // x1, y1, x2, y2
    
    // 45° degrees
    ctx.createLinearGradient(0, 0, 150, 150); // x1, y1, x2, y2
    

    etc.