Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor MultiCanvas #113

Merged
merged 2 commits into from
Jul 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ipycanvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ def set_line_dash(self, segments):
# Image methods
def draw_image(self, image, x=0, y=0, width=None, height=None):
"""Draw an ``image`` on the Canvas at the coordinates (``x``, ``y``) and scale it to (``width``, ``height``)."""
if (not isinstance(image, (Canvas, Image))):
raise TypeError('The image argument should be an Image widget or a Canvas widget')
if (not isinstance(image, (Canvas, MultiCanvas, Image))):
raise TypeError('The image argument should be an Image, a Canvas or a MultiCanvas widget')

if width is not None and height is None:
height = width
Expand Down
127 changes: 72 additions & 55 deletions src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Distributed under the terms of the Modified BSD License.

import {
DOMWidgetModel, DOMWidgetView, ISerializers, Dict, ViewList, unpack_models
DOMWidgetModel, DOMWidgetView, ISerializers, Dict, unpack_models
} from '@jupyter-widgets/base';

import {
Expand Down Expand Up @@ -219,7 +219,7 @@ class CanvasModel extends DOMWidgetModel {

const image = await unpack_models(serializedImage, this.widget_manager);

if (image instanceof CanvasModel) {
if (image instanceof CanvasModel || image instanceof MultiCanvasModel) {
this._drawImage(image.canvas, x, y, width, height);
return;
}
Expand Down Expand Up @@ -431,6 +431,8 @@ class MultiCanvasModel extends DOMWidgetModel {
_canvases: [],
sync_image_data: false,
image_data: null,
width: 700,
height: 500,
};
}

Expand All @@ -445,45 +447,74 @@ class MultiCanvasModel extends DOMWidgetModel {
initialize(attributes: any, options: any) {
super.initialize(attributes, options);

this.on('change:_canvases', this.updateListeners.bind(this));
this.canvas = document.createElement('canvas');
this.ctx = getContext(this.canvas);

this.resizeCanvas();

this.on_some_change(['width', 'height'], this.resizeCanvas, this);
this.on('change:_canvases', this.updateCanvasModels.bind(this));
this.on('change:sync_image_data', this.syncImageData.bind(this));
this.updateListeners();

this.updateCanvasModels();
}

private updateListeners() {
get canvasModels(): CanvasModel[] {
return this.get('_canvases');
}

private updateCanvasModels() {
// TODO: Remove old listeners
for (const canvasModel of this.get('_canvases')) {
canvasModel.on('new-frame', this.syncImageData, this);
for (const canvasModel of this.canvasModels) {
canvasModel.on('new-frame', this.updateCanvas, this);
}

this.updateCanvas();
}

private async syncImageData() {
if (!this.get('sync_image_data')) {
return;
private updateCanvas() {
this.ctx.clearRect(0, 0, this.get('width'), this.get('height'));

for (const canvasModel of this.canvasModels) {
this.ctx.drawImage(canvasModel.canvas, 0, 0);
}

// Draw on a temporary off-screen canvas.
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = this.get('width');
offscreenCanvas.height = this.get('height');
const ctx = getContext(offscreenCanvas);
this.forEachView((view: MultiCanvasView) => {
view.updateCanvas();
});

for (const canvasModel of this.get('_canvases')) {
ctx.drawImage(canvasModel.canvas, 0, 0);
this.syncImageData();
}

// Also update the sub-canvas image-data
const bytes = await toBytes(canvasModel.canvas);
private resizeCanvas() {
this.canvas.setAttribute('width', this.get('width'));
this.canvas.setAttribute('height', this.get('height'));
}

canvasModel.set('image_data', bytes);
canvasModel.save_changes();
private async syncImageData() {
if (!this.get('sync_image_data')) {
return;
}

const bytes = await toBytes(offscreenCanvas);
const bytes = await toBytes(this.canvas);

this.set('image_data', bytes);
this.save_changes();
}

private forEachView(callback: (view: MultiCanvasView) => void) {
for (const view_id in this.views) {
this.views[view_id].then((view: MultiCanvasView) => {
callback(view);
});
}
}

canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;

views: Dict<Promise<MultiCanvasView>>;

static model_name = 'MultiCanvasModel';
static model_module = MODULE_NAME;
static model_module_version = MODULE_VERSION;
Expand All @@ -496,48 +527,34 @@ class MultiCanvasModel extends DOMWidgetModel {
export
class MultiCanvasView extends DOMWidgetView {
render() {
this.container = document.createElement('div');
this.container.style.position = 'relative';

this.el.appendChild(this.container);
this.ctx = getContext(this.el);

this.canvas_views = new ViewList<CanvasView>(this.createCanvasView, this.removeCanvasView, this);
this.updateCanvasViews();
this.resizeCanvas();
this.model.on_some_change(['width', 'height'], this.resizeCanvas, this);

this.model.on('change:_canvases', this.updateCanvasViews.bind(this));
this.updateCanvas();
}

private updateCanvasViews() {
this.canvas_views.update(this.model.get('_canvases'));
clear() {
this.ctx.clearRect(0, 0, this.el.width, this.el.height);
}

private createCanvasView(canvasModel: CanvasModel, index: number) {
// The following ts-ignore is needed due to ipywidgets's implementation
// @ts-ignore
return this.create_child_view(canvasModel).then((canvasView: CanvasView) => {
const canvasContainer = document.createElement('div');

canvasContainer.style.zIndex = index.toString();

if (index == 0) {
// This will enforce the container to respect the children size.
canvasContainer.style.position = 'relative';
canvasContainer.style.float = 'left';
} else {
canvasContainer.style.position = 'absolute';
}

canvasContainer.appendChild(canvasView.el);
this.container.appendChild(canvasContainer);
updateCanvas() {
this.clear();
this.ctx.drawImage(this.model.canvas, 0, 0);
}

return canvasView;
});
private resizeCanvas() {
this.el.setAttribute('width', this.model.get('width'));
this.el.setAttribute('height', this.model.get('height'));
}

private removeCanvasView(canvasView: CanvasView) {
this.container.removeChild(canvasView.el);
get tagName(): string {
return 'canvas';
}

private container: HTMLDivElement;
private canvas_views: ViewList<CanvasView>;
el: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;

model: MultiCanvasModel;
}