0

I am trying to fill a circle (arc) with an image.

Here is my code :

draw() {
ctx.save();
let boulePat = new Image();
switch(this.couleur) {
  case "red":
    boulePat.src = "images/red.png";
    break;
  case "green":
    boulePat.src = "images/green.png";
    break;
  case "orange":
    boulePat.src = "images/orange.png";
    break;
  case "yellow":
    boulePat.src = "images/yellow.png";
    break; 
  case "purple":
    boulePat.src = "images/purple.png";
    break;
 }
  var pattern = ctx.createPattern(boulePat, "repeat");
  ctx.beginPath();
  ctx.arc(this.x, this.y, 15, 0, 2 * Math.PI);
  ctx.fillStyle = pattern;
  ctx.fill();
  ctx.restore();
}

With this, I have empty or black circles ...

Could you help me, please ? Thank you very much.

  • Looks like you have to wait for the image to load before using it in `createPattern`. The rest of your code looks fine. See: https://stackoverflow.com/questions/10791610/javascript-html5-using-image-to-fill-canvas – mhodges Jan 10 '19 at 21:46

2 Answers2

0

You have to wait for the image to load before you can use it in createPattern, like so:

See this stack answer

Keep in mind, the image will start from 0,0 coordinates in relation to the canvas. You will need to factor this into any offsets (using ctx.transform(xOffset, yOffset)) if necessary.

var canvas = document.getElementById("myCanvasNoTranslate");
var canvas2 = document.getElementById("myCanvasWithTranslate");

function drawCanvas(_canvas) {
  var context = _canvas.getContext("2d");
  draw(context);
}

function draw(ctx) {
  ctx.save();
  ctx.strokeSyle = "rgb(0, 0, 0)";
  ctx.lineWidth = 3;
  ctx.strokeRect(0, 0, 400, 200);
  
  let boulePat = new Image();
  boulePat.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/16777216colors.png/100px-16777216colors.png";
  boulePat.onload = function () {
    var pattern = ctx.createPattern(boulePat, "repeat");
    ctx.fillStyle = pattern;
    ctx.beginPath();
    ctx.arc(100, 100, 50, 0, 2 * Math.PI);
    // translate canvas to offset where the image coordinates are for .fill()
    if (ctx.canvas === canvas2) {
      ctx.translate(50, 50);  
    }
    ctx.fill();
    // restore ctx back to before the translate()
    ctx.restore();
  }
}

drawCanvas(canvas);
drawCanvas(canvas2);
With ctx.translate() for background fill<br/>
<canvas id="myCanvasWithTranslate" height="200px" width="400px"></canvas>
<br/><br/>
Without ctx.translate() for background fill<br/>
<canvas id="myCanvasNoTranslate" height="200px" width="400px"></canvas>
mhodges
  • 10,938
  • 2
  • 28
  • 46
  • Hello, thank you for the answer. I don't get how to use an image of a circle into my circles in my canvas ... How to set the image position ? – Vincent Bourdon Jan 11 '19 at 07:33
  • @VincentBourdon You will have to use a transform on the pattern to set the offset. [See this stack post for how to do that](https://stackoverflow.com/questions/20253210/canvas-pattern-offset) – mhodges Jan 11 '19 at 21:26
  • @VincentBourdon I also updated my post to show how you can use `ctx.translate()` to accomplish what you asked about. – mhodges Jan 11 '19 at 21:33
0

The code below takes an image URL, the radius of the arc, and the angle/size of the arc, and returns a canvas that has that image clipped to the given arc, and "contained" within the arc such that the image fills the arc without changing the image's aspect ratio:

  async function getArcClippedCanvas(imageUrl, radius, arcSizeDeg) {   
    let arcSizeRad = (arcSizeDeg/360)*2*Math.PI;
    
    // derive required width and height of canvas from radius and arc size
    let width;
    if(arcSizeDeg >= 180) {
      width = radius*2;
    } else {
      width = radius*Math.sin(arcSizeRad/2)*2;
    }
    
    let height;
    if(arcSizeDeg <= 180) {
      height = radius;
    } else {
      height = radius + radius*Math.sin( (arcSizeRad-Math.PI)/2 );
    }
    
    let arcCenterX = width/2;
    let arcCenterY = radius; // remember, y axis starts from top of canvas
     
    let canvas = document.createElement("canvas");
    let ctx = canvas.getContext("2d");
    
    canvas.width = width;
    canvas.height = height;
    
    let img = new Image();
    await new Promise(resolve => {
      img.onload = resolve;
      img.src = imageUrl;
    });
    
    let centerAngle = -Math.PI/2;
    
    ctx.beginPath();
    ctx.moveTo(arcCenterX, arcCenterY); 
    ctx.arc(arcCenterX, arcCenterY, radius, centerAngle - (arcSizeDeg/2)*2*Math.PI/360, centerAngle + (arcSizeDeg/2)*2*Math.PI/360);
    ctx.clip();
    
    // we want to "cover" the canvas with the image without changing the image's aspect ratio
    drawImageToCanvasContained(ctx, img, 0, 0, canvas.width, canvas.height);
    
    return canvas;
  }


  function drawImageToCanvasContained(ctx, img, x, y, w, h, offsetX, offsetY) {
    // By Ken Fyrstenberg Nilsen: https://stackoverflow.com/a/21961894/11950764
    if(arguments.length === 2) {
      x = y = 0;
      w = ctx.canvas.width;
      h = ctx.canvas.height;
    }

    // default offset is center
    offsetX = typeof offsetX === "number" ? offsetX : 0.5;
    offsetY = typeof offsetY === "number" ? offsetY : 0.5;

    // keep bounds [0.0, 1.0]
    if(offsetX < 0) offsetX = 0;
    if(offsetY < 0) offsetY = 0;
    if(offsetX > 1) offsetX = 1;
    if(offsetY > 1) offsetY = 1;

    let iw = img.width;
    let ih = img.height;
    let r = Math.min(w / iw, h / ih);
    let nw = iw * r;   // new prop. width
    let nh = ih * r;   // new prop. height
    let cx, cy, cw, ch, ar = 1;

    // decide which gap to fill    
    if(nw < w) ar = w / nw;                             
    if(Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh;  // updated
    nw *= ar;
    nh *= ar;

    // calc source rectangle
    cw = iw / (nw / w);
    ch = ih / (nh / h);

    cx = (iw - cw) * offsetX;
    cy = (ih - ch) * offsetY;

    // make sure source rectangle is valid
    if(cx < 0) cx = 0;
    if(cy < 0) cy = 0;
    if(cw > iw) cw = iw;
    if(ch > ih) ch = ih;

    // fill image in dest. rectangle
    ctx.drawImage(img, cx, cy, cw, ch,  x, y, w, h);
  }
  
joe
  • 3,752
  • 1
  • 32
  • 41