Awesome
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.
-
Identify the first script in the page, and set a breakpoint at the start of the file.
-
Reload the page. The browser will now trigger that breakpoint.
-
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, '')); })();
-
Step out of the debugger.
-
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 oflineTo
followed byclosePath
andstroke
). 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, usegetCanvasName(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 andgetCanvasReplay
will continue to work. -
serializeArg(obj)
- Get the string representation for the given object. Callingeval
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 newImageData
object with a given width and height, initialized withdata
. -
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)
.