当前位置: 首页 > 文档资料 > HTML Canvas Deep Dive >

Chapter 3 Advanced Drawing and Events

优质
小牛编辑
126浏览
2023-12-01

Image Fills

In Chapter 1 we learned that Canvas can fill shapes with colors and gradients. You can also fill shapes with images by defining a pattern. You can control how the pattern is repeated the same as you would with background images in CSS.

var img = new Image();
img.onload = function() {
    
    var canvas = document.getElementById('can');
    var c = canvas.getContext('2d');
    var pat = c.createPattern(img,'repeat');    
    c.fillStyle = pat;
    c.fillRect(10,10,80,80);    
    
    var canvas2 = document.getElementById('can2');
    var c2 = canvas2.getContext('2d');
    var pat2 = c2.createPattern(img,'repeat-y');    
    c2.fillStyle = pat2;
    c2.fillRect(10,10,80,80);    
    
}
img.src = 'smile.png';

As with gradients, the pattern is drawn relative to the current coordinate system. That's why I had to translate by 200 pixels to the right before drawing the second rectangle. Since it doesn't repeat in the X direction, only y, making the filled area bigger won't actually draw more of the pattern. Try dragging the values around to see how it works.

var pat1 = ctx.createPattern(img,'repeat');    
ctx.fillStyle = pat1;
ctx.fillRect(0,0,100,100);    

var pat2 = ctx.createPattern(img,'repeat-y');    
ctx.fillStyle = pat2;
ctx.translate(200,0);
ctx.fillRect(0,0,100,100);

Note that filling with an image texture only works if the image has already been loaded, so be sure to do the drawing from the image's onload callback.

Opacity

The Canvas API lets you control the opacity of any drawing function with the globalAlpha property. This next demo draws two red squares overlapping with the background showing through by changing the globalAlpha before each drawing operation.

ctx.fillStyle = 'red';
//divide by 100 to get a fraction between 0 and 1
ctx.globalAlpha = 50/100; 
ctx.fillRect(0,0,50,50);
ctx.globalAlpha = 30/100;
ctx.fillRect(25,25,50,50);
ctx.globalAlpha = 1.0;

This opacity setting works with all drawing operations. Try changing the opacity values above to see the effect. Be sure to set it back to 1.0 when you are done so that it won't affect later drawing. The globalAlpha property must be a value between 0 and 1 or else it will be ignored (or may unexpected behavior on some platforms).

Transforms

In the bar chart chapter we drew the same rectangle over and over again just with different x and y coordinates. Rather than modifying those coordinates we could have used a translate function. Each time through the loop we can translate by an additional 100 pixels to move the next bar over to the right.

ctx.fillStyle = "red";
for(var i=0; i<data.length; i++) {
    var dp = data[i];
    ctx.translate(100, 0);
    ctx.fillRect(0,0,50,dp);
}

Try dragging the x translate variable to see how the effect combines across the chart.

Like many 2D APIs, Canvas has support for the standard translate, rotate, and scale transforms. This lets you draw shapes transformed around on the screen without having to calculate new points by hand. Canvas does the math for you. You can also combine transforms by calling them in order. For example, to draw a rectangle translated to the center and then rotated by 30 degrees you would do this:

ctx.fillStyle = "red";
ctx.translate(50,50);
//convert degrees to radians
var rads = 30 * Math.PI*2.0/360.0;
ctx.rotate(rads)
ctx.fillRect(0,0,100,100);

Each time you call translate, rotate, or scale it adds on to the previous transformation. Over time this could get confusing, of course. You could undo the transforms like this:

for(var i=0; i<data.length; i++) {
    c.translate(40+i*100, 460-dp*4);
    var dp = data[i];
    c.fillRect(0,0,50,dp*4);
    c.translate(-40-i*100, -450+dp*4);
}

but that's a lot of annoying code to write. If you forget to undo it just once then you could be screwed and spend hours looking through your code for that one bug. (not that I've ever done that, of course!) Instead Canvas provides a state saving API.

State Saving

The context2D object represents the current drawing state. In this book I always use the ctx variable to hold this context. The state includes the current transform, the fill and stroke colors, the current font, and a few other variables. You can save this state by pushing it onto a stack using the save() function. After you save the state you can make modifications, then restore to the previous state with the restore() function. Canvas takes care of the book-keeping for you. Here is the previous example written with state saving instead. Notice that we don't have to do the un-translation step.

for(var i=0; i<data.length; i++) {
    c.save();
    c.translate(40+i*100, 460-dp*4);
    var dp = data[i];
    c.fillRect(0,0,50,dp*4);
    c.restore();
}

Clipping

Sometimes you may want to draw just part of a shape. You can do this with the clip function. It takes the current shape and uses it as a mask for further drawing. This means that any drawing will only happen inside of the clip. Anything you draw outside of the clip will not be shown on screen. This can be useful for when you want to create a complex graphic by combining shapes, or when you want to update just a part of the screen for performance reasons. Here's an example where we draw a bunch of squares clipped by a circle:

c.strokeStyle = "black";
c.lineWidth = 10;
c.fillStyle = "red";

// draw rect the first time
c.fillRect(0,0,600,200);

c.save();

// create triangle path
c.beginPath();
c.moveTo(200,100);
c.lineTo(300,300);
c.lineTo(100,300);
c.closePath();

// stroke the triangle so we can see it
c.stroke();

// use triangle as clip, then fill rect in again with yellow
c.clip();
c.fillStyle = "yellow";
c.fillRect(0,0,600,200);
c.restore();

// draw rect the first time
ctx.fillStyle = 'red';
ctx.fillRect(0,0,400,100);

// create triangle path
ctx.beginPath();
ctx.moveTo(200,50);
ctx.lineTo(250,150);
ctx.lineTo(150,150);
ctx.closePath();

// stroke the triangle so we can see it
ctx.lineWidth = 10;
ctx.stroke();

// use triangle as clip, 
ctx.clip();
//fill rect in again with yellow
ctx.fillStyle = 'yellow';
ctx.fillRect(0,0,400,100);

Notice how the yellow rectangle fills the intersection of the red rectangle and the triangle. Also notice that the lower part of the triangle has a thick border, but the upper part has a thinner border. This is because the border is centered on the actual geometric edges of the triangle shape. The yellow covers up the inside border when it is clipped by the geometric triangle, but the outside border remains uncovered.

Events

Canvas doesn't define any new events. You can listen to the same mouse and touch events that you'd work with anywhere else. This is both good and bad.

The Canvas just looks like a rectangular area of pixels to the rest of the browser. The browser doesn't know about any shapes you've drawn. If you drag your mouse cursor over the canvas then the browser will send you standard drag events to the canvas as a whole, not to anything within the canvas. This means that if you want to do special things like making buttons or a drawing tool you will have to do the event processing yourself by converting the raw mouse events that the browser gives you to your own data model.

Calculating which shape is under the mouse cursor could be very difficult. Fortunately Canvas has an API to help: isPointInPath. This function will tell you if a given coordinate is inside of the current path. Here's a quick example:

c.beginPath();
c.arc(
    100,100, 40,  //40 pix radius circle at 100,100
    0,Math.PI*2,  //0 to 360 degrees for a full circle
);
c.closePath();
var a = c.isPointInPath(80,0);     // returns true
var b = c.isPointInPath(200,100);  // returns false

Another option is to use a scenegraph library such as Amino which lets you work in terms of shapes instead of pixels. It will handle event processing and repaints for you.