Skip to content

Instantly share code, notes, and snippets.

@mikechambers
Last active May 7, 2022 11:07
Show Gist options
  • Save mikechambers/39d1e9f4253615e4008ef6d16c8bba12 to your computer and use it in GitHub Desktop.
Save mikechambers/39d1e9f4253615e4008ef6d16c8bba12 to your computer and use it in GitHub Desktop.
HTML5 CanvasRenderingContext2D Proxy
/*
Created by Mike Chambers
Copyright 2018
Released under an MIT License
https://github.com/mikechambers
ES6 JavaScript module and class that proxies,
captures and batches all canvas context 2d api
calls.
This can be useful if you want to transform the
output and calls into another format such as SVG.
To use, just pass the context from the canvas into
the constructor and make context calls on it as normal.
The second argument to the construtor is a boolean which
specifies whether the calls will be executed immediately
or batched until render is call (default is true)
calls will be batched until you call render.
import Context2D from "./context_proxy.js"
let ctx = canvas.getContext("2d);
let context = new Context2D(ctx, true);
context.beginPath();
context.arc(100, 100, 50, 0, Math.PI * 2);
context.stroke();
context.render();
You can also set:
context.debug = true;
to get some runtime
information sent to the console (currently just number
of calls / commands about to be run, as well as time it
took to make the calls).
Note there is room for performance optimizations, but I
wanted to share this as a general template in case
anyone else found it useful.
Please post any suggestions, fixes in the comments
*/
export default class Context {
constructor(context, capture = false) {
this._context = context;
this._commands = [];
this._capture = capture;
this._debug = false;
}
handleCommand(cmd) {
if (this._capture) {
this._commands.push(cmd);
} else {
cmd();
}
}
clearCommands() {
this._commands = [];
}
render() {
let start;
if (this._debug) {
start = Date.now();
console.log("-------------");
console.log(`RENDER_START : ${this._commands.length} commands`);
}
for (let command of this._commands) {
command();
}
this.clearCommands();
if (this._debug) {
console.log(`RENDER_COMPLETE : ${Date.now() - start}ms`);
}
}
set debug(value) {
this._debug = value;
}
get debug() {
return this._debug;
}
get canvas() {
return this._context.canvas;
}
/******* currentTransform*******/
set currentTransform(value) {
let c = () => {
this._context.currentTransform = value;
};
this.handleCommand(c);
}
get currentTransform() {
return this._context.currentTransform;
}
/******* direction*******/
set direction(value) {
let c = () => {
this._context.direction = value;
};
this.handleCommand(c);
}
get direction() {
return this._context.direction;
}
/******* fillStyle*******/
set fillStyle(value) {
let c = () => {
this._context.fillStyle = value;
};
this.handleCommand(c);
}
get fillStyle() {
return this._context.fillStyle;
}
/******* filter*******/
set filter(value) {
let c = () => {
this._context.filter = value;
};
this.handleCommand(c);
}
get filter() {
return this._context.filter;
}
/******* font*******/
set font(value) {
let c = () => {
this._context.font = value;
};
this.handleCommand(c);
}
get font() {
return this._context.font;
}
/******* globalAlpha*******/
set globalAlpha(value) {
let c = () => {
this._context.globalAlpha = value;
};
this.handleCommand(c);
}
get globalAlpha() {
return this._context.globalAlpha;
}
/******* globalCompositeOperation*******/
set globalCompositeOperation(value) {
let c = () => {
this._context.globalCompositeOperation = value;
};
this.handleCommand(c);
}
get globalCompositeOperation() {
return this._context.globalCompositeOperation;
}
/******* imageSmoothingEnabled*******/
set imageSmoothingEnabled(value) {
let c = () => {
this._context.imageSmoothingEnabled = value;
};
this.handleCommand(c);
}
get imageSmoothingEnabled() {
return this._context.imageSmoothingEnabled;
}
/******* imageSmoothingQuality*******/
set imageSmoothingQuality(value) {
let c = () => {
this._context.imageSmoothingQuality = value;
};
this.handleCommand(c);
}
get imageSmoothingQuality() {
return this._context.imageSmoothingQuality;
}
/******* lineCap*******/
set lineCap(value) {
let c = () => {
this._context.lineCap = value;
};
this.handleCommand(c);
}
get lineCap() {
return this._context.lineCap;
}
/******* lineDashOffset*******/
set lineDashOffset(value) {
let c = () => {
this._context.lineDashOffset = value;
};
this.handleCommand(c);
}
get lineDashOffset() {
return this._context.lineDashOffset;
}
/******* lineJoin*******/
set lineJoin(value) {
let c = () => {
this._context.lineJoin = value;
};
this.handleCommand(c);
}
get lineJoin() {
return this._context.lineJoin;
}
/******* lineWidth*******/
set lineWidth(value) {
let c = () => {
this._context.lineWidth = value;
};
this.handleCommand(c);
}
get lineWidth() {
return this._context.lineWidth;
}
/******* miterLimit*******/
set miterLimit(value) {
let c = () => {
this._context.miterLimit = value;
};
this.handleCommand(c);
}
get miterLimit() {
return this._context.miterLimit;
}
/******* shadowBlur*******/
set shadowBlur(value) {
let c = () => {
this._context.shadowBlur = value;
};
this.handleCommand(c);
}
get shadowBlur() {
return this._context.shadowBlur;
}
/******* shadowColor*******/
set shadowColor(value) {
let c = () => {
this._context.shadowColor = value;
};
this.handleCommand(c);
}
get shadowColor() {
return this._context.shadowColor;
}
/******* shadowOffsetX*******/
set shadowOffsetX(value) {
let c = () => {
this._context.shadowOffsetX = value;
};
this.handleCommand(c);
}
get shadowOffsetX() {
return this._context.shadowOffsetX;
}
/******* shadowOffsetY*******/
set shadowOffsetY(value) {
let c = () => {
this._context.shadowOffsetY = value;
};
this.handleCommand(c);
}
get shadowOffsetY() {
return this._context.shadowOffsetY;
}
set strokeStyle(value) {
let c = () => {
this._context.strokeStyle = value;
};
this.handleCommand(c);
}
get strokeStyle() {
return this._context.strokeStyle;
}
/******* textAlign*******/
set textAlign(value) {
let c = () => {
this._context.textAlign = value;
};
this.handleCommand(c);
}
get textAlign() {
return this._context.textAlign;
}
/******* textBaseline*******/
set textBaseline(value) {
let c = () => {
this._context.textBaseline = value;
};
this.handleCommand(c);
}
get textBaseline() {
return this._context.textBaseline;
}
addHitRegion(...args) {
let c = () => {
this._context.addHitRegion(...args);
};
this.handleCommand(c);
}
arc(...args) {
let c = () => {
this._context.arc(...args);
};
this.handleCommand(c);
}
arcTo(...args) {
let c = () => {
this._context.arcTo(...args);
};
this.handleCommand(c);
}
beginPath(...args) {
let c = () => {
this._context.beginPath(...args);
};
this.handleCommand(c);
}
bezierCurveTo(...args) {
let c = () => {
this._context.bezierCurveTo(...args);
};
this.handleCommand(c);
}
clearHitRegions(...args) {
let c = () => {
this._context.clearHitRegions(...args);
};
this.handleCommand(c);
}
clearRect(...args) {
let c = () => {
this._context.clearRect(...args);
};
this.handleCommand(c);
}
clip(...args) {
let c = () => {
this._context.clip(...args);
};
this.handleCommand(c);
}
closePath(...args) {
let c = () => {
this._context.closePath(...args);
};
this.handleCommand(c);
}
createImageData(...args) {
let c = () => {
this._context.createImageData(...args);
};
this.handleCommand(c);
}
createLinearGradient(...args) {
let c = () => {
this._context.createLinearGradient(...args);
};
this.handleCommand(c);
}
createPattern(...args) {
let c = () => {
this._context.createPattern(...args);
};
this.handleCommand(c);
}
createRadialGradient(...args) {
let c = () => {
this._context.createRadialGradient(...args);
};
this.handleCommand(c);
}
drawFocusIfNeeded(...args) {
let c = () => {
this._context.drawFocusIfNeeded(...args);
};
this.handleCommand(c);
}
drawImage(...args) {
let c = () => {
this._context.drawImage(...args);
};
this.handleCommand(c);
}
drawWidgetAsOnScreen(...args) {
let c = () => {
this._context.drawWidgetAsOnScreen(...args);
};
this.handleCommand(c);
}
drawWindow(...args) {
let c = () => {
this._context.drawWindow(...args);
};
this.handleCommand(c);
}
ellipse(...args) {
let c = () => {
this._context.ellipse(...args);
};
this.handleCommand(c);
}
fill(...args) {
let c = () => {
this._context.fill(...args);
};
this.handleCommand(c);
}
fillRect(...args) {
let c = () => {
this._context.fillRect(...args);
};
this.handleCommand(c);
}
fillText(...args) {
let c = () => {
this._context.fillText(...args);
};
this.handleCommand(c);
}
getImageData(...args) {
let c = () => {
this._context.getImageData(...args);
};
this.handleCommand(c);
}
getLineDash(...args) {
let c = () => {
this._context.getLineDash(...args);
};
this.handleCommand(c);
}
isPointInPath(...args) {
let c = () => {
this._context.isPointInPath(...args);
};
this.handleCommand(c);
}
isPointInStroke(...args) {
let c = () => {
this._context.isPointInStroke(...args);
};
this.handleCommand(c);
}
lineTo(...args) {
let c = () => {
this._context.lineTo(...args);
};
this.handleCommand(c);
}
measureText(...args) {
let c = () => {
this._context.measureText(...args);
};
this.handleCommand(c);
}
moveTo(...args) {
let c = () => {
this._context.moveTo(...args);
};
this.handleCommand(c);
}
putImageData(...args) {
let c = () => {
this._context.putImageData(...args);
};
this.handleCommand(c);
}
quadraticCurveTo(...args) {
let c = () => {
this._context.quadraticCurveTo(...args);
};
this.handleCommand(c);
}
rect(...args) {
let c = () => {
this._context.rect(...args);
};
this.handleCommand(c);
}
removeHitRegion(...args) {
let c = () => {
this._context.removeHitRegion(...args);
};
this.handleCommand(c);
}
resetTransform(...args) {
let c = () => {
this._context.resetTransform(...args);
};
this.handleCommand(c);
}
restore(...args) {
let c = () => {
this._context.restore(...args);
};
this.handleCommand(c);
}
rotate(...args) {
let c = () => {
this._context.rotate(...args);
};
this.handleCommand(c);
}
save(...args) {
let c = () => {
this._context.save(...args);
};
this.handleCommand(c);
}
scale(...args) {
let c = () => {
this._context.scale(...args);
};
this.handleCommand(c);
}
scrollPathIntoView(...args) {
let c = () => {
this._context.scrollPathIntoView(...args);
};
this.handleCommand(c);
}
setLineDash(...args) {
let c = () => {
this._context.setLineDash(...args);
};
this.handleCommand(c);
}
setTransform(...args) {
let c = () => {
this._context.setTransform(...args);
};
this.handleCommand(c);
}
stroke(...args) {
let c = () => {
this._context.stroke(...args);
};
this.handleCommand(c);
}
strokeRect(...args) {
let c = () => {
this._context.strokeRect(...args);
};
this.handleCommand(c);
}
strokeText(...args) {
let c = () => {
this._context.strokeText(...args);
};
this.handleCommand(c);
}
transform(...args) {
let c = () => {
this._context.transform(...args);
};
this.handleCommand(c);
}
translate(...args) {
let c = () => {
this._context.translate(...args);
};
this.handleCommand(c);
}
}
@mvanga
Copy link

mvanga commented Jun 10, 2021

Nice code! I do think there's a bug in there though: if you're in capture mode and call a getter immediately after a setter, without a render call in between, you'll see the incorrect values.

One way to fix it would be to keep track of what properties are dirty along with their new values. If the property being requested via a getter is dirty, return the modified value instead.

Or perhasp setting of canvas properties can be made synchronous and immediate rather than returning functions? Only method calls would then be deferred.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment