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