Skip to content

Commit ad41cfd

Browse files
committed
Add support for caching commands Python side
1 parent b5b16bc commit ad41cfd

File tree

6 files changed

+177
-42
lines changed

6 files changed

+177
-42
lines changed

examples/introduction.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
"metadata": {},
5050
"outputs": [],
5151
"source": [
52-
"c = ipycanvas.Canvas(stroke_style='red', size=(200, 200))\n",
52+
"c = ipycanvas.Canvas(size=(200, 200))\n",
53+
"\n",
54+
"c.stroke_style = 'red'\n",
5355
"\n",
5456
"# Draw smiley face\n",
5557
"c.begin_path()\n",

examples/numpy_heatmap.ipynb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import numpy as np\n",
10+
"from ipycanvas import Canvas"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"# make these smaller to increase the resolution\n",
20+
"dx, dy = 0.05, 0.05\n",
21+
"\n",
22+
"# generate 2 2d grids for the x & y bounds\n",
23+
"y, x = np.mgrid[slice(1, 5 + dy, dy),\n",
24+
" slice(1, 5 + dx, dx)]\n",
25+
"\n",
26+
"z = np.sin(x)**10 + np.cos(10 + y*x) * np.cos(x)"
27+
]
28+
},
29+
{
30+
"cell_type": "code",
31+
"execution_count": null,
32+
"metadata": {},
33+
"outputs": [],
34+
"source": [
35+
"def colormap(value, min, max):\n",
36+
" scaled_value = (value - min) / (max - min) \n",
37+
" color_value = 255 if value > max else scaled_value * 255\n",
38+
" return 'rgb({0}, {0}, 0)'.format(int(color_value))"
39+
]
40+
},
41+
{
42+
"cell_type": "code",
43+
"execution_count": null,
44+
"metadata": {},
45+
"outputs": [],
46+
"source": [
47+
"n_pixels = 10\n",
48+
"\n",
49+
"canvas = Canvas(size=(z.shape[0] * n_pixels, z.shape[1] * n_pixels))"
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"min = np.min(z)\n",
59+
"max = np.max(z)\n",
60+
"\n",
61+
"canvas.caching = True\n",
62+
"r = 0\n",
63+
"for row in z:\n",
64+
" c = 0\n",
65+
" for value in row:\n",
66+
" canvas.fill_style = colormap(value, min, max)\n",
67+
" canvas.fill_rect(r * n_pixels, c * n_pixels, n_pixels, n_pixels)\n",
68+
" \n",
69+
" c += 1\n",
70+
" r += 1"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": null,
76+
"metadata": {},
77+
"outputs": [],
78+
"source": [
79+
"canvas"
80+
]
81+
},
82+
{
83+
"cell_type": "code",
84+
"execution_count": null,
85+
"metadata": {},
86+
"outputs": [],
87+
"source": [
88+
"canvas.flush()"
89+
]
90+
}
91+
],
92+
"metadata": {
93+
"kernelspec": {
94+
"display_name": "Python 3",
95+
"language": "python",
96+
"name": "python3"
97+
},
98+
"language_info": {
99+
"codemirror_mode": {
100+
"name": "ipython",
101+
"version": 3
102+
},
103+
"file_extension": ".py",
104+
"mimetype": "text/x-python",
105+
"name": "python",
106+
"nbconvert_exporter": "python",
107+
"pygments_lexer": "ipython3",
108+
"version": "3.7.3"
109+
}
110+
},
111+
"nbformat": 4,
112+
"nbformat_minor": 4
113+
}

ipycanvas/canvas.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66

77
from ipywidgets import Color, DOMWidget
88

9-
from traitlets import Float, Tuple, Unicode
9+
from traitlets import Float, Tuple, Unicode, observe
1010

1111
from ._frontend import module_name, module_version
1212

1313

14+
def to_camel_case(snake_str):
15+
"""Turn a snake_case string into a camelCase one."""
16+
components = snake_str.split('_')
17+
return components[0] + ''.join(x.title() for x in components[1:])
18+
19+
1420
class Canvas(DOMWidget):
1521
_model_name = Unicode('CanvasModel').tag(sync=True)
1622
_model_module = Unicode(module_name).tag(sync=True)
@@ -21,11 +27,14 @@ class Canvas(DOMWidget):
2127

2228
size = Tuple((700, 500), help='Size of the Canvas, this is not equal to the size of the view').tag(sync=True)
2329

24-
fill_style = Color('black').tag(sync=True)
25-
stroke_style = Color('black').tag(sync=True)
26-
global_alpha = Float(1.0).tag(sync=True)
30+
fill_style = Color('black')
31+
stroke_style = Color('black')
32+
global_alpha = Float(1.0)
2733

2834
def __init__(self, *args, **kwargs):
35+
self.caching = kwargs.get('caching', False)
36+
self.commands_cache = []
37+
2938
super(Canvas, self).__init__(*args, **kwargs)
3039
self.layout.width = str(self.size[0]) + 'px'
3140
self.layout.height = str(self.size[1]) + 'px'
@@ -78,5 +87,29 @@ def quadratic_curve_to(self, cp1x, cp1y, x, y):
7887
def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
7988
self._send_canvas_command('bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y)
8089

90+
def flush(self):
91+
if not self.caching:
92+
return
93+
94+
self.send(self.commands_cache)
95+
96+
self.caching = False
97+
self.commands_cache = []
98+
99+
@observe('fill_style', 'stroke_style', 'global_alpha')
100+
def _on_set_attr(self, change):
101+
command = {
102+
'name': 'set',
103+
'attr': to_camel_case(change.name),
104+
'value': change.new
105+
}
106+
self._send_command(command)
107+
81108
def _send_canvas_command(self, name, *args):
82-
self.send({'name': name, 'args': args})
109+
self._send_command({'name': name, 'args': args})
110+
111+
def _send_command(self, command):
112+
if self.caching:
113+
self.commands_cache.append(command)
114+
else:
115+
self.send(command)

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/widget.ts

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,19 @@ class CanvasModel extends DOMWidgetModel {
2121
_view_module: CanvasModel.view_module,
2222
_view_module_version: CanvasModel.view_module_version,
2323
size: [],
24-
fill_style: 'black',
25-
stroke_style: 'black'
2624
};
2725
}
2826

2927
static serializers: ISerializers = {
3028
...DOMWidgetModel.serializers,
31-
// Add any extra serializers here
3229
}
3330

3431
initialize(attributes: any, options: any) {
3532
super.initialize(attributes, options);
3633

3734
this.commandsCache = [];
3835

39-
this.cacheSetCommand('fill_style', 'fillStyle');
40-
this.cacheSetCommand('stroke_style', 'strokeStyle');
41-
this.cacheSetCommand('global_alpha', 'globalAlpha');
42-
4336
this.on('msg:custom', (command) => { this.commandsCache.push(command); });
44-
45-
this.on('change:fill_style', () => { this.cacheSetCommand('fill_style', 'fillStyle'); });
46-
this.on('change:stroke_style', () => { this.cacheSetCommand('stroke_style', 'strokeStyle'); });
47-
this.on('change:global_alpha', () => { this.cacheSetCommand('global_alpha', 'globalAlpha'); });
48-
}
49-
50-
cacheSetCommand(python_name: string, ts_name: string) {
51-
this.commandsCache.push({
52-
name: 'set',
53-
attr: ts_name,
54-
value: this.get(python_name)
55-
});
5637
}
5738

5839
static model_name = 'CanvasModel';
@@ -62,7 +43,7 @@ class CanvasModel extends DOMWidgetModel {
6243
static view_module = MODULE_NAME;
6344
static view_module_version = MODULE_VERSION;
6445

65-
commandsCache: any;
46+
commandsCache: Array<any>;
6647
}
6748

6849

@@ -89,25 +70,28 @@ class CanvasView extends DOMWidgetView {
8970

9071
firstDraw() {
9172
// Replay all the commands that were received until this view was created
92-
for (const command of (this.model as CanvasModel).commandsCache) {
93-
if (command.name == 'set') {
94-
this.ctx[command.attr] = command.value;
95-
} else {
96-
this.ctx[command.name](...command.args);
97-
}
73+
for (const command of this.model.commandsCache) {
74+
this._processCommand(command);
9875
}
9976
}
10077

10178
modelEvents() {
102-
this.model.on('msg:custom', (command) => {
103-
this.ctx[command.name](...command.args);
104-
});
79+
this.model.on('msg:custom', this._processCommand.bind(this));
80+
}
10581

106-
this.model.on('change:size', () => { this.resize_canvas(); });
82+
private _processCommand (command: any) {
83+
if (command instanceof Array) {
84+
for (const subcommand of command) {
85+
this._processCommand(subcommand);
86+
}
87+
return;
88+
}
10789

108-
this.model.on('change:fill_style', () => { this.ctx.fillStyle = this.model.get('fill_style'); });
109-
this.model.on('change:stroke_style', () => { this.ctx.strokeStyle = this.model.get('stroke_style'); });
110-
this.model.on('change:global_alpha', () => { this.ctx.globalAlpha = this.model.get('global_alpha'); });
90+
if (command.name == 'set') {
91+
this.ctx[command.attr] = command.value;
92+
} else {
93+
this.ctx[command.name](...command.args);
94+
}
11195
}
11296

11397
resize_canvas() {
@@ -119,4 +103,5 @@ class CanvasView extends DOMWidgetView {
119103

120104
canvas: any;
121105
ctx: any;
106+
model: CanvasModel;
122107
}

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"rootDir": "src",
1313
"skipLibCheck": true,
1414
"sourceMap": true,
15+
// This allows us to initialize members in the "initialize" method
16+
"strictPropertyInitialization": false,
1517
"strict": true,
1618
"target": "es2015"
1719
},

0 commit comments

Comments
 (0)