Skip to content

Commit

Permalink
feature: wrap renderer in a Proxy to expose Canvas2d context
Browse files Browse the repository at this point in the history
Ported over from public wasm repo: #324

We recently exposed `drawImage()` support from our `CanvasRenderer`, which naturally led to seeing if we can support the rest of the Canvas2D context APIs by approaching a similar strategy (simply adding the call to our deferred `drawList`).

We may be able to just return a `Proxy` from `makeRenderer()` which would intercept the calls to `CanvasRenderer` and route each API function call either to our `CanvasRenderer` if the API the user wants to call exists on it, OR the `._ctx` (the actual Canvas2D context) from `CanvasRenderer`. This should allow us to support our custom `save()`, `restore()`, `_drawImageMesh()`, etc. calls, while any non-overriden API we've wrapped in `CanvasRenderer` would go straight to the context itself.

From a consumer perspective, nothing should change here. They call `const renderer = rive.makeRenderer(canvas-element)` and use  `renderer.save()`, `renderer.restore()`, etc. in a render loop as expected.

Diffs=
65156867f feature: wrap renderer in a Proxy to expose Canvas2d context (#5491)

Co-authored-by: Zachary Plata <[email protected]>
  • Loading branch information
zplata and zplata committed Aug 16, 2023
1 parent d47e6de commit 780e9a5
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
adeebb26a7ac58a1aaa18a40ed9d98af46735c97
65156867f73322b56b31c610ed7dd97a0620dc02
2 changes: 1 addition & 1 deletion js/src/rive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ export class Rive {
private _layout: Layout;

// The runtime renderer
private renderer: rc.Renderer | rc.CanvasRenderer;
private renderer: rc.WrappedRenderer;

// Tracks if a Rive file is loaded
private loaded = false;
Expand Down
53 changes: 31 additions & 22 deletions js/src/rive_advanced.mjs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface RiveCanvas {
makeRenderer(
canvas: HTMLCanvasElement | OffscreenCanvas,
useOffscreenRenderer?: boolean
): CanvasRenderer | Renderer;
): WrappedRenderer;

/**
* Computes how the Rive is laid out onto the canvas
Expand Down Expand Up @@ -203,29 +203,38 @@ export declare class CanvasRenderer extends Renderer {
constructor(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
);

/**
* Canvas2D API for drawing an image onto a canvas
*/
drawImage: (
image:
| HTMLImageElement
| SVGImageElement
| HTMLVideoElement
| HTMLCanvasElement
| ImageBitmap
| OffscreenCanvas,
sx?: number,
sy?: number,
sWidth?: number,
sHeight?: number,
dx?: number,
dy?: number,
dWidth?: number,
dHeight?: number
) => void;
}

type OmittedCanvasRenderingContext2DMethods = "createConicGradient" |
"createImageData" |
"createLinearGradient" |
"createPattern" |
"createRadialGradient" |
"getContextAttributes" |
"getImageData" |
"getLineDash" |
"getTransform" |
"isContextLost" |
"isPointInPath" |
"isPointInStroke" |
"measureText";

/**
* Proxy class that handles calls to a CanvasRenderer instance and handles Rive-related rendering calls such
* as `save`, `restore`, `transform`, and more, effectively overriding and/or wrapping Canvas2D context
* APIs for Rive-specific purposes. Other calls not intentionally overridden are passed through to the
* Canvas2D context directly.
*
* Note: Currently, any calls to the Canvas2D context that you expect to return a value (i.e. `isPointInStroke()`)
* will return undefined
*/
export type CanvasRendererProxy = CanvasRenderer & Omit<CanvasRenderingContext2D, OmittedCanvasRenderingContext2DMethods>;

/**
* Renderer type for `makeRenderer()` that returns Renderer (webgl) or a CanvasRendererProxy (canvas2d)
*/
export type WrappedRenderer = Renderer | CanvasRendererProxy;

export declare class CanvasRenderPaint extends RenderPaint {
draw(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
Expand Down
3 changes: 2 additions & 1 deletion wasm/examples/centaur_game/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import RiveCanvas, {
Artboard,
SMIInput,
StateMachineInstance,
CanvasRendererProxy
} from "@rive-app/canvas-advanced-single";
import Centaur from "./centaur.riv";

Expand Down Expand Up @@ -133,7 +134,7 @@ async function main() {
window.onresize = computeSize;
computeSize();

const renderer = rive.makeRenderer(canvas);
const renderer = rive.makeRenderer(canvas) as CanvasRendererProxy;

// Game variables.
let currentMoveSpeed = 0;
Expand Down
57 changes: 50 additions & 7 deletions wasm/js/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ function makeMatrix(xx, xy, yx, yy, tx, ty) {
const VTX_ARRAY = 0;
const UV_ARRAY = 1;

// We'll allow calling methods on the c2d context via the Proxy returned by `.makeRenderer()`. This is a list of methods that are allowed.
const c2dMethodBlockList = [
"createConicGradient",
"createImageData",
"createLinearGradient",
"createPattern",
"createRadialGradient",
"getContextAttributes",
"getImageData",
"getLineDash",
"getTransform",
"isContextLost",
"isPointInPath",
"isPointInStroke",
"measureText",
];

// Used for rendering meshes.
const offscreenWebGL = new (function () {
let _gl = null;
Expand Down Expand Up @@ -655,12 +672,6 @@ Module["onRuntimeInitialized"] = function () {
paint["draw"].bind(paint, this._ctx, path._path2D, fillRule)
);
},
"drawImage": function (image, ...args) {
var ctx = this._ctx;
this._drawList.push(function () {
ctx["drawImage"](image, ...args);
});
},
"_drawRiveImage": function (image, blend, opacity) {
var img = image._image;
if (!img) {
Expand Down Expand Up @@ -820,7 +831,39 @@ Module["onRuntimeInitialized"] = function () {
}));

Module["makeRenderer"] = function (canvas) {
return new CanvasRenderer(canvas);
const newCanvasRenderer = new CanvasRenderer(canvas);
const c2dSource = newCanvasRenderer._ctx;
return new Proxy(newCanvasRenderer, {
get(target, property) {
if (typeof target[property] === "function") {
return function (...args) {
return target[property].apply(target, args);
};
} else if (typeof c2dSource[property] === "function") {
if (c2dMethodBlockList.indexOf(property) > -1) {
throw new Error(
"RiveException: Method call to '" +
property +
"()' is not allowed, as the renderer cannot immediately pass through the return \
values of any canvas 2d context methods."
);
} else {
return function (...args) {
newCanvasRenderer._drawList.push(
c2dSource[property].bind(c2dSource, ...args)
);
};
}
}
return target[property];
},
set(target, property, value) {
if (property in c2dSource) {
c2dSource[property] = value;
return true;
}
},
});
};

Module["renderFactory"] = {
Expand Down

0 comments on commit 780e9a5

Please sign in to comment.