A tool to automatically create test cases to aid <canvas> debugging
JavaScript HTML
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
README.md
canvas-interceptor.js
test.html

README.md

Canvas Interceptor

canvas-interceptor.js is a snippet to aid with debugging canvas issues, and designed to create reduced test cases.

Load canvas-interceptor.js before the web page uses the HTML5 canvas API. Then the all method calls and property invocations on canvas objects will be logged and stored in the __proxyLogs property on the canvas context. The final code to recreate the canvas can be generated by calling getCanvasReplay(canvas).

Example:

<script src="canvas-interceptor.js"></script>

<canvas id="mycanvas"></canvas>
<!-- ... a lot of canvas magic on #mycanvas... -->

<script>
var canvas = document.getElementById('mycanvas');
var code = getCanvasReplay(canvas);
// code can be copied to a new file to recreate the canvas,
// and used to e.g. create reduced test cases for canvas bugs.
// E.g. via the console: copy(code)
</script>

See test.html for more examples.

Usage

If you control the source code, loading the snippet via a <script> tag as in the above example should work as expected. But if you want to debug a third-party application, it is more convenient to paste something in the JavaScript console.

  1. Identify the first script in the page, and set a breakpoint at the start of the file.
  2. Reload the page. The browser will now trigger that breakpoint.
  3. Paste the following code:

    ;(function(){
    var x = new XMLHttpRequest;
    x.open('GET', 'https://robwu.nl/s/canvas-interceptor.js', false);
    x.send();
    window.eval(x.responseText.replace(/(["'])use strict\1/g, ''));
    })();
    
  4. Step out of the debugger.
  5. Now you can freely use the public methods (documented below) to debug canvas issues.

Reducing test cases

After calling getCanvasReplay(), you can end up with thousands lines of code. This code can be pasted to a file and used for debugging. Here are some tips to reduce the file, in order to pinpoint the bug:

  • Look for .save() and .restore() calls. These functions push and pop the canvas state in a stack-like way. Keep removing all lines between .save() up to the first .restore() call (without other .save() calls in between!) until the bug that you're trying to locate (dis)appears.
  • If you end up with extra .save() calls after removing all instructions between and including .save() and .restore(), just remove the remaining .save() calls. They are not going to be useful.
  • Remove independent and uninteresting drawings. E.g. a complete path (moveTo followed by a sequence of lineTo followed by closePath and stroke). Or e.g. a set of transformations that slightly alters the appearance.
  • Change some values (widths / transformations) to see the impact of a command on the whole drawing, and if the result is acceptable, cut a whole chunk of commands.

Browser compatibility

Run test.html to see whether the tool works as expected.

  • Chrome 43+ (not 42- because accessor descriptors were not prototypical, and data descriptors were not configurable)
  • Opera 31+ (same reason as Chrome).
  • Firefox 17+ (not 16- because accessor descriptors were inaccessible)
  • IE 10+ and Edge 0.11+ (IE9 and earlier doesn't support
  • Safari (not supported because accessor descriptors are not prototypical)

API documentation

When canvas-interceptor.js is loaded, it modifies the prototype of CanvasRenderingContext2D to intercept calls. The following APIs are exposed:

  • getCanvasReplay(HTMLCanvasElement|CanvasRenderingContext2D) - Get the code to recreate the state of the canvas from the logs.

  • clearCanvasLogs(HTMLCanvasElement|CanvasRenderingContext2D) - Reset the logs for the given canvas / context.

  • getCanvasName(CanvasRenderingContext2D) - Get the identifier of the context. If the context was not known yet, a new name will be generated and stored in the __proxyName property of the given parameter. To get the name for a given canvas, use getCanvasName(canvas.getContext('2d')).

  • CanvasRenderingContext2D.prototype.__proxyUnwrap - Disable canvas logging. To re-enable canvas logging, the page has to be refreshed. Existing logs are still preserved and getCanvasReplay will continue to work.

  • serializeArg(obj) - Get the string representation for the given object. Calling eval on the return value ought to reconstruct the original object. Not all objects can be serialized; if serialization is not supported, object literals with an inline comment is returned ({ /* ... */ }).

  • constructImageData(width, height, data) - Create a new ImageData object with a given width and height, initialized with data.

  • wrapObject(...) implements the logic of intercepting APIs. Do not use this method unless you know what you're doing (see the code for documentation).

If you need to use a canvas method without logging, retrieve the original property from .__proxyOriginal, and call the method (value) or setter/getter. Example: ctx.__proxyOriginal.scale.value.call(ctx, 1, 1) or ctx.__proxyOriginal.lineWidth.set.call(ctx, 1).