zarocknz/javascript-winwheel

Is it possible to crop images of a segment by its borders?

Opened this issue · 3 comments

I want behaviour like in css - overflow: hidden

is it possible?

also, is it possible to manipulate image as background in css - for example - set positioning?

Hi. Unfortunately is not possible to use CSS on HTML canvas. Drawing on canvas is like using MS paint where you can only use basic lines, shapes, and text.

You will need to create the image(s) exactly as they need to appear in the segments with transparency outside the borders of the segments.

I also wanted to do this so I put together some code to achieve it. Sharing here in case it's useful to others:

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

And here's how I'm using getArcClippedCanvas(...) to manually create the imgData property of each segment that Winwheel creates behind the scenes:

      let segments = [ ... ];
      ...
      let segmentSizeSum = segments.reduce((a,v) => a + (v.size || 0), 0);
      let unsizedSegments = segments.reduce((a,v) => a + (v.size === undefined ? 1 : 0), 0);
      let segmentSizeRemainder = 360 - segmentSizeSum;
      for(let segment of segments) {
        let segmentSize = segment.size !== undefined ? segment.size : segmentSizeRemainder/unsizedSegments;
        segment.imgData = await window.getArcClippedCanvas(segment.image, wheelDiameter/2, segmentSize);
        delete segment.image;
      }
      ...

It "contains" and centers the image within the "slice". I haven't tested this super thoroughly, but it seems to be working okay so far.

Edit: Ah, I've just noticed that if a segment is larger than 180 degrees then the segment's image doesn't render in the correct place. I can see why it's happening, but I've not yet looked into Winwheel's internals to see how hard this is to fix.