Question

I'm writing an extension to the javascript context object.

What I want to achieve:

In the end, I want to have a function which takes only one argument, canvas. From there on it should extend that canvas element with all kinds of extra functionality that I will write myself.

So basically, turning a canvas into a special canvas as follows:

Html

<canvas id='normal'></canvas>
<canvas id='special'></canvas>

Javascript

var normal = document.getElementById("normal"),
    special = document.getElementById("special");

var Extendedctx = function (canvas) {
    // Magic?
}

Extendedctx(special); // Extends the "special" canvas's context (ctx) with lots of extra functionality.

special.getContext('2d').nonBuiltinFunction(); // Should work
normal.getContext('2d').nonBuiltinFunction(); // Should throw an exception

The normal canvas however, should stay exactly the same. So, extending the prototype of the builtin ctx-object is not a viable solution.

I'm trying to achieve this by doing the following:

var canvas = document.getElementById('testcanvas'),
    ctx = canvas.getContext('2d'),

    Extendedctx = function (canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.__proto__ = Object.create(this.ctx);
    return this
},  pad = new Extendedctx(canvas);

pad.fillRect(0, 0, 40, 40);

However, the following exception occurs at the last line:

Uncaught TypeError: Illegal invocation 

According to the following SO post quote, this should make sense:

In your code you are assigning a native method to a property of custom object. When you call support.animationFrame(function () {}) , it is executed in the context of current object (ie support). For the native requestAnimationFrame function to work properly, it must be executed in the context of window

Source

So, to fix that you'll have to call pad.fillRect.call(pad.ctx, 0, 0, 40, 40);

Now, this would get annoying and messy really quickly. What I want to end up with is that:

pad.fillRect(0, 0, 40, 40);

would simply work, as well as any other inherited function I might want to use (eg ctx.arc)

I really don't know what to do here, I tried googling something along the lines of 'altering functions calls in javascript' and such. No real useful results.

Here's the jsfiddle in case you need to fiddle around with the js

Was it helpful?

Solution 2

You could mixin the properties you want to add on the context.

var canvas = document.getElementById('testcanvas');

var ExtendedCtx = function(canvas) {
    var ctx = canvas.getContext('2d');
    ctx.newProp = 20;
    ctx.newMethod = function(){
        return this.newProp/2;
    }
    return ctx;
}

var pad = ExtendedCtx(canvas);

pad.fillRect(0, 0, 40, 40);
console.log(pad.newProp, pad.newMethod(), pad.canvas); // 20, 10, canvas element

OTHER TIPS

So what you're trying to do is create an object whose prototype is a 2d canvas context.

First, although it won't work, here's the correct way to do that in a general-purpose web page (e.g., one that can't rely on ES5 yet, because of IE8):

function getExtendedContext(canvas) {
    var ectx;

    // A one-off constructor function
    function ctor() { }

    // Set the object that will be assigned as a prototype
    // when using that ctor
    ctor.prototype = canvas.getContext("2d");

    // Create our object
    ectx = new ctor();

    // Add properties to it
    ectx.nonBuiltinFunction = function() { /* ... */ };

    // Return it
    return ectx;
}

// Usage
var ctx = getExtendedContext(document.getElementById("special"));
ctx.nonBuiltinFunction();
ctx.fillRect(0, 0, 40, 40);

[With ES5, we could use Object.create, but it can't be correctly shimmed on older browsers (and I strongly recommend not half-shimming it).]

Live Example | Live Source (but again, it doesn't work)

The problem is that the canvas object and its context objects are DOM objects, not JavaScript objects. DOM objects are what the ECMAScript specification calls "host" objects, and they are not required to act like JavaScript objects. The ECMAScript specification has various notes in it about this (and similarly for host-provided functions, like alert).

At least on Chrome, the 2d context object refuses to participate in JavaScript inheritance. When we call ctx.fillRect(0, 0, 40, 40); above, we get the exception that you mentioned in your question:

Uncaught TypeError: Illegal invocation

The reason is that fillRect checks to see if this (within the call) is a 2d context object, and it isn't — it's the object we created, which has the context as its prototype. So fillRect throws a TypeError, because this isn't a context.

This is unfortunate, because it makes doing what you're trying to do much harder. To create a true facade for a canvas element, or even just for a 2d context, would require creating a huge amount of code: You'd have to create an object that has all of the methods of a 2d context, and when those methods are called, it calls them on the real 2d context it wraps. Worse, because 2d contexts have writable properties, if you can't count on an ES5 environment, you'd have to set all of those properties on the underlying context before making any calls to it. Truly ugly.

So since we can't use inheritance, and using a facade would be really ugly, we need to look at extension: Actually extending the 2d context object itself, rather than using it as a prototype or wrapping it in a facade.

Chrome, at least, tolerates your adding properties to the 2d context object, so you can do this:

function getExtendedContext(canvas) {
    var ectx;

    ectx = canvas.getContext("2d");
    ectx.nonBuiltinFunction = function() { /* ... */ };

    return ectx;
}

Live Example | Live Source

Whether that will work on other browsers is something you'd have to test, because each host is free to do what it wants, including "sealing" host objects.

In your examples, you seemed to want to override the getContext on the HTMLCanvasElement object itself. Chrome allows you to do that:

function extendCanvas(canvas) {
    var realGetContext;

    // Save a reference to the real `getContext` function        
    realGetContext = canvas.getContext;

    // Replace it with our own
    canvas.getContext = function(type) {
        // Call the real one
        var ctx = realGetContext.apply(this, arguments);

        // If the request was for a 2d canvas, extend it
        if (type === "2d") {
            extend2dContext(ctx);
        }

        // Return it
        return ctx;
    };

    return canvas;
}

function extend2dContext(ctx) {
    ctx.nonBuiltinFunction = function() {
        this.fillStyle = "blue";
        this.fillRect(41, 41, 30, 30);
    };

    return ctx;
}

// Usage
var canvas = extendCanvas(document.getElementById("someCanvas"));
var ctx = canvas.getContext("2d");
ctx.nonBuiltinFunction();

Live Example | Live Source

...but whether other browsers will is, again, something that you'd have to test. If you run into an environment where you can't extend the objects like this, your choices are doing a full facade (which will be really ugly) or falling back on procedural programming (functions you pass the canvas into).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top