Friday, April 8, 2016

Colors, gradients, patterns, shadows, etc.

Canvas context: colors

INTRODUCTION

In previous examples, we saw how to set the current color using the strokeStyle and fillStyle properties of the canvas context object.
We will go a little more into details about colors and we will see how we can use gradients or patterns/textures/images (in other words: fill shapes or fill the outline of the shapes with some images that repeat themselves).

COLORS AND TRANSPARENCY

You can use the same syntax for colors that is supported by CSS3. The next lines show possible values/syntaxes.
  1. ctx.strokeStyle = 'red';
  2. ctx.fillStyle = "#00ff00";
  3. ctx.strokeStyle = "rgb(0, 0, 255)";
  4. ctx.fillStyle = "rgba(0, 0, 255, 0.5)";
Note that:
    • All values are strings,
    • Line 4 defines a "transparent color", the "a" of "rgba" means "alpha channel". Its value is between 0 and 1, where 0 means "completely transparent" and 1 means "opaque".
Here is an example that shows how to draw different filled rectangles in blue, with different levels of transparency. 

transparent rgba color example


Linear gradients

It is possible to define the stroke or the fill style as a "gradient", a set of interpolated colors, like in this example below (try it online at: http://jsbin.com/ruguxi/2/edit)
linear gradient from blue to white to red
The concept of linear gradient is seen as an "invisible" rectangle in which a set of colors are interpolated along a line.
The gradient becomes visible when we draw shapes on top of the invisible gradient, and when the fillStyleor strokeStyle property has for value this gradient.
Here are the different steps needed:

Step 1: define a linear gradient

Syntax: 
  1. ctx.createLinearGradient(x0,y0,x1,y1);
... where the (x0, y0) and (x1, y1) parameters define "the direction of the gradient" (as a vector with a starting and an ending point). This direction is an invisible line along which the colors that compose the gradient will be interpolated.
Let's detail an example:
  1. grdFrenchFlag = ctx.createLinearGradient(0, 0, 300, 0);
This line defines the direction of the gradient: a virtual, invisible line that goes from the top left corner of the canvas (0, 0) to the top right corner of the canvas (300, 0). The interpolated colors will propagate along this line. 
If this gradient is going to be reused by different functions, it is a good practice to create/initialize it in a function called when the page is loaded and to store it in a global variable.

Step 2: add a number of "color stops" to this gradient

We will add a set of "colors" and "stops" to this gradient. The stops go from 0 (beginning of the virtual line defined just above), to 1 (end of the virtual line). A color associated with a value of 0.5 will be right in the middle of the virtual line.
Here is an example that corresponds to an interpolated version of the French flag, going from blue to white, then to red, with proportional intervals. We define three colors, blue at position 0, white at position 0.5 and red at position 1:
  1. grdFrenchFlag.addColorStop(0, "blue");
  2. grdFrenchFlag.addColorStop(0.5, "white");
  3. grdFrenchFlag.addColorStop(1, "red");

Step 3: draw some shapes

First, let's set the fillStyle or strokeStyle of the context with this gradient, then let's draw some shapes "on top of the gradient".
In our example, the gradient corresponds to an invisible rectangle that fills the canvas. If we draw a rectangle of the canvas size, it should be filled with the entire gradient:
  1. ctx.fillStyle = grdFrenchFlag;
  2. ctx.fillRect(0, 0, 300, 200);
The result is shown below: a big rectangle that fills the whole canvas, with colors going from blue (left) to white (middle) to red (right).
linear gradient from blue to white to red, left to right

CHANGING THE DIRECTION OF THE GRADIENT

If you modify the source code that defines the direction of the gradient as follows...
  1. grdFrenchFlag = ctx.createLinearGradient(0, 0, 300, 200);
... then you will define a gradient that goes from the top left corner of the canvas to the bottom right of the canvas. Let's see what it does (online version here: http://jsbin.com/ruguxi/4/edit):
diagonal gradient from top left to bottom right

DRAWING SHAPES THAT DO NOT COVER THE WHOLE GRADIENT

Instead of drawing a filled rectangle that covers the whole surface of the canvas, let's draw several smaller rectangles.
smaller rectangle on top of a gradient
Note that the canvas has its default background color where we did not draw anything. And where we drawn rectangles, we can see "through" and the colors from the gradient are visible.
Here is the code that draws the checkboard:
  1. ctx.fillStyle = grdFrenchFlag;
  2. ctx.fillRect(0, 0, 50, 50);
  3. ctx.fillRect(100, 0, 50, 50);
  4. ctx.fillRect(200, 0, 50, 50);
  5. ctx.fillRect(50, 50, 50, 50);
  6. ctx.fillRect(150, 50, 50, 50);
  7. ctx.fillRect(250, 50, 50, 50);
  8. ctx.fillRect(0, 100, 50, 50);
  9. ctx.fillRect(100, 100, 50, 50);
  10. ctx.fillRect(200, 100, 50, 50);
  11. ctx.fillRect(50, 150, 50, 50);
  12. ctx.fillRect(150, 150, 50, 50);
  13. ctx.fillRect(250, 150, 50, 50);
This code is rather ugly isn't it? It would have been better  to use a loop...
Here is function that draws a chessboard (online example here: http://jsbin.com/dekiji/2/edit):
  1. // n = number of cells per row/column
  2. function drawCheckboard(n) {
  3.     ctx.fillStyle = grdFrenchFlag;
  4.     var l = canvas.width;
  5.     var h = canvas.height;
  6.  
  7.     var cellWidth = l / n;
  8.     var cellHeight = h / n;
  9.     for(= 0; i < n; i+=2) {
  10.        for(= 0; j < n; j++) {
  11.           ctx.fillRect(cellWidth*(i+j%2), cellHeight*j, cellWidth,cellHeight);
  12.        }
  13.     }
  14. }
The two loops (lines 11-15) draw only one cell out of two (see the i+=2 at line 11), and shift them one cell on the right if the current line is an odd number (the formula cellWidth * (I+j%2) does that, when computing the X coordinate of each cell).
This code is much more complex that the previous one, takes 16 lines instead of 13, but is much more powerful. Try to call the function with a value of 10, 20, or 2... 

DRAWING OUTLINED SHAPES WITH GRADIENTS

As we used fillStyle and fillRect for drawing rectangles filled with a gradient, we can also usestrokeStyle and strokeRect in order to draw wireframed rectangles. The next example is just a variation of the previous one, where we just used the lineWidth property to set the outline of the rectangles at 5 pixels: http://jsbin.com/dekiji/4/edit
stroke rectangles with gradient
Extract from source code:
  1. function drawCheckboard(n) {
  2.     ctx.strokeStyle = grdFrenchFlag;
  3.     ctx.lineWidth=10;
  4.     ...
  5.     for(= 0; i < n; i+=2) {
  6.         for(= 0; j < n; j++) {
  7.             ctx.strokeRect(cellWidth*(i+j%2), cellHeight*j, cellWidth,cellHeight);
  8.         }
  9.     }
  10. }

WHAT HAPPENS IF WE DEFINE A GRADIENT SMALLER THAN THE CANVAS?

Let's go back to the very first example of this page. The one with the blue-white-red interpolated French flag. This time we will define a gradient that is smaller. Instead of going from (0, 0) to (300, 0), it will go from(100, 0) to (200, 0), while the canvas remains the same (width=300height=200).
  1. grdFrenchFlag = ctx.createLinearGradient(100, 0, 200, 0);
Like in the first example we will draw a filled rectangle that is the same size of the canvas. Here is the online version: http://jsbin.com/ruvuta/1/edit, and here is a screenshot of the result:
gradient smaller thant the drawn shape
We notice that "before" the gradient start, the first color of the gradient is repeated without any interpolation (columns 0-100 are all blue), then we "see through" and the gradient is drawn (columns 100-200), then the last color of the gradient is repeated without any interpolation (columns 200-300 are red).

WHAT HAPPENS IF WE DEFINE A GRADIENT BIGGER THAN THE CANVAS?

Nothing special, we will "see through the drawn shapes" and parts of the gradient that are located in the canvas area will be shown. You can try this example that defines a gradient twice bigger than the canvas: 
  1. grdFrenchFlag = ctx.createLinearGradient(0, 0, 600, 400);
And if we draw the same rectangle with the canvas size, here is the result:
gradient bigger than the canvas size
The red color is far away further than the bottom right corner.... we see only the first top left quarter of the gradient.

DRAW SHAPES THAT SHARE THE SAME GRADIENT AS A WHOLE

This time, we would like to draw the chessboard with the gradient in each cell. How can we do this with one single gradient?
We can't! At least we can't without recreating it for each cell!
It suffices to create a new gradient before drawing each filled rectangle, and set it with the starting and ending point of its direction/virtual line accordingly to the rectangle coordinates. Here is an online example and the resulting display:
chessboard with individual gradient for each cell
Extract from source code:
  1. function setGradient(x, y, width, height) {
  2.     grdFrenchFlag = ctx.createLinearGradient(x, y, width, height);
  3.     grdFrenchFlag.addColorStop(0, "blue");
  4.     grdFrenchFlag.addColorStop(0.5, "white");
  5.     grdFrenchFlag.addColorStop(1, "red");
  6.     // set the new gradient to the current fillStyle
  7.     ctx.fillStyle = grdFrenchFlag;
  8. }
  9.  
  10. // n = number of cells per row/column
  11. function drawCheckboard(n) {
  12.    var l = canvas.width;
  13.    var h = canvas.height;
  14.  
  15.    var cellWidth = l / n;
  16.    var cellHeight = h / n;
  17.    for(= 0; i < n; i+=2) {
  18.      for(= 0; j < n; j++) {
  19.         var x = cellWidth*(i+j%2);
  20.         var y = cellHeight*j;
  21.         setGradient(x, y, x+cellWidth, y+cellHeight);
  22.         ctx.fillRect(x, y, cellWidth, cellHeight);
  23.      }
  24.    }
  25. }
We wrote a function setGradient(startX, startY, endX, endY) that creates a gradient and set thefillStyle context property so that any filled shape drawn will be with this gradient.
In the drawCheckBoard(...) function we call it just before drawing rectangles. Like that, each rectangle is drawn using its own gradient.

THIS FLAG IS NOT REALLY LOOKING LIKE THE FRENCH FLAG, ISN'T IT?

Indeed the French flag in these images is more accurate:
french flag with linear gradient
french flags with linear gradient
We slightly modified the examples of this chapter so that the flag looks more like the French flag. Look at these modified versions below,  and try to find out what has changed in the gradient definitions:
  • Radial gradients

    BASIC PRINCIPLE / SYNTAX: DEFINE TWO CIRCLES AT GRADIENT CREATION

    Radial gradients are for creating gradients that propagate/interpolate colors along circles instead of propagating/interpolating along a virtual line, like linear gradients.
    Here is an example of a radial gradient that interpolates the color of the rainbow. Online version: http://jsbin.com/mijoni/2/edit
    radial gradient example: circles with the rainbow colors
    The gradient is defined like that in the example:
    1. var grd = context.createRadialGradient(150, 100, 30, 150, 100, 100);
    2. grd.addColorStop(0, "red");
    3. grd.addColorStop(0.17, "orange");
    4. grd.addColorStop(0.33, "yellow");
    5. grd.addColorStop(0.5, "green");
    6. grd.addColorStop(0.666, "blue");
    7. grd.addColorStop(1, "violet");
    8. context.fillStyle = grd;
    The method from the context object createRadialGradient(cx1, cy1, radius1, cx2, cy2, radius2) takes as the first three parameters the "starting" circle of the gradient, and as the three last parameters, the "ending circle".
    In the above example, the gradients starts at a circle located at (150, 100), with a radius of 30, and propagates to a circle with the same center as the first one (150, 100), but with a bigger radius of 100. Here, we can see them:
    //d37djvu3ytnwxt.cloudfront.net/asset-v1:W3Cx+W3C-HTML5+2015T3+type@asset+block/radialgradient2.jpg
    The way we added color stops is similar to what we have see with linear gradients.

    WHAT HAPPENS IF THE CIRCLES ARE NOT LOCATED AT THE SAME PLACE?

    You obtain some nice effects, here we set the second circle's center 60 pixels to the right of the first circle's center (cx = 210 instead of 150). Online example: http://jsbin.com/fufelef/1/edit?html
    1. grd = ctx.createRadialGradient(150, 100, 30, 210, 100, 100);
    And here is the result:
    radial gradient with circles non aligned

    WHAT HAPPENS IF THE GRADIENT IS SMALLER OR LARGER THAN THE SHAPES WE DRAW?

    Like for the linear gradient, if the gradient is smaller, then for the area "before" the first stop color, this color will be used to fill the shape. This is the case of the red color inside the small circle of the gradient in our example: it is filled in red!
    For the area outside of the gradient, the last stop color is used, this is the case in our example with the purple color that fills the rest of the filled rectangle area "after the external circle of the gradient.

Painting with patterns

PRINCIPLE

The principle of "pattern" drawing consists in repeating an image (if the image is smaller than the surface of the shape you are going to draw) for filling the surface of objects to be drawn (either filled or stroked).
To illustrate this principle, in the next examples, we are going to draw rectangles using this pattern: 
an example of repeateable pattern
We will need to do some steps before drawing any shapes with this pattern:
    1. Create a JavaScript image object

      1. var imageObj = new Image();
    2. Define a callback function that will be called once the image has been fully loaded in memory, we cannot draw before the image has been loaded.

      1. imageObj.onload = function(){
      2. ...
      3. }
    3. Set the source of this image to the URL of the pattern (in our example with url of the pattern),

      1. imageObj.src = "http://www.myserver.com/myRepeatablePattern.png";
    4. As soon as step 3 is executed, an HTTP request is sent in background by the browser, and when the image is loaded in memory, the callback defined at step 2 is called. We create a pattern object inside, from the loaded image:

      1. // callback called asynchronously, after the src attribute of imageObj is set
      2. imageObj.onload = function(){ 
      3.     // We enter here when the image is loaded, we create a pattern object.
      4.     // a good practice is to set this as a global variable, easier to share
      5.     pattern1 = ctx.createPattern(imageObj, "repeat");
      6. };
    5. Inside the callback function (or inside a function called from inside the callback) we can draw.

      1. // callback called asynchronously, after the src attribute of imageObj is set
      2. imageObj.onload = function(){
      3.     pattern1 = ctx.createPattern(imageObj, "repeat");
      4.     // Draw a textured rectangle
      5.     ctx.fillStyle = pattern1;
      6.     ctx.fillRect(10, 10, 500, 800);
      7. };


EXAMPLE 1: DRAW TWO RECTANGLES WITH A PATTERN (ONE FILLED, ONE STROKED)

Here we have two rectangles drawn using a pattern (an image that can be repeated along the X and Y axis). The first one is a filled rectangle while the second one is "stroked" with a lineWidth of 10 pixels.
example of painting with patterns
HTML source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <body onload="init();">
  4.    <canvas id="myCanvas" width="500" height="400">
  5.        Your browser does not support the canvas tag. </canvas>
  6.    </body>
  7. </html>
JavaScript source code:
  1. var canvas, ctx, pattern1;
  2.  
  3. function init() {
  4.    canvas = document.querySelector('#myCanvas');
  5.    ctx = canvas.getContext('2d');
  6.    // We need 1) to create an empty image object, 2) to set a callback function
  7.    // that will be called when the image is fully loaded, 3) to create a
  8.    // pattern object, 4) to set the fillStyle or the strokeStyle property of
  9.    // the context with this pattern, 5) to draw something
  10.    // WE CANNOT DRAW UNTIL THE IMAGE IS FULLY LOADED -> draw from inside the
  11.    // onload callback only !
  12.    // 1 - Allocate an image
  13.    var imageObj = new Image();
  14.  
  15.    // 2 - callback called asynchronously, after the src attribute of imageObj
  16.    // is set
  17.    imageObj.onload = function(){
  18.       // We enter here only when the image has been loaded by the browser
  19.       // 4 - Pattern creation using the image object
  20.       // Instead of "repeat", try different values : repeat-x, repeat-y,
  21.       // or no-repeat, You may draw larger shapes in order to see
  22.       // different results
  23.       // It is a good practice to leave this as a global variable if it
  24.       // will be reused by other functions
  25.       pattern1 = ctx.createPattern(imageObj, "repeat");
  26.       // 5 - Draw things. Here a textured rectangle
  27.       ctx.fillStyle = pattern1;
  28.       ctx.fillRect(10, 10, 200, 200);
  29.       // ... And a wireframe one
  30.       ctx.lineWidth=20;
  31.       ctx.strokeStyle=pattern1;
  32.       ctx.strokeRect(230, 20, 150, 100);
  33.   };
  34.   // 3 - Send the request for loading the image
  35.   // Settingthe src attribute will tell the browser to send an asynchronous
  36.   // request.
  37.   // When the browser will get an answer, the callback above will be called
  38.   imageObj.src = "http://www.dreamstime.com/colourful-flowers-repeatable-pattern-thumb18692760.jpg";
  39. }

EXAMPLE 2: THE REPEATABILITY OF A PATTERN

To "better" see the repeatability of the pattern, here is the same example with a 1000x1000 pixel wide canvas.
Online version here: http://jsbin.com/befiti/3/edit, and here is the result:
same example with bigger canvas and bigger rectangles
You can change the way the pattern is repeated by modifying the second parameter of this method:
  1. pattern1 = ctx.createPattern(imageObj, "repeat");
Please try: repeat-xrepeat-y or no-repeat as acceptable values. Just change this line in the online example and you will see live results.

Multiple image loader (for advanced users, or just take it as it is: a utility function you can reuse)

See below the 4 rectangles drawn with 4 different patterns.
4 rectangles drawn with different patterns

DRAW WITH MULTIPLE PATTERNS? WE NEED TO LOAD ALL OF THEM BEFORE DRAWING! 

We said earlier that we cannot draw before the image used by a pattern is loaded. This can become rapidly complicated if we need to draw using multiple patterns. We need a way to load all images and then, only when all images have been loaded, start drawing.
JavaScript is an asynchronous language. When you set the src attribute of an image, then an asynchronous request is sent by the browser, and then after a while, the onload callback is called... The difficult part to understand for those who are not familiar with JavaScript is that these requests are done in parallel and we do not know when the images will be loaded, and in which order.

SOLUTION: A MULTIPLE IMAGE LOADER THAT COUNTS THE LOADED IMAGES AND CALLS A FUNCTION YOU PASS WHEN DONE!

The trick consists in having an array of URLs that will be used by our multiple image loader, then in the onloadcallback, that will be called once per image loaded, we count the number of images effectively loaded.
When all images have been loaded, then we call a callback function that has been passed to our loader.
A complete example code that produces the result shown at the beginning of this page is at: http://jsbin.com/fufiyu/6/edit

Define the list of images to be loaded

  1. // List of images to load, we used a JavaScript object instead of
  2. // an array, so that named indexes (aka properties)
  3. // can be used -> easier to manipulate
  4. var imagesToLoad = {
  5.      flowers:
  6.         'http://cdn2.digitalartsonline.co.uk/images/features/1675/intro.jpg',
  7.      lion: 'http://farm3.static.flickr.com/2319/2486384418_8c031fec76_o.jpg',
  8.      blackAndWhiteLys: 'http://pshero.com/assets/tutorials/0062/final.jpg',
  9.      tiledFloor:
  10.       'https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnqMJRDWtsilwvxWgAMjjW6IK1Kah5eirAmWJj5CWr-KUCaLNoCDaieJOJfE7HjT5U3MbVWtTAZScjUc4l1uQSelMu1S5xrAoghKM8SFNyAfmG3LBYQLkNGHkP_dV74J7kygSSzl0ht5I/s1600/symmetry:assymetry+repeatable+pattern.jpg'
  11. };
Notice that instead of using a traditional array, we defined this list as a JavaScript object, with properties whose names will be easier to manipulate (flowers, lion, tiledFloor, etc.).

The image loader function

  1. function loadImages(imagesToBeLoaded, drawCallback) {
  2.      var imagesLoaded = {};
  3.      var loadedImages = 0;
  4.      var numberOfImagesToLoad = 0;
  5.      // get num of images to load
  6.      for(var name in imagesToBeLoaded) {
  7.          numberOfImagesToLoad++;
  8.      }
  9.      for(var name in imagesToBeLoaded) {
  10.          imagesLoaded[name] = new Image();
  11.          imagesLoaded[name].onload = function() {
  12.              if(++loadedImages >= numberOfImagesToLoad) {
  13.                  drawCallback(imagesLoaded);
  14.              } // if
  15.          }; // function
  16.          imagesLoaded[name].src = imagesToBeLoaded[name];
  17.       } // for
  18. } // function
This function takes as a parameter the list of images to be loaded, and a drawCallback function that will be called only once all images have been loaded. This callback takes as a parameter a new object that is the list of images that have been loaded (see line 16).
We first count the number of images to load (lines 7-9), then for each image to be loaded we create a new JavaScript image object (line 12) and set its src attribute (line 19), this will start to load the image.
When an image comes in, the onload callback is called (line 14) and inside, we increment the number of images loaded (line 15) and test if this number is >= of the total number of images that should be loaded. If this is the case, the callback function is called (line 16).

Example of use of this loader

  1. loadImages(imagesToLoad, function(imagesLoaded) {
  2.     patternFlowers = ctx.createPattern(imagesLoaded.flowers, 'repeat');
  3.     patternLion    = ctx.createPattern(imagesLoaded.lion, 'repeat');
  4.     patternBW = ctx.createPattern(imagesLoaded.blackAndWhiteLys, 'repeat');
  5.     patternFloor   = ctx.createPattern(imagesLoaded.tiledFloor, 'repeat');
  6.     drawRectanglesWithPatterns();
  7. });
  • Line 1 is the call to the image loader, the first parameter is the list of images to be loaded, while the second parameter is the callback function that will be called once all images have been loaded.
  • Lines 2-5: in this callback we create patterns from the loaded images (note the use of the property names imagesLoaded.flowers, etc. that makes the code easier to read).
  • Line 7: then we call a function that will draw the rectangles. 
Here is the function:
  1. function drawRectanglesWithPatterns() {
  2.     ctx.fillStyle=patternFloor;
  3.     ctx.fillRect(0,0,200,200);
  4.     ctx.fillStyle=patternLion;
  5.     ctx.fillRect(200,0,200,200);
  6.     ctx.fillStyle=patternFlowers;
  7.     ctx.fillRect(0,200,200,200);
  8.  
  9.     ctx.fillStyle=patternBW;
  10.     ctx.fillRect(200,200,200,200);
  11. }

Drawing shadows

CONTEXT PROPERTIES TO DRAW WITH SHADOWS

a shadowed rectangle

There are 4 properties of the canvas context that are useful for indicating that we want to draw shapes with shadows:
    1. shadowColor: color to use for shadows,
    2. shadowBlur: blur level for shadows,
    3. shadowOffsetX: horizontal distance of the shadow from the shape,
    4. shadowOffsetY: vertical distance of the shadow from the shape

EXAMPLE 1: SIMPLE

two shadowed rectangles
HTML source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <body onload = init();>
  4.     <canvas id="myCanvas" width="400" height =800>
  5.         Your browser does not support the canvas tag.
  6. </canvas>
  7. </body>
  8. </html>
JavaScript source code:
  1. var canvas, ctx;
  2. function init() {
  3.     canvas = document.getElementById('myCanvas');
  4.     ctx = canvas.getContext('2d');
  5.     // call to a function that will set the 4 context properties for shadows
  6.     setShadow();
  7.     // all drawings that will occur will cast shadows
  8.     // first green filled rectangle
  9.     ctx.fillStyle = "#22FFDD"; 
  10.     ctx.fillRect(20, 20, 200, 100);
  11.     // second stroked rectangle
  12.     ctx.strokeStyle = "purple"; 
  13.     ctx.lineWidth=10;
  14.     ctx.strokeRect(20, 150, 200, 100);
  15. }
  16.  
  17. // We define the 4 properties in a dedicated function, for clarity
  18. function setShadow() {
  19.     ctx.shadowColor = "Grey";    // color
  20.     ctx.shadowBlur = 20;         // blur level
  21.     ctx.shadowOffsetX = 15;      // horizontal offset
  22.     ctx.shadowOffsetY = 15;      // vertical offset
  23. }
  • Lines 21-27: we set the 4 properties that define shadows in a dedicated function, for a better clarity.
  • Line 8: we called this function once before drawing the rectangles.
  • Lines 11-18: we draw a filled and a stroked rectangle. Both rectangles cast shadows.

EXAMPLE 2: UNWANTED SHADOWS!

Let's take a previous example: the one that draws a filled circle with an outline: http://jsbin.com/gazuba/2/edit
filled circle with outline
Now, let's add a shadow to it, online example: http://jsbin.com/gokemu/1/edit
Here is an extract from the code:
  1. ...
  2. ctx.beginPath();
  3. // Add to the path a full circle (from 0 to 2PI)
  4. ctx.arc(centerX, centerY, radius, 0, 2*Math.PI, false);
  5. // With path drawing you can change the context
  6. // properties until a call to stroke() or fill() is performed
  7. ctx.fillStyle = "lightBlue";
  8. // add shadows before drawing the filled circle
  9. addShadows();
  10. // Draws the filled circle in light blue
  11. ctx.fill();
  12. // Prepare for the outline
  13. ctx.lineWidth = 5;
  14. ctx.strokeStyle = "black";
  15. // draws AGAIN the path (the circle), this
  16. // time in wireframe
  17. ctx.stroke();
  18. // Notice we called context.arc() only once ! And drew it twice
  19. // with different styles
  20. ...
  21.  
  22. function addShadows() {
  23.     ctx.shadowColor = "Grey"; // color
  24.     ctx.shadowBlur = 20;      // blur level
  25.     ctx.shadowOffsetX = 15;   // horizontal offset
  26.     ctx.shadowOffsetY = 15;   // vertical offset
  27. }
And here is the result:
unwanted shadow casted by the outline
Ah, indeed, the call to ctx.fill() casts a shadow, but the call to ctx.stroke(), that paints the whole path again, casts a shadow too, and this time the outline produces an unwanted shadow... How can we avoid this effect, while using the same technique for drawing the path?
The trick consists in saving the context before setting the shadow properties, then draw the filled circle, then restore the context (to its previous state: without shadows), then draw the outlined circle by callingctx.stroke().
Correct version of the code: http://jsbin.com/kedobi/2/edit
  1. ...
  2. // save the context before setting shadows and drawing the filled circle
  3.  ctx.save();
  4. // With path drawing you can change the context
  5. // properties until a call to stroke() or fill() is performed
  6.  ctx.fillStyle = "lightBlue";
  7. // add shadows before drawing the filled circle
  8.  addShadows();
  9. // Draws the filled circle in light blue
  10.  ctx.fill();
  11. // restore the context to its previous saved state
  12.  ctx.restore();
  13. ...
And here is the final result:
unwanted shadow disappeared

Styling lines

Several context properties can be used to set the thickness of the shape outlines, the way line end caps are drawn, etc.
They apply to all shapes that are drawn in path mode (lines, curves, arcs) and some apply also to rectangles.

LINE STYLE: CHANGE THE LINE THICKNESS

We have seen this before. This is done by changing the value (in pixels) of the lineWidth property of the context:
  1. ctx.lineWidth = 10; // set the thickness of every shape drawn in stroke/wireframe mode to 10 pixels
Here is a complete example where we draw with a lineWidth of 20 pixels. You can play with the complete interactive example here: http://jsbin.com/dacuco/2/edit
line width changed
Source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>A simple example of lineWidth property use</title>
  5. </head>
  6. <body>
  7.    <canvas id="myCanvas" width="500">
  8.        Your browser does not support the canvas tag.
  9.    </canvas>
  10. <script>
  11. var canvas = document.getElementById('myCanvas');
  12. var ctx = canvas.getContext('2d');
  13.  
  14. // first path
  15. ctx.moveTo(20, 20);
  16. ctx.lineTo(100, 100);
  17. ctx.lineTo(100, 0);
  18.  
  19.  
  20. // second part of the path
  21. ctx.moveTo(120, 20);
  22. ctx.lineTo(200, 100);
  23. ctx.lineTo(200, 0);
  24.  
  25. // indicate stroke color + draw first part of the path
  26. ctx.strokeStyle = "#0000FF";
  27. // Current line thickness is 20 pixels
  28. ctx.lineWidth = 20;
  29. ctx.stroke();        // draws the whole path at once
  30.  
  31. // Draws a rectangle in immediate mode
  32. ctx.strokeRect(230, 10, 100, 100);
  33. </script>
  34.  
  35. </body>
  36. </html>

LINE STYLE: CHANGING THE END CAPS FOR A LINE

The lineCap property of the context indicates the way line end caps are rendered. Possible values are butt(default), roundsquare (from top to bottom in the next illustration) . Note that a value of "round" or "square" makes the lines slightly longer than the default value "butt".
line cap values
Try the next example interactively:  http://jsbin.com/yaliya/2/edit
line cap values table
Source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.    <title>A simple example of lineCap property use</title>
  5. </head>
  6. <body>
  7.  
  8. <canvas id="myCanvas" width="500">
  9.     Your browser does not support the canvas tag.</canvas>
  10. <script>
  11. var canvas = document.getElementById('myCanvas');
  12. var ctx = canvas.getContext('2d');
  13.  
  14. // first path
  15. ctx.moveTo(20, 20);
  16. ctx.lineTo(100, 100);
  17. ctx.lineTo(100, 30);
  18.  
  19. // second part of the path
  20. ctx.moveTo(120, 20);
  21. ctx.lineTo(200, 100);
  22. ctx.lineTo(200, 30);
  23.  
  24. // indicate stroke color + draw first part of the path
  25. ctx.strokeStyle = "#0000FF";
  26. // Current line thickness is 20 pixels
  27. ctx.lineWidth = 20;
  28.  
  29. // Try different values : butt, square, round
  30. ctx.lineCap = "round";
  31.  
  32. ctx.stroke();
  33. // Draws a rectangle
  34. ctx.strokeRect(230, 10, 100, 100);
  35. </script>
  36.  
  37. </body>
  38. </html>
Note that in this example, the rectangle is not affected. It has no line ends visible - all its sides meet. However, the next property we're going to look at will have an effect on rectangles!

LINE STYLE: SETTING THE TYPE OF CORNER WHEN TWO LINES MEET

The lineJoin property of the context indicates the way corners are rendered, when two lines meet. Possible values are miter (the default) for creating sharp corners, round, or bevel for "cut corners".
Try the next example interactively: http://jsbin.com/dozida/2/edit
lineJoin value table
Source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>A simple example of lineJoin property use</title>
  5. </head>
  6. <body>
  7.  
  8. <canvas id="myCanvas" width="500">Your browser does not support the canvas tag.</canvas>
  9.  
  10. <script>
  11. var canvas = document.getElementById('myCanvas');
  12. var ctx = canvas.getContext('2d');
  13. // first path
  14. ctx.moveTo(20, 20);
  15. ctx.lineTo(100, 100);
  16. ctx.lineTo(100, 30);
  17.  
  18. // second part of the path
  19. ctx.moveTo(120, 20);
  20. ctx.lineTo(200, 100);
  21. ctx.lineTo(200, 30);
  22.  
  23. // indicate stroke color + draw first part of the path
  24. ctx.strokeStyle = "#0000FF";
  25. // Current line thickness is 20 pixels
  26. ctx.lineWidth = 20;
  27. // Try different values : miter(default), bevel, round
  28. ctx.lineJoin = "round";
  29. ctx.stroke();
  30. // Draws a rectangle
  31. ctx.strokeRect(230, 10, 100, 100);
  32. </script>
  33.  
  34. </body>
  35. </html>

LINE STYLE: SPECIFIC CASE OF LINEJOIN="MITER", THE MITERLIMIT PROPERTY, A WAY TO AVOID LOOOOOOONG CORNERS!

The miterLimit property value corresponds to the maximum miter length: the distance between the inner corner and the outer corner where two lines meet. When the angle of a corner between two lines gets smaller, the miter length grows and can become too long.
In order to avoid this situation, we can set the miterLimit property of the context to a threshold value. If the miter length exceeds the miterLimit value, then the corner will be rendered as if the lineJoin property had been set to "bevel" and the corner will be "cut".
miterLimit property shown with 3 different angles, we see that the part tha goes out of the angle can become very long
You can try an interactive example here: http://jsbin.com/bokusa/3/edit
In the example, try different values for the miterLimit property. You'll see that the way the corners are rendered changes at values around 2 and 3.
Source code:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>A simple example of miterLimit property use</title>
  5. </head>
  6. <body>
  7.  
  8. <canvas id="myCanvas" width="500">Your browser does not support the canvas tag.</canvas>
  9.  
  10. <script>
  11. var canvas = document.getElementById('myCanvas');
  12. var ctx = canvas.getContext('2d');
  13. // first path
  14. ctx.moveTo(20, 20);
  15. ctx.lineTo(100, 100);
  16. ctx.lineTo(100, 30);
  17.  
  18. // second part of the path
  19. ctx.moveTo(120, 20);
  20. ctx.lineTo(200, 100);
  21. ctx.lineTo(200, 30);
  22.  
  23. // indicate stroke color + draw first part of the path
  24. ctx.strokeStyle = "#0000FF";
  25. // Current line thickness is 20 pixels
  26. ctx.lineWidth = 20;
  27. // Try different values : miter(default), bevel, round
  28. ctx.lineJoin = "miter";
  29. // try to change this value, try 2, 3, 4, 5 et...
  30. ctx.miterLimit = 1;
  31. ctx.stroke();
  32.  
  33. // Draws a rectangle
  34. ctx.strokeRect(230, 10, 100, 100);
  35. </script>
  36. </body>
  37. </html>

No comments:

Post a Comment