Search code examples
javascripthtmlcanvashtml5-canvasdistortion

Create an arc distortion of an image with canvas


I would like to create an arc distortion of an image with canvas.

My goal is to do the same thing as imagemagick but in javascript with canvas: https://legacy.imagemagick.org/usage/distorts/#circular_distorts

Here is the expected result with the angle parameter that corresponds to the images below: 60°, 120°, 180°, 270°, 360°

Arc distortion

I only found two interesting codes that go in the right direction:

This experimental script

which works directly on the pixel array but does not keep the aspect ratio of the original image and the angle given as a parameter does not work well: https://github.com/sergiks/canvas-text-arc

This other script

which makes a rotation on each column of the image with drawimage but does not allow to configure the angle of the arc, it is a 360° rotation by default: http://jsfiddle.net/hto1s6fy/

var cv = document.getElementById('cv');
var ig = document.getElementById('ig');
var ctx = cv.getContext('2d');


// draw the part of img defined by the rect (startX, startY, endX, endY) inside 
//   the circle of center (cx,cy) between radius (innerRadius -> outerRadius) 
// - no check performed -
function drawRectInCircle(img, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY) {
    var angle = 0;

    var step = 1 * Math.atan2(1, outerRadius);
    var limit = 2 * Math.PI;

    ctx.save();
    ctx.translate(cx, cy);
    while (angle < limit) {
        ctx.save();
        ctx.rotate(angle);
        ctx.translate(innerRadius, 0);
        ctx.rotate(-Math.PI / 2);
        var ratio = angle / limit;
        var x = startX + ratio * (endX - startX);
        ctx.drawImage(img, x, startY, 1, (endY - startY), 0, 0, 1, (outerRadius - innerRadius));
        ctx.restore();
        angle += step;
    }
    ctx.restore();
}

var cx = 300,
    cy = 300;

var innerRadius = 0;
var outerRadius = 300;

var startX = 0,
    endX = 1361,
    startY = 0,
    endY = 681;

drawRectInCircle(ig, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY);

Imagemagick source code

Finally, I also looked at the C source code of imagemagick but I don't have the skills to transpose it: https://github.com/imagemagick/imagemagick/blob/main/magickcore/distort.c (to see what concerns arc distortion, use the keyword "ArcDistortion")


Solution

  • Though this is an interesting topic and I also like to re-invent the wheel sometimes, it isn't necessary in this case. Someone else had a go at it yet and released a JavaScript library called lens, which replicates some of ImageMagick's filters. Luckily the 'Arc distortion' is among those.

    Lens offers a method called distort() which accepts an input like a <canvas> element, applies the transformation requested and outputs raw pixel data, which you can then use to make another <canvas>.

    Here's a quick example:

    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    ctx.font = "48px sans";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillStyle = "#0";
    ctx.fillText("Around the World", canvas.width / 2, canvas.height / 2);
    
    
    async function makeArc(image, angle, rotate = 0) {
      let result = await lens.distort(
        image,
        lens.Distortion.ARC, [angle, rotate], {
          imageVirtualPixelMethod: angle === 360 ? lens.VirtualPixelMethod.HORIZONTAL_TILE : lens.VirtualPixelMethod.TRANSPARENT
        }
      );
    
      let tempCanv = document.createElement("canvas");
      let tempCtx = tempCanv.getContext("2d");
      tempCanv.width = result.image.width;
      tempCanv.height = result.image.height;
      tempCtx.putImageData(result.image.imageData, 0, 0);
      document.body.appendChild(tempCanv);
    }
    
    makeArc(canvas, 120, 0);
    <script src="https://cdn.jsdelivr.net/npm/@alxcube/[email protected]/dist/lens.min.js"></script>
    <canvas id="canvas" width="450" height="50"></canvas>