Robert
Robert

Reputation: 686

Is it possible to draw an image on a path in HTML5 canvas?

For instance, say I have the following path.

<canvas id="main" width="500" height="250"></canvas>

var canvas = document.getElementById("main");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(100,20);
ctx.arcTo(150,20,150,70,50); 
ctx.lineTo(150,120);
ctx.lineWidth = 3;
ctx.stroke();

Is it possible to draw images on the arc of the line? If so, How?

Upvotes: 1

Views: 2476

Answers (2)

Blindman67
Blindman67

Reputation: 54026

Slice an image to draw on curves.

Yes it is possible, though ideally this would be a job for WebGL. The next best solution is a scan line render but that is way to much CPU load for poor Javascript to manage.

The next best I mean "OK sort of." option is a little image slicing.

You simply draw the image in thin slices around the arc. The 2D renderer is not perfect and tries to draw half pixels as best it can. The result is some noise along the edge of each slice where you can see through. To overcome this I draw each slice slightly wider to cover up any holes.

If you need high quality, rendering it all at double the size on an offscreen canvas and then scale down to a onscreen canvas (don't forget smoothing) will make most think it was drawn that way.

As the inner and outer edges of the arc have different circumferences some of the image must be squashed or stretched. In the demo I keep the inner edge of the image to the correct width and stretch the outer edge. It is easy to change but ensure that you use the outer edge to workout how many slices to draw.

WARNING the radius given is for the inner edge. It is vetted to stop the for loop getting too long and blocking the page. You may want to limit the radius so the inner circumference is the same as the image width. radius = radius < img.width / (Math.PI * 2) ? img.width / (Math.PI * 2) : radius;

It is easy to adapt to lines and curves. All you need is the tangent or curve normal (should be unit vector ie length 1) Use this vector to set the transform ctx.setTransform(nx,ny,tx,ty,px,py). THe first two values point out from the bottom of the image to the top, the next two numbers are along the tangent from left to right. The last two are the point on the curve to draw the slice.

// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}

// create a canvas and add to dom
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;

// create a image (canvas) to draw on the arc.
const textToDisplay = "<<Image on arc>>"

ctx.font = "64px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,84);
text.ctx.fillStyle = "#F90";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = "64px arial";
text.ctx.fillStyle = "#0F0";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,58);
text.ctx.fillText(textToDisplay,38,58);



// draws image on arc
// img image to render
// x,y  center of arc
// radius the inner edge (bottom of image) radius
// fromAng The angle to start drawing the image in radians
// toAng (optional if not given image width will be used to get toAng)
// returns undefined
function drawArcImage(img,x,y,radius,fromAng,toAng){
    
    // WARNING if you let the radius get to small the ratio between the inner and out circumference
    // gets very large. This will result in the image being stretched over a quintabazzilon pixels.
    // so must vet the radius or you will block the page and upset the browser gods.
    
    radius = Math.abs(radius); // only positive
    radius = radius < img.height / 8 ? img.height / 8 : radius;
    var outRad = radius + img.height;
    var cir = Math.PI * 2 * radius; // get inner circumference
    if(toAng === undefined){
        var toAng = (img.width / cir) * Math.PI * 2 ; // get the angle the image will cover
    }
    var cirOut = toAng * outRad;      // get the out edge distance in pixels
    var imgStep = img.width / cirOut; // the image step per slice
    var imgX = 0;                     // track the image line to draw
    var angStep = toAng / cirOut;     // the angle steps
    // For each pixel on the out edge draw a slice
    for(var i = 0; i < toAng; i += angStep){
        var dx = Math.cos(fromAng + i);
        var dy = Math.sin(fromAng + i);
        // set up the transform to draw a slice from the inner to outer edges
        ctx.setTransform(dy,-dx,-dx,-dy,dx * radius + x,dy * radius + y); 
        // get and draw the slice. I stretch it a little (2pix) to cover imperfect rendering
        ctx.drawImage(img,imgX,0,imgStep,img.height,-1,-img.height,2,img.height);
        // move to next slice
        imgX += imgStep;
    }
    ctx.setTransform(1,0,0,1,0,0);  // reset the transform
}


// animate the image to prove it is real.. LOL
var animTick = 0;
var animRate = 0.01;
var pos = 0;
// update function call via RAF
function update(){
    animTick += animRate;   // update tick
    // random anim sin waves.
    var rad = Math.sin(animTick) * (256-text.height - 20) + 20;
    pos += Math.sin(animTick*10) * 0.02;
    pos += Math.sin(animTick/ 3) * 0.02;
    pos += Math.sin(animTick/ 7) * 0.05;
    // clear
    ctx.clearRect(0,0,can.width,can.height)
    // draw
    drawArcImage(text,256,256,rad,pos)
    // do again and again and again
    requestAnimationFrame(update);
}
update();

Upvotes: 4

ahitt6345
ahitt6345

Reputation: 510

This is an answer to a similar question:

You could, in the draw loop implement a "line drawing algorithm" that does not exactly draw a line but draws an item at a place where that point would be. Except, replace the line algorithm here to draw an arc instead.

function line(x0, y0, x1, y1){
     var dx = Math.abs(x1-x0);
     var dy = Math.abs(y1-y0);
     var sx = (x0 < x1) ? 1 : -1;
     var sy = (y0 < y1) ? 1 : -1;
     var err = dx-dy;

     while(true){ // put draw loop here.
         drawImage(image,x0,y0);//setPixel(x0,y0);  // Do what you need to for this

         if ((x0==x1) && (y0==y1)) break;
             var e2 = 2*err;
             if (e2 >-dy){ err -= dy; x0  += sx; }
             if (e2 < dx){ err += dx; y0  += sy; }
    }
}

code taken from: Bresenham algorithm in Javascript

I would suggest using a library like p5.js to do something like this. http://p5js.org

Upvotes: 2

Related Questions