Tuesday, April 27, 2010

Case Study - Drawing - Inheritance (extract from Object Oriented JavaScript)

Case Study: Drawing Shapes

Let's finish off this chapter with a more practical example of using inheritance (check demo).

The task is to be able to calculate the area and the perimeter of different shapes, as well as to draw them, while reusing as much code as possible.

Explanation:
Let's have one Shape constructor that contains all of the common parts. From there, we can have Triangle, Rectangle and Square constructors, all inheriting from Shape. A square is really a rectangle with same-length sides, so let's reuse Rectangle when building the Square.

In order to define a shape, we'll use points with x and y coordinates.

A generic shape can have any number of points. A triangle is defined with three points, a rectangle (to keep it simpler)—with one point and the lengths of the sides.

The perimeter of any shape is the sum of its sides' lengths.

The area is shape-specific and will be implemented by each shape.
The common functionality in Shape would be:

A draw() method that can draw any shape given the points
A getParameter() method
A property that contains an array of points

Other methods and properties as needed

Let's have two other helper constructors: Point and Line.

- Point will help when defining shapes;
- Line will ease some calculations, as it can give the length of the line connecting any two given points. (using the Pythagorean Theorem: a2 + b2 = c2 (imagine a right -angled triangle where the hypotenuse connects the two given points)).


The example is very well explained in the book (Object Oriented JavaScript), so there is no point to go line by line here.

There are couple of interesting points:

1)
The last child constructor is Square. A square is a special case of a rectangle,
so it makes sense to reuse Rectangle. The easiest thing to do here is to borrow
the constructor.

  function Square(p, side){
      Rectangle.call(this, p, side, side);
  }

2)
Now that we have all constructors, let's take care of inheritance. Any pseudo-classical pattern (one that works with constructors as opposed to objects) will do.

Let's try using a modified and simplified version of the prototype-chaining pattern (the first method described in this chapter).

This pattern calls for creating a new instance of the parent and setting it as the child's prototype.

In this case, it's not necessary to have a new instance for each child—they can all share it.

   (function () {
 
      var s = new Shape();
  
      Triangle.prototype = s;
 
      Rectangle.prototype = s;
 
      Square.prototype = s;
    })();


3)
When something is written nicely like this, it very easy to add functionality. It took me couple of minutes to add fill color methods for the shapes. (CSS - webkit animation - Safari)

Here is demo.


Too many javascript code are tangled, unreadable, you can really forget about reusing it.

Here is code, also you can see it under source on demo:

 
 function Point(x, y) {
  this.x = x;
  this.y = y;
 }
 
 function Line(p1, p2) {
  this.p1 = p1;
  this.p2 = p2;
  this.length = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
 }
 
 function Shape() {
  this.points = [];
  this.lines = [];
  this.init();
 }
 
 Shape.prototype = {
 //reset pointer to constructor
 constructor: Shape,
 //initialization, sets this.context to point
 //to the context of the canvas object
 init: function() {
  if(typeof this.context === 'undefined') {
   var canvas = document.getElementById('canvas');
   Shape.prototype.context = canvas.getContext('2d');
  }
 },
 //method that draws a shape by looping through this.points
 draw: function() {
  var ctx = this.context;
  ctx.strokeStyle = this.getColor();
  ctx.beginPath();
  ctx.moveTo(this.points[0].x, this.points[0].y);
  for(var i = 1; i < this.points.length; i++) {
   ctx.lineTo(this.points[i].x, this.points[i].y);
  }
  ctx.closePath();
  ctx.stroke();
 },
 //method that generates a random color
 getColor: function() {
  var rgb = [];
  for(var i = 0; i < 3; i++) {
   rgb[i] = Math.round(255 * Math.random());
  } 
  return 'rgb(' + rgb.join(',') + ')';
 },
 setFillColor: function(colorArray) {
  var ctx_f = this.context;
  var colorArray = colorArray || [100, 100, 100]; //providing default if called without color
  var rgb = [];
  for(var i =0; i < colorArray.length; i++) {
   rgb[i] = colorArray[i];
  }
  ctx_f.fillStyle = 'rgb(' + rgb.join(',') + ')';
  ctx_f.fill();
 },
 setFillAlphaColor: function(colorArray) {
  var ctx_f = this.context;
  var colorArray = colorArray || [100, 100, 100, 0.5]; //providing default if called without color
  var rgb = [];
  for(var i =0; i < colorArray.length; i++) {
   rgb[i] = colorArray[i];
  }
  ctx_f.fillStyle = 'rgba(' + rgb.join(',') + ')';
  ctx_f.fill();
 },
 //method that loops through the points array, 
 //creates Line instances and adds them to this.lines
 getLines: function() {
  if(this.lines.length > 0) {
   return this.lines;
  }
  var lines = [];
  for(var i = 0; i < this.points.length; i++) {
   lines[i] = new Line(this.points[i], (this.points[i+1]) ? this.points[i+1] : this.points[0]);
  }
  this.lines = lines;
  return lines;
 },
 //shell method, to be implemented by children
 getArea: function() {},
  //sum the lengths of all lines
 getPerimeter: function() { 
  var lines = this.getLines();
  var perim = 0;
  for(var i = 0; i < lines.length; i++) {
   perim += lines[i].length;
  }
  return perim;
 }
 
}
 
//Now the children constructors: Triangle first

function Triangle(a, b, c) {
 this.points = [a, b, c];
 this.getArea = function() {
  var p = this.getPerimeter();
  var s = p /2;
  return Math.sqrt(
   s * (s - this.lines[0].length) * (s - this.lines[1].length) * (s - this.lines[2].length)
  );
 };
}

//next comes: Rectangle constructor:

function Rectangle(p, side_a, side_b) {
 this.points = [
  p,
  new Point(p.x + side_a, p.y),  //top right
  new Point(p.x + side_a, p.y+side_b), //bottom right
  new Point(p.x, p.y + side_b) //bottom left
 ];
 this.getArea = function() { return side_a * side_b;};
} 

//last child constructor is Square: 

function Square(p, side) {
 Rectangle.call(this, p, side, side);
}


(function() {
  var s = new Shape();
  Triangle.prototype = s;
  Rectangle.prototype = s;
  Square.prototype = s; 
 
 })();


Testing part:
//Testing: 
 var p1 = new Point(100, 100);
 var p2 = new Point(300, 100);
 var p3 = new Point(200, 0);
 
//order of shapes important, so big one doesn't hide small one.  
 
//chimney  
var chim = new Rectangle(new Point(120, 40), 20, 40); 
chim.draw();
chim.getPerimeter();
chim.setFillColor([169, 27, 53]);
 
 //now you can create triangle by passing the three point to the Triangle constructor:
 //roof body
 var t = new Triangle(p1, p2 , p3);
 t.draw();
 t.getPerimeter();
 t.setFillColor([149, 38, 23]);
 
 //house body
 var hsbd = new Square(p1, 200);
 hsbd.draw();
 hsbd.setFillColor([149, 93, 23]);
 
 //door
 var r = new Rectangle(new Point(180, 200), 50, 100);
 r.draw();
 r.getArea();
 r.getPerimeter()
 r.setFillColor([110, 80, 60]);
 
 //left window
 var s = new Square(new Point(130, 130), 50);
 s.draw()
 s.getArea();
 s.getPerimeter()
 s.setFillAlphaColor([145, 190, 196, 0.7]);
 
 //right window
 var s2 = new Square(new Point(230, 130), 50);
 s2.draw()
 s2.getArea();
 s2.getPerimeter()
 s2.setFillAlphaColor([145, 190, 230, 0.4]);

No comments:

Post a Comment