Skip to content

Commit 9680521

Browse files
committed
Implement draw_image
1 parent 3b23b21 commit 9680521

File tree

10 files changed

+341
-61
lines changed

10 files changed

+341
-61
lines changed

docs/source/drawing_images.rst

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,47 @@ Drawing images
44
From an Image widget
55
--------------------
66

7-
Coming soon!
7+
You can draw from an `Image <https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#Image>`_ widget directly, this is the less optimized solution but it works perfectly fine if you don't draw more than a hundred images at a time.
8+
9+
- ``draw_image(image, x, y, width=None, height=None)``: Draw an ``image`` on the Canvas at the coordinates (``x``, ``y``) and scale it to (``width``, ``height``). The ``image`` must be an ``Image`` widget or another ``Canvas``.
10+
11+
.. code:: Python
12+
13+
from ipywidgets import Image
14+
15+
from ipycanvas import Canvas
16+
17+
sprite1 = Image.from_file('sprites/smoke_texture0.png')
18+
sprite2 = Image.from_file('sprites/smoke_texture1.png')
19+
20+
canvas = Canvas(size=(300, 300))
21+
22+
canvas.fill_style = '#a9cafc'
23+
canvas.fill_rect(0, 0, 300, 300)
24+
25+
canvas.draw_image(sprite1, 50, 50)
26+
canvas.draw_image(sprite2, 100, 100)
27+
28+
canvas
29+
30+
.. image:: images/draw_image1.png
831

932
From another Canvas
1033
-------------------
1134

12-
Coming soon!
35+
You can draw from another ``Canvas`` widget. This is the fastest way of drawing an image on the canvas.
36+
37+
.. code:: Python
38+
39+
canvas2 = Canvas(size=(600, 300))
40+
41+
# Here ``canvas`` is the canvas from the previous example
42+
canvas2.draw_image(canvas, 0, 0)
43+
canvas2.draw_image(canvas, 300, 0)
44+
45+
canvas2
46+
47+
.. image:: images/draw_image2.png
1348

1449
From a NumPy array
1550
------------------
@@ -41,3 +76,68 @@ You can directly draw a NumPy array of pixels on the ``Canvas``, it must be a 3-
4176
canvas
4277
4378
.. image:: images/numpy.png
79+
80+
Optimizing drawings
81+
-------------------
82+
83+
Drawing from another ``Canvas`` is by far the fastest of the three solutions presented here. So if you want to draw the same image a thousand times, it is recommended to first draw this image on a temporary canvas, then draw from the temporary canvas a thousand times.
84+
85+
.. code:: Python
86+
87+
from random import choice, randint, uniform
88+
from math import pi
89+
90+
from ipywidgets import Image, HBox
91+
92+
from ipycanvas import Canvas, hold_canvas
93+
94+
# Create temporary Canvases
95+
canvas_sprite1 = Canvas(size=(100, 100))
96+
canvas_sprite1.draw_image(Image.from_file('sprites/smoke_texture0.png'), 0, 0)
97+
98+
canvas_sprite2 = Canvas(size=(100, 100))
99+
canvas_sprite2.draw_image(Image.from_file('sprites/smoke_texture1.png'), 0, 0)
100+
101+
canvas_sprite3 = Canvas(size=(100, 100))
102+
canvas_sprite3.draw_image(Image.from_file('sprites/smoke_texture2.png'), 0, 0)
103+
104+
sprites = [canvas_sprite1, canvas_sprite2, canvas_sprite3]
105+
106+
# Display them horizontally
107+
HBox(sprites)
108+
109+
.. image:: images/sprites.png
110+
111+
.. code:: Python
112+
113+
canvas = Canvas(size=(800, 600))
114+
115+
with hold_canvas(canvas):
116+
for _ in range(2_000):
117+
canvas.save()
118+
119+
# Choose a random sprite texture
120+
sprite = sprites[choice(range(3))]
121+
122+
# Choose a random sprite position
123+
pos_x = randint(0, canvas.size[0])
124+
pos_y = randint(0, canvas.size[1])
125+
126+
# Choose a random rotation angle (but first set the rotation center with `translate`)
127+
canvas.translate(pos_x, pos_y)
128+
canvas.rotate(uniform(0., pi))
129+
130+
# Choose a random sprite size
131+
canvas.scale(uniform(0.2, 1.))
132+
133+
# Restore the canvas center
134+
canvas.translate(- pos_x, - pos_y)
135+
136+
# Draw the sprite
137+
canvas.draw_image(sprite, pos_x, pos_y)
138+
139+
canvas.restore()
140+
141+
canvas
142+
143+
.. image:: images/thousands_sprites.png

docs/source/images/draw_image1.png

12.8 KB
Loading

docs/source/images/draw_image2.png

16.6 KB
Loading

docs/source/images/sprites.png

16.3 KB
Loading
364 KB
Loading

examples/sprites.ipynb

Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@
2222
"from random import choice, randint, uniform\n",
2323
"from math import pi\n",
2424
"\n",
25-
"from PIL import Image\n",
25+
"from PIL import Image as PILImage\n",
2626
"\n",
2727
"import numpy as np\n",
2828
"\n",
29-
"from ipycanvas import MultiCanvas, hold_canvas"
29+
"from ipywidgets import Image\n",
30+
"\n",
31+
"from ipycanvas import Canvas, hold_canvas"
32+
]
33+
},
34+
{
35+
"cell_type": "markdown",
36+
"metadata": {},
37+
"source": [
38+
"## Drawing a sprite from an ``Image`` widget"
3039
]
3140
},
3241
{
@@ -35,9 +44,15 @@
3544
"metadata": {},
3645
"outputs": [],
3746
"source": [
38-
"def load_image(path):\n",
39-
" \"\"\"Load an image into a NumPy array.\"\"\"\n",
40-
" return np.array(Image.open(path))"
47+
"sprite1 = Image.from_file('sprites/smoke_texture0.png')\n",
48+
"sprite2 = Image.from_file('sprites/smoke_texture1.png')\n",
49+
"\n",
50+
"canvas1 = Canvas(size=(300, 300))\n",
51+
"canvas1.fill_style = '#a9cafc'\n",
52+
"canvas1.fill_rect(0, 0, 300, 300)\n",
53+
"canvas1.draw_image(sprite1, 50, 50)\n",
54+
"\n",
55+
"canvas1"
4156
]
4257
},
4358
{
@@ -46,8 +61,14 @@
4661
"metadata": {},
4762
"outputs": [],
4863
"source": [
49-
"# Loading sprites as NumPy arrays\n",
50-
"sprites_data = [load_image('sprites/smoke_texture{}.png'.format(i)) for i in range(3)]"
64+
"canvas1.draw_image(sprite2, 100, 100)"
65+
]
66+
},
67+
{
68+
"cell_type": "markdown",
69+
"metadata": {},
70+
"source": [
71+
"## Drawing from another ``Canvas``"
5172
]
5273
},
5374
{
@@ -56,7 +77,10 @@
5677
"metadata": {},
5778
"outputs": [],
5879
"source": [
59-
"sprites_data[0].shape"
80+
"canvas2 = Canvas(size=(600, 300))\n",
81+
"canvas2.draw_image(canvas1, 0, 0)\n",
82+
"\n",
83+
"canvas2"
6084
]
6185
},
6286
{
@@ -65,11 +89,23 @@
6589
"metadata": {},
6690
"outputs": [],
6791
"source": [
68-
"m = MultiCanvas(n_canvases=2, size=(800, 600))\n",
69-
"m[0].fill_style = '#a9cafc'\n",
70-
"m[0].fill_rect(0, 0, m.size[0], m.size[1])\n",
71-
"\n",
72-
"m"
92+
"canvas2.draw_image(canvas1, 300, 0)"
93+
]
94+
},
95+
{
96+
"cell_type": "markdown",
97+
"metadata": {},
98+
"source": [
99+
"## Drawing from a NumPy array"
100+
]
101+
},
102+
{
103+
"cell_type": "code",
104+
"execution_count": null,
105+
"metadata": {},
106+
"outputs": [],
107+
"source": [
108+
"sprite3 = np.array(PILImage.open('sprites/smoke_texture2.png'))"
73109
]
74110
},
75111
{
@@ -78,34 +114,88 @@
78114
"metadata": {},
79115
"outputs": [],
80116
"source": [
81-
"with hold_canvas(m[1]):\n",
82-
" for _ in range(200):\n",
83-
" m[1].save()\n",
117+
"canvas2.put_image_data(sprite3, 250, 150)"
118+
]
119+
},
120+
{
121+
"cell_type": "markdown",
122+
"metadata": {},
123+
"source": [
124+
"## Drawing thousands of sprites, the more optimized solution is to first \"cache\" your images in other ``Canvas`` instances"
125+
]
126+
},
127+
{
128+
"cell_type": "code",
129+
"execution_count": null,
130+
"metadata": {},
131+
"outputs": [],
132+
"source": [
133+
"# The fastest solution is drawing from another canvas\n",
134+
"canvas_sprite1 = Canvas(size=(100, 100))\n",
135+
"canvas_sprite1.draw_image(sprite1, 0, 0)\n",
136+
"canvas_sprite1"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {},
143+
"outputs": [],
144+
"source": [
145+
"canvas_sprite2 = Canvas(size=(100, 100))\n",
146+
"canvas_sprite2.draw_image(sprite2, 0, 0)\n",
147+
"canvas_sprite2"
148+
]
149+
},
150+
{
151+
"cell_type": "code",
152+
"execution_count": null,
153+
"metadata": {},
154+
"outputs": [],
155+
"source": [
156+
"canvas_sprite3 = Canvas(size=(100, 100))\n",
157+
"canvas_sprite3.draw_image(Image.from_file('sprites/smoke_texture2.png'), 0, 0)\n",
158+
"canvas_sprite3"
159+
]
160+
},
161+
{
162+
"cell_type": "code",
163+
"execution_count": null,
164+
"metadata": {},
165+
"outputs": [],
166+
"source": [
167+
"canvas3 = Canvas(size=(800, 600))\n",
168+
"\n",
169+
"sprites = [canvas_sprite1, canvas_sprite2, canvas_sprite3]\n",
170+
"\n",
171+
"with hold_canvas(canvas3):\n",
172+
" for _ in range(2_000):\n",
173+
" canvas3.save()\n",
84174
"\n",
85175
" # Choose a random sprite texture\n",
86-
" sprite = sprites_data[choice(range(3))]\n",
87-
" \n",
176+
" sprite = sprites[choice(range(3))]\n",
177+
"\n",
88178
" # Choose a random sprite position\n",
89-
" pos_x = randint(0, m.size[0] - sprite.shape[1])\n",
90-
" pos_y = randint(0, m.size[1] - sprite.shape[0])\n",
179+
" pos_x = randint(0, canvas3.size[0] - 50)\n",
180+
" pos_y = randint(0, canvas3.size[1] - 50)\n",
91181
"\n",
92182
" # Choose a random rotation angle (but first set the rotation center with `translate`)\n",
93-
" t_x = pos_x + 0.5 * sprite.shape[1]\n",
94-
" t_y = pos_y + 0.5 * sprite.shape[0]\n",
95-
" m[1].translate(t_x, t_y)\n",
96-
" m[1].rotate(uniform(0., pi))\n",
183+
" canvas3.translate(pos_x, pos_y)\n",
184+
" canvas3.rotate(uniform(0., pi))\n",
97185
"\n",
98186
" # Choose a random sprite size\n",
99187
" scale = uniform(0.2, 1.)\n",
100-
" m[1].scale(scale)\n",
188+
" canvas3.scale(scale)\n",
101189
"\n",
102190
" # Restore the canvas center\n",
103-
" m[1].translate(- t_x, - t_y)\n",
191+
" canvas3.translate(- pos_x, - pos_y)\n",
104192
"\n",
105193
" # Draw the sprite\n",
106-
" m[1].put_image_data(sprite, pos_x, pos_y)\n",
194+
" canvas3.draw_image(sprite, pos_x, pos_y)\n",
195+
"\n",
196+
" canvas3.restore()\n",
107197
"\n",
108-
" m[1].restore()"
198+
"canvas3"
109199
]
110200
}
111201
],
@@ -125,7 +215,7 @@
125215
"name": "python",
126216
"nbconvert_exporter": "python",
127217
"pygments_lexer": "ipython3",
128-
"version": "3.7.3"
218+
"version": "3.7.4"
129219
}
130220
},
131221
"nbformat": 4,

ipycanvas/canvas.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from traitlets import Enum, Float, Instance, List, Tuple, Unicode, observe
1212

13-
from ipywidgets import CallbackDispatcher, Color, DOMWidget, widget_serialization
13+
from ipywidgets import CallbackDispatcher, Color, DOMWidget, Image, widget_serialization
1414

1515
from ._frontend import module_name, module_version
1616

@@ -307,6 +307,18 @@ def set_line_dash(self, segments):
307307
self._send_canvas_command('setLineDash', (self._line_dash, ))
308308

309309
# Image methods
310+
def draw_image(self, image, x, y, width=None, height=None):
311+
"""Draw an ``image`` on the Canvas at the coordinates (``x``, ``y``) and scale it to (``width``, ``height``)."""
312+
if (not isinstance(image, (Canvas, Image))):
313+
raise TypeError('The image argument should be an Image widget or a Canvas widget')
314+
315+
if width is not None and height is None:
316+
height = width
317+
318+
serialized_image = widget_serialization['to_json'](image, None)
319+
320+
self._send_canvas_command('drawImage', (serialized_image, x, y, width, height))
321+
310322
def put_image_data(self, image_data, dx, dy):
311323
"""Draw an image on the Canvas.
312324

0 commit comments

Comments
 (0)