2

I'm a Ruby developer who finally decided to learn JavaScript seriously. So I purchased some books and I started to dive in, but I got stuck quickly when I tried to understand prototypal inheritance...

One of the examples of the book is the following. Given a Shape which prototype has a draw method, and two child shapes: a Triangle and a Rectangle which prototype inherit from Shape;

  • when I call the draw function on Triangle and Rectangle instances the method will draw them properly.
  • when I add a second method to show their name, every instance will log it properly.

Everything was understandable perfectly until I added a third method to fill the shapes... And only the last one get filled. no matter which one I call. Why? Is there something special in canvas?

Here is the code of the exercise:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

function Shape() {
  this.points = [];
  this.init();
}

Shape.prototype = {
  constructor: Shape,
  init: function() {
    if (this.context === undefined) {
      Shape.prototype.context = document.getElementById('canvas').getContext('2d');
    };
    if (this.name === undefined) {
      Shape.prototype.name = 'generic shape'
    }
  },
  draw: function() {
    var i, ctx = this.context;
    ctx.strokeStyle = 'rgb(0,0,255)';
    ctx.beginPath();
    ctx.moveTo(this.points[0].x, this.points[0].y);
    for (i = 1; i < this.points.length; i++) {
      ctx.lineTo(this.points[i].x, this.points[i].y);
    }
    ctx.closePath();
    ctx.stroke();
  },
  fill: function(color) {
    var ctx = this.context;
    ctx.fillStyle = color;
    ctx.fill();
  },
  say_name: function() {
    console.log('Hello my name is ' + this.name)
  }
};

function Triangle(a, b, c) {
  this.points = [a, b, c];
  this.name = 'Triangle'
  this.context = document.getElementById('canvas').getContext('2d');
}

function Rectangle(side_a, side_b) {
  var p = new Point(200, 200);
  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.name = 'Rectangle'
  this.context = document.getElementById('canvas').getContext('2d');
}

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

function testTriangle() {
  var p1 = new Point(100, 100);
  var p2 = new Point(300, 100);
  var p3 = new Point(200, 0);
  return new Triangle(p1, p2, p3);
}

function testRectangle() {
  return new Rectangle(100, 100);
}

function make_me_crazy() {
  var t = testTriangle();
  var r = testRectangle();
  t.draw();
  r.draw();
  t.say_name();
  r.say_name();
  t.fill('red');
}
make_me_crazy();
<canvas height='600' width='800' id='canvas' />

Thank you!

More details:

  • Why the function say_name is working exactly I expect saying: 'I am a triangle' or 'I am a rectangle' and never 'I am a generic shape', but the fill function fills the rectangle despite I'm calling it on a triangle instance? As people rightly answered to flip the two draw functions calls, I would specify better the following. The problem is not about the color of a shape, but the context pointer. why only the last shape is filled? If I add more shapes before calling fill only the last one get filled. This means I'm doing something wrong referring to the canvas. I supposed it was "the place where I draw shapes" but it seems more like "the last active shape"
  • How can I fix that code to make it working correctly filling the shape I want whenever I want? I mean. what if I want to have a function which receive an instance of a particular shape and fills it?
  • Is there any way to access a the draws contained into a canvas?
2
  • Within your make_me_crazy function, switch your draw calls (draw the rectangle first, then the triangle). The result of that should tell you something about context! Commented Feb 10, 2017 at 14:09
  • Yes, you are right. I supposed the context to be the "shape I'm working on" but is not.. is... the canvas? If I add a third shape, the fill method fills it. So I deduce the context is the last active shape, no matter which one calls the fill method.. but why? How can properly refer to a shape in any moment? Commented Feb 10, 2017 at 14:14

2 Answers 2

1

The core of the problem is the context - your shapes are sharing the single context of the canvas, and therefore it is not straight-forward to flip back and forth between objects. Instead, think of your order-of-operations as handling a single shape at a time and only moving on to the next one when you are done with the former.

Note the order of calls in the make_me_crazy function:

function Point(x, y) {
    this.x = x;
    this.y = y;
  }

  function Shape() {
    this.points = [];
    this.init();
  }

  Shape.prototype = {
    constructor: Shape,
    init: function(){
      if (this.context === undefined) {
        Shape.prototype.context = document.getElementById('canvas').getContext('2d');
      };
      if(this.name === undefined){
        Shape.prototype.name = 'generic shape'
      }
    },
    draw: function(){
      var i, ctx = this.context;
      ctx.strokeStyle = 'rgb(0,0,255)';
      ctx.beginPath();
      ctx.moveTo(this.points[0].x, this.points[0].y);
      for (i = 1; i<this.points.length; i++) {
        ctx.lineTo(this.points[i].x, this.points[i].y);
      }
      ctx.closePath();
      ctx.stroke();
    },
    fill: function(color){
      var ctx = this.context;
      ctx.fillStyle = color;
      ctx.fill();
    },
    say_name: function(){console.log('Hello my name is '+ this.name)}
  };

  function Triangle(a,b,c){
    this.points = [a, b, c];
    this.name = 'Triangle'
    this.context = document.getElementById('canvas').getContext('2d');
  }

  function Rectangle(side_a, side_b){
    var p = new Point(200, 200);
    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.name = 'Rectangle'
    this.context = document.getElementById('canvas').getContext('2d');
  }

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

  function testTriangle(){
    var p1 = new Point(100, 100);
    var p2 = new Point(300, 100);
    var p3 = new Point(200, 0);
    return new Triangle(p1, p2, p3);
  }

  function testRectangle(){
    return new Rectangle(100, 100);
  }

  function make_me_crazy(){
    var t = testTriangle();
    t.say_name();
    t.draw();
    t.fill('red');
    
    var r = testRectangle();
    r.draw();
    r.say_name();
  }
  make_me_crazy();
<canvas height='600' width='800' id='canvas'></canvas>

Sign up to request clarification or add additional context in comments.

1 Comment

Right too. I have made me to rethink to my questions to find a better way to express my doubt. Thank you!
0

About the points of your question.

For the first one: the key is this line of code

if(this.name === undefined){
    Shape.prototype.name = 'generic shape'
}

When you instantiate Rectangle and Triangle, both of them set name. In the other hand, the render method is only available in the Shape prototype.

About the second point (and the third one): Maybe are you painting the Rectangle over the Triangle. Try to switch the order of the draw calls to check it.

2 Comments

Render method? mmhh maybe that's the point, as I use the same canvas for all the shapes maybe I should "reset" the context to the shape I want to fill... but how?
And, no, the two shapes don't overlap. (and if I add other shapes the fill method always fills the last one)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.