Skip to content

Commit 8cbd81d

Browse files
committed
blitting still not okay
1 parent df69e7b commit 8cbd81d

File tree

3 files changed

+171
-52
lines changed

3 files changed

+171
-52
lines changed

pygame_matplotlib/backend_pygame.py

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@
77
import matplotlib
88
matplotlib.use("'module://pygame_matplotlib.backend_pygame'")
99
"""
10+
from __future__ import annotations
11+
import matplotlib
12+
from matplotlib.pyplot import figure
1013
import numpy as np
1114
from matplotlib.transforms import Affine2D
1215
import pygame
16+
from pygame import surface
1317
import pygame.image
1418
from pygame import gfxdraw
1519
from matplotlib._pylab_helpers import Gcf
1620
from matplotlib.backend_bases import (
17-
FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase)
21+
FigureCanvasBase,
22+
FigureManagerBase,
23+
GraphicsContextBase,
24+
RendererBase,
25+
)
1826
from matplotlib.figure import Figure
1927
from matplotlib.path import Path
28+
from matplotlib.transforms import Bbox
2029

2130

2231
class FigureSurface(pygame.Surface, Figure):
@@ -26,20 +35,22 @@ class FigureSurface(pygame.Surface, Figure):
2635
that help handling the figures in pygame.
2736
"""
2837

38+
canvas: FigureCanvasPygame
39+
2940
def __init__(self, *args, **kwargs):
3041
"""Create a FigureSurface object.
3142
3243
Signature is the same as matplotlib Figure object.
3344
"""
3445
Figure.__init__(self, *args, **kwargs)
3546
pygame.Surface.__init__(self, self.bbox.size)
36-
self.fill('white')
47+
self.fill("white")
3748

3849
def set_bounding_rect(self, rect: pygame.Rect):
3950
"""Set a bounding rectangle around the figure."""
4051
# First convert inches to pixels for matplotlib
4152
DPI = self.get_dpi()
42-
self.set_size_inches(rect.width/float(DPI), rect.height/float(DPI))
53+
self.set_size_inches(rect.width / float(DPI), rect.height / float(DPI))
4354
# Initialize a new surface for the plot
4455
pygame.Surface.__init__(self, (rect.width, rect.height))
4556
# Redraw the figure
@@ -52,27 +63,33 @@ class RendererPygame(RendererBase):
5263
The draw methods convert maptplotlib into pygame.draw .
5364
"""
5465

66+
surface: pygame.Surface
67+
5568
def __init__(self, dpi):
5669
super().__init__()
5770
self.dpi = dpi
5871

72+
def rect_from_bbox(self, bbox: Bbox) -> pygame.Rect:
73+
"""Convert a matplotlib bbox to the equivalent pygame Rect."""
74+
print(bbox.get_points())
75+
5976
def draw_path(self, gc, path, transform, rgbFace=None):
6077

6178
if rgbFace is not None:
62-
color = tuple([
63-
int(val*255) for i, val in enumerate(rgbFace) if i < 3
64-
])
79+
color = tuple(
80+
[int(val * 255) for i, val in enumerate(rgbFace) if i < 3]
81+
)
6582
else:
66-
color = tuple([
67-
int(val*255) for i, val in enumerate(gc.get_rgb()) if i < 3
68-
])
83+
color = tuple(
84+
[int(val * 255) for i, val in enumerate(gc.get_rgb()) if i < 3]
85+
)
6986

7087
linewidth = int(gc.get_linewidth())
7188

7289
transfrom_to_pygame_axis = Affine2D()
73-
transfrom_to_pygame_axis.set_matrix([
74-
[1, 0, 0], [0, -1, self.surface.get_height()], [0, 0, 1]
75-
])
90+
transfrom_to_pygame_axis.set_matrix(
91+
[[1, 0, 0], [0, -1, self.surface.get_height()], [0, 0, 1]]
92+
)
7693

7794
transform += transfrom_to_pygame_axis
7895

@@ -95,21 +112,17 @@ def draw_path(self, gc, path, transform, rgbFace=None):
95112
poly_points.append(point)
96113
elif code == Path.CURVE3 or code == Path.CURVE4:
97114
end_point = point[2:]
98-
points_curve = np.concatenate(
99-
(previous_point, point)
100-
).reshape((-1, 2))
115+
points_curve = np.concatenate((previous_point, point)).reshape(
116+
(-1, 2)
117+
)
101118
gfxdraw.bezier(
102119
self.surface, points_curve, len(points_curve), color
103120
)
104121
previous_point = end_point
105122
poly_points.append(end_point)
106123
elif code == Path.CLOSEPOLY:
107124
if len(poly_points) > 2:
108-
gfxdraw.filled_polygon(
109-
self.surface,
110-
poly_points,
111-
color
112-
)
125+
gfxdraw.filled_polygon(self.surface, poly_points, color)
113126
elif code == Path.MOVETO:
114127
poly_points.append(point)
115128
previous_point = point
@@ -119,37 +132,38 @@ def draw_path(self, gc, path, transform, rgbFace=None):
119132
# draw_markers is optional, and we get more correct relative
120133
# timings by leaving it out. backend implementers concerned with
121134
# performance will probably want to implement it
122-
# def draw_markers(self, gc, marker_path, marker_trans, path, trans,
123-
# rgbFace=None):
124-
# pass
135+
# def draw_markers(self, gc, marker_path, marker_trans, path, trans,
136+
# rgbFace=None):
137+
# pass
125138

126139
# draw_path_collection is optional, and we get more correct
127140
# relative timings by leaving it out. backend implementers concerned with
128141
# performance will probably want to implement it
129-
# def draw_path_collection(self, gc, master_transform, paths,
130-
# all_transforms, offsets, offsetTrans,
131-
# facecolors, edgecolors, linewidths, linestyles,
132-
# antialiaseds):
133-
# pass
142+
# def draw_path_collection(self, gc, master_transform, paths,
143+
# all_transforms, offsets, offsetTrans,
144+
# facecolors, edgecolors, linewidths, linestyles,
145+
# antialiaseds):
146+
# pass
134147

135148
# draw_quad_mesh is optional, and we get more correct
136149
# relative timings by leaving it out. backend implementers concerned with
137150
# performance will probably want to implement it
138-
# def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
139-
# coordinates, offsets, offsetTrans, facecolors,
140-
# antialiased, edgecolors):
141-
# pass
151+
# def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
152+
# coordinates, offsets, offsetTrans, facecolors,
153+
# antialiased, edgecolors):
154+
# pass
142155

143156
def draw_image(self, gc, x, y, im):
144157
img_surf = pygame.image.frombuffer(
145158
# Need to flip the image as pygame starts top left
146159
np.ascontiguousarray(np.flip(im, axis=0)),
147-
(im.shape[1], im.shape[0]), 'RGBA'
160+
(im.shape[1], im.shape[0]),
161+
"RGBA",
148162
)
149163
self.surface.blit(
150164
img_surf,
151165
# Image starts top left
152-
(x, self.surface.get_height() - y - im.shape[0])
166+
(x, self.surface.get_height() - y - im.shape[0]),
153167
)
154168

155169
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
@@ -161,7 +175,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
161175
myfont = pygame.font.Font(prop.get_file(), int(font_size))
162176
# apply it to text on a label
163177
font_surface = myfont.render(
164-
s, gc.get_antialiased(), [val*255 for val in gc.get_rgb()]
178+
s, gc.get_antialiased(), [val * 255 for val in gc.get_rgb()]
165179
)
166180
if mtext is not None:
167181
# Reads the position of the mtext
@@ -196,6 +210,22 @@ def points_to_pixels(self, points):
196210
# else
197211
# return points/72.0 * self.dpi.get()
198212

213+
def clear(self):
214+
self.surface.fill("white")
215+
216+
def copy_from_bbox(self, bbox):
217+
copy = self.surface.copy()
218+
copy.bbox = bbox
219+
return copy
220+
221+
def restore_region(self, region: pygame.Surface, bbox: Bbox, xy):
222+
rect = (
223+
pygame.Rect(0, 0, *self.surface.get_size())
224+
if bbox is None
225+
else self.rect_from_bbox(bbox)
226+
)
227+
region.blit(self.surface, rect)
228+
199229

200230
class GraphicsContextPygame(GraphicsContextBase):
201231
"""
@@ -243,7 +273,7 @@ def show(*, block=None):
243273
should do nothing.
244274
"""
245275
for manager in Gcf.get_all_fig_managers():
246-
manager.show()
276+
manager.show(block)
247277

248278

249279
def new_figure_manager(num, *args, FigureClass=FigureSurface, **kwargs):
@@ -282,25 +312,33 @@ class methods button_press_event, button_release_event,
282312
figure : `matplotlib.figure.Figure`
283313
A high-level Figure instance
284314
"""
315+
285316
# File types allowed for saving
286317
filetypes = {
287-
'jpeg': 'Joint Photographic Experts Group',
288-
'jpg': 'Joint Photographic Experts Group',
289-
'png': 'Portable Network Graphics',
290-
'bmp': 'Bitmap Image File',
291-
'tga': 'Truevision Graphics Adapter',
318+
"jpeg": "Joint Photographic Experts Group",
319+
"jpg": "Joint Photographic Experts Group",
320+
"png": "Portable Network Graphics",
321+
"bmp": "Bitmap Image File",
322+
"tga": "Truevision Graphics Adapter",
292323
}
324+
figure: FigureSurface
325+
renderer: RendererPygame
293326

294327
def __init__(self, figure=None):
295328
super().__init__(figure)
296329

297330
# You should provide a print_xxx function for every file format
298331
# you can write.
299332
for file_extension in self.filetypes.keys():
300-
setattr(
301-
self, 'print_{}'.format(file_extension),
302-
self._print_any
303-
)
333+
setattr(self, "print_{}".format(file_extension), self._print_any)
334+
335+
def copy_from_bbox(self, bbox):
336+
renderer = self.get_renderer()
337+
return renderer.copy_from_bbox(bbox)
338+
339+
def restore_region(self, region, bbox=None, xy=None):
340+
renderer = self.get_renderer()
341+
return renderer.restore_region(region, bbox, xy)
304342

305343
def draw(self):
306344
"""
@@ -311,17 +349,44 @@ def draw(self):
311349
deferred work (like computing limits auto-limits and tick
312350
values) that users may want access to before saving to disk.
313351
"""
314-
renderer = RendererPygame(self.figure.dpi)
315-
renderer.surface = self.figure
316-
self.figure.draw(renderer)
352+
self.renderer = self.get_renderer(cleared=True)
353+
self.renderer.surface = self.figure
354+
self.figure.draw(self.renderer)
355+
356+
def blit(self, bbox=None):
357+
self.renderer = self.get_renderer(cleared=False)
358+
self.renderer.surface = self.figure
359+
self.figure.draw(self.renderer)
360+
361+
def get_renderer(self, cleared=False) -> RendererPygame:
362+
fig = self.figure
363+
reuse_renderer = (
364+
hasattr(self, "renderer")
365+
and getattr(self, "_last_fig", None) == fig
366+
)
367+
if not reuse_renderer:
368+
self.renderer = RendererPygame(self.figure.dpi)
369+
self._last_fig = fig
370+
elif cleared:
371+
self.renderer.clear()
372+
return self.renderer
317373

318374
def _print_any(self, filename, *args, **kwargs):
319375
"""Call the pygame image saving method for the correct extenstion."""
320376
self.draw()
321377
pygame.image.save(self.figure, filename)
322378

323379
def get_default_filetype(self):
324-
return 'jpg'
380+
return "jpg"
381+
382+
def flush_events(self):
383+
self.main_display.blit(self.figure, (0, 0))
384+
pygame.display.update()
385+
events = pygame.event.get()
386+
for event in events:
387+
if event.type == pygame.QUIT:
388+
pygame.quit()
389+
show_fig = False
325390

326391

327392
class FigureManagerPygame(FigureManagerBase):
@@ -331,20 +396,23 @@ class FigureManagerPygame(FigureManagerBase):
331396
For non-interactive backends, the base class is sufficient.
332397
"""
333398

334-
def show(self):
399+
def show(self, block):
335400
# do something to display the GUI
336401
pygame.init()
337402
main_display = pygame.display.set_mode(
338403
self.canvas.figure.get_size(), # Size matches figure size
339404
)
340405

406+
self.canvas.main_display = main_display
407+
341408
FPS = 60
342409
FramePerSec = pygame.time.Clock()
343410

344411
self.canvas.figure.canvas.draw()
345412
main_display.blit(self.canvas.figure, (0, 0))
413+
pygame.display.update()
346414

347-
show_fig = True
415+
show_fig = True if block is None else False
348416
while show_fig:
349417
events = pygame.event.get()
350418
pygame.display.update()

test_blitting.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import matplotlib
2+
import matplotlib.pyplot as plt
3+
4+
matplotlib.use('module://pygame_matplotlib.backend_pygame')
5+
import numpy as np
6+
7+
x = np.linspace(0, 2 * np.pi, 100)
8+
9+
fig, ax = plt.subplots()
10+
11+
# animated=True tells matplotlib to only draw the artist when we
12+
# explicitly request it
13+
(ln,) = ax.plot(x, np.sin(x), animated=True)
14+
15+
# make sure the window is raised, but the script keeps going
16+
plt.show(block=False)
17+
18+
# stop to admire our empty window axes and ensure it is rendered at
19+
# least once.
20+
#
21+
# We need to fully draw the figure at its final size on the screen
22+
# before we continue on so that :
23+
# a) we have the correctly sized and drawn background to grab
24+
# b) we have a cached renderer so that ``ax.draw_artist`` works
25+
# so we spin the event loop to let the backend process any pending operations
26+
plt.pause(0.1)
27+
28+
# get copy of entire figure (everything inside fig.bbox) sans animated artist
29+
print(fig.bbox)
30+
bg = fig.canvas.copy_from_bbox(fig.bbox)
31+
print(bg)
32+
33+
# draw the animated artist, this uses a cached renderer
34+
ax.draw_artist(ln)
35+
# show the result to the screen, this pushes the updated RGBA buffer from the
36+
# renderer to the GUI framework so you can see it
37+
fig.canvas.blit(fig.bbox)
38+
39+
for j in range(1000):
40+
# reset the background back in the canvas state, screen unchanged
41+
fig.canvas.restore_region(bg)
42+
# update the artist, neither the canvas state nor the screen have changed
43+
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
44+
# re-render the artist, updating the canvas state, but not the screen
45+
ax.draw_artist(ln)
46+
# copy the image to the GUI state, but screen might not be changed yet
47+
fig.canvas.blit(fig.bbox)
48+
# flush any pending GUI events, re-painting the screen if needed
49+
fig.canvas.flush_events()
50+
# you can put a pause in if you want to slow things down
51+
# plt.pause(.1)

test_plt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
plt.xlabel('swag')
1717

1818

19-
plt.show()
19+
plt.show()

0 commit comments

Comments
 (0)