Draw me some renderables!
This blog is a follow-up on my previous blog about "Class"-inheritance in JavaScript. I wanted to utilize the features in HTML5 Canvas and keep the work as object oriented as possible. In this post, I do not follow the MVC-pattern, however one could easily split the classes in models, views and controllers.
In this post, I want to document my experiment with much code, basic explanation and little depth. I wonder if I manage to succeed on the explanation and depth parts... This will be a long post!
What will be drawn?
In this project I wanted to draw simple shapes onto a canvas. Some of these shapes should have a fixed animation (rotate, move, bounce) and some shapes should be positioned as the user desires. All shapes should have a shadow, a stroke and a fill color. Drawing text should be possible as well, to display a message to the user which tells them they can move their mouse or use their keyboard to interact.
Inheritance
I do however use the principle I discussed in my previous post. I create a base class for renderable objects and keep extending on those. This way, I could easily add more shapes and text to the view, which so happens to be a canvas.
That's also where this project began, making a library which contained the little bit of code required to extend classes more easily. I added an extend function the the Function prototype which allowed me to call .extend on any function, passing on a new constructor function. This library also adds a notImplemented function to the Object prototype. This added function allows superclasses to define functions with a default body that should throw an exception.
;(function() {
'use strict';
// Add a notImplemented method to Object, so that superclasses can call it to throw an error when
// a method is called that is not implemented.
if(!("notImplemented" in Object.prototype) || typeof(Object.prototype.notImplemented) !== "function") {
var NotImplementedException = function NotImplementedException(message) {
this.name = "NotImplementedException";
this.message = message || "This method is not implemented."
};
Object.prototype.notImplemented = function(message) {
throw new NotImplementedException(message);
};
}
// The extend function, which is also wrapped in a method in Function.prototype
function extend(original, ctor) {
ctor.prototype = Object.create(original.prototype);
ctor.parent = original;
ctor.prototype.constructor = ctor;
return ctor;
};
// A method for all Functions allowing simple extending
if(!('extend' in Function.prototype) || typeof(Function.prototype.extend) !== "function") {
Function.prototype.extend = function(ctor) {
return extend(this, ctor);
};
}
}());
Monitoring input
I wanted to add user interaction to my animated canvas, however I wanted to keep the whole deal as OO as possible. I decided to create two Monitor classes which do not implement any superclass. When an instance is made from one of these classes, the object should keep track of information by itself without interfering with other objects.
KeyDownMonitor
The first one simply keeps track of which keys are currently pressed down, and any other object may then test a keycode against its state to see whether a key is down or not.
// An object of this class keeps track of which keys are currently being pressed
;(function(root) {
'use strict';
var KeyDownMonitor = function() {
this.keysDown = {};
var that = this;
window.addEventListener('keydown', function(e) {
that.keysDown[e.which || e.keyCode] = true;
});
window.addEventListener('keyup', function(e) {
that.keysDown[e.which || e.keyCode] = false;
});
};
KeyDownMonitor.prototype.isKeyDown = function(code) {
return this.keysDown[code];
};
root.KeyDownMonitor = KeyDownMonitor;
}(window));
MousePositionMonitor
The next one will simply keep track of the X and Y coordinate of the mouse cursor and store it in an object.
// An object of this class keeps track of the mouse position via window.onmousemove.
;(function(root) {
'use strict';
var MousePositionMonitor = function() {
this.position = {x: 0, y: 0};
var that = this;
window.addEventListener('mousemove', function(event) {
var dot, eventDoc, doc, body, pageX, pageY;
event = event || window.event; // IE-ism
// If pageX/Y aren't available and clientX/Y are,
// calculate pageX/Y - logic taken from jQuery.
// (This is to support old IE)
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0 );
}
that.position.x = event.pageX;
that.position.y = event.pageY;
});
};
MousePositionMonitor.prototype.getMousePosition = function() {
return this.position;
};
root.MousePositionMonitor = MousePositionMonitor;
}(window));
Renderable
Now comes the superclass that should be implemented by all classes that produce renderable objects, Renderable. This class does not contain many properties and methods, but a few that truly matter in this scenario. Each child of Renderable should implement a step() method (controller logic) and a render method (view logic). Each child of renderable may add any property (model logic) however should maintain the position property.
/**
The base class for all renderable classes. Each renderable shape, object,
image, text etc should extend this class for clarity.
*/
;(function(root) {
'use strict';
// Renderable constructor, very basic.
var Renderable = function(x, y) {
this.position = {
x: x || 0,
y: y || 0
};
};
// Each renderable should implement render, duh!
Renderable.prototype.render = function(ctx) {
// Object.prototype.notImplemented is defined in Inheritance.js
this.notImplemented('render() is not implemented.');
};
// Each renderable should implement step for model logic, but it may be an empty function.
Renderable.prototype.step = function(canvas) {
this.notImplemented('step() is not implemented.');
};
root.Renderable = Renderable;
}(window));
Canvas
Considering I have the base of the renderable parts set up, I now want to wrap up my logic specific to the canvas element in a library. This Canvas class will create an object which holds information about the state such as the canvas element, the 2d context and a list of renderable objects. It also checks if the browser supports the HTML5 canvas element. Another feature of the Canvas class is rendering FPS onto the ctx, which can be turned off through a boolean property. The step() method calls all the step() methods on all known renderables, and the render() method calls all the render() methods on all known renderables.
This library is a bit too large to post in the post, so I'll just link to it: CodePen > Canvas.js.
Shape
We are nearly at the point of actually having enough usable code to start rendering things onto a canvas. When we are at said point, expanding it with more shapes and renderables will be easy!
We first need a baseclass for all shapes. This class should implement Renderable and it should be inherited by all shaped objects (squares, circles, triangles etc.). Shape is a great name!
Shape extends Renderable by using the extend method I added earlier and sets a new constructor that immediately calls the parent constructor (so, Renderable's constructor). The parent constructor will make sure the position property is set and the Shape constructor will add a dimension property and a style property.
Shape contains a few methods to relativelty and absolutely resize and move objects. Any class extending Shape should still implement step() and render(), because Shape does not have these methods.
Once again, a link to the source: Shape.js
SquareBox, implementations yay!
We can finally create a class that actually represents a physical shape now we have all our base classes and helpers. We'll start off with a simple square box, which accepts x and y positions and a size, which represents both width and height. This class implements Shape and has to implement a step() and render() method, even though step might do absolutely nothing. The render() method will receive a reference to the context object, so it can immediately start drawing!
// Just an extension of Shape
;(function(root) {
'use strict';
// Simply call the parent constructor and provide size for both width and height
var SquareBox = Shape.extend(function SquareBox(x, y, size) {
SquareBox.parent.call(this, x, y, size, size);
});
// override getSize, as Size might be specific to what shape you're working with.
SquareBox.prototype.getSize = function() {
return this.dimension.x; // x == y in a SquareBox.
};
// render a box with a fill color and a line!
SquareBox.prototype.render = function(ctx) {
ctx.fillStyle = this.style.fill;
ctx.fillRect(this.position.x, this.position.y, this.dimension.x, this.dimension.y);
ctx.lineWidth = 1;
ctx.strokeStyle = this.style.stroke;
ctx.strokeRect(this.position.x, this.position.y, this.dimension.x, this.dimension.y);
};
SquareBox.prototype.step = function(canvas) {
// does nothing by default.
};
root.SquareBox = SquareBox;
}(window));
Implementing and finally rendering
So now we have added enough to actually start seeing a result in our precious modern browser which supports the canvas element and its API. Here's a pen that creates an instance to Canvas, creates two instances to the monitors and two SquareBox instances. The animation loop then steps and renders all renderables through the canvas instance.
Check out this pen.
Step 2: From static to animated
By adding HorizontalMovingSquareBox.js and VerticalMovingSquareBox.js to the project, the SquareBox instances are very easily replaced with animating SquareBoxes. These two classes extend SquareBox and do actually implement the step() method to animate the shape and ensure it remains within the viewport.
Check out this pen.
Step 3: Go absolutely mad!
I'm guessing you will get the drift now, each renderable object has a step() and render() method, so all classes implementing Renderable will be able to do whatever they want considering their step() and render() methods will be called at about 60 frames per second. So I now added all the scripts into one pen and went crazy... Here's all the libraries (which should be merged and minified when used), including a polyfill for a few things: Canvas-OOP-Code.zip. You can find the implementation code in the pen below.
Check out this pen.
Thanks!
Thank you for sticking with me until the end of this post (unless you just skimmed it and scrolled down :( ). It probably was a long read, but I hope a somewhat interesting one!
End of transmission.