Add a another two examples of patterns
[PoPiPaint.git] / CanvasView.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2014  Antonio Ospite <ao2@ao2.it>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import wx
19
20 from math import *
21
22 import CanvasModel
23
24
25 # Polar coordinate system:
26 # http://en.wikipedia.org/wiki/Polar_coordinate_system
27 # (r, theta)
28
29 def cartesian2polar(x, y, offset_angle=0):
30     """return the polar coordinates relative to a circle
31     centered in (0,0) going counterclockwise.
32
33     returned angle is in degrees
34     """
35     r = sqrt(x*x + y*y)
36     theta = atan2(float(y), float(x)) + offset_angle
37
38     # report theta in the [0,360) range
39     theta = degrees(theta)
40     if theta < 0:
41         theta = 360 + theta
42
43     return r, theta
44
45
46 def polar2cartesian(r, theta, offset_angle=0):
47     """
48     theta expected in degrees
49     """
50     theta = radians(theta) - offset_angle
51     x = r * cos(theta)
52     y = r * sin(theta)
53
54     return x, y
55
56
57 class CanvasView(wx.Window):
58     def __init__(self, *args, **kwargs):
59         self.model = kwargs.pop('model')
60         base_image = kwargs.pop('base_image', None)
61         wx.Window.__init__(self, *args, **kwargs)
62
63         # This eliminates flickering on Windows
64         self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
65
66         w = h = int(self.model.external_radius * 2)
67
68         self.offset_angle = -pi/2.
69
70         self.SetInitialSize((w, h))
71
72         self.SetFocus()
73
74         self.Bind(wx.EVT_PAINT, self.OnPaint)
75
76         # Have a reference DC with the desired coordinate system.
77         # Because we want the position of the mouse pointer relative to the
78         # center of this canvas.
79         self.dc = wx.MemoryDC(wx.EmptyBitmap(w, h))
80         self.dc.SetDeviceOrigin(w/2., h/2.)
81
82         self.draw_grid = True
83
84         if base_image:
85             self.loadImage(base_image)
86
87         # FIXME: fix the file path.
88         #
89         # Actually it'd be even better to make the grid drawn to a MemoryDC with
90         # drawGrid(), but this is not working with python-wxgtk2.8 because there
91         # are bugs regarding bitmaps with alpha channels and MemoryDC
92         self.grid_buffer = self.loadGrid("res/grid.png")
93
94         self.pixels_buffer = wx.EmptyBitmapRGBA(w, h, 0, 0 ,0, 255)
95         self.drawAllPixels()
96
97     def setPixelCoordinates(self, gc):
98         w, h = self.GetSize()
99         gc.Translate(w/2., h/2.)
100         gc.Rotate(-self.offset_angle)
101
102     def MakePixelsGC(self, dc_buffer):
103         dc = wx.MemoryDC(dc_buffer)
104         gc = wx.GraphicsContext.Create(dc)
105         self.setPixelCoordinates(gc)
106         return gc
107
108     def loadGrid(self, file_path):
109         image = wx.Image(file_path)
110
111         im_w, im_h = image.GetSize()
112         w, h = self.GetSize()
113         if im_w != w or im_h != h:
114             return None
115
116         return wx.BitmapFromImage(image)
117
118     def loadImage(self, file_path):
119         image = wx.Image(file_path)
120
121         w, h = image.GetSize()
122         if w != self.model.width or h != self.model.height:
123             return False
124
125         bitmap = wx.BitmapFromImage(image)
126         bitmap.CopyToBuffer(self.model.pixels_array)
127         return True
128
129     def drawGrid(self, gc):
130         pen_size = 1
131         gc.SetPen(wx.Pen('#555555', pen_size))
132         gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
133
134         for i in range(0, self.model.height):
135             r, theta = self.model.toPolar(0, i)
136             path = gc.CreatePath()
137             path.AddCircle(0, 0, r)
138             gc.DrawPath(path)
139
140         # draw the outmost circle
141         r, theta = self.model.toPolar(0, self.model.height)
142         path = gc.CreatePath()
143         path.AddCircle(0, 0, r - pen_size)
144         gc.DrawPath(path)
145
146         min_r = self.model.internal_radius
147         max_r = self.model.external_radius - pen_size
148         for i in range(0, self.model.width):
149             r, theta = self.model.toPolar(i, 0)
150             x1, y1 = polar2cartesian(min_r, theta)
151             x2, y2 = polar2cartesian(max_r, theta)
152
153             path = gc.CreatePath()
154             path.MoveToPoint(x1, y1)
155             path.AddLineToPoint(x2, y2)
156             gc.DrawPath(path)
157
158     def _drawPixel(self, gc, x, y, color=None):
159         pen_size = 1
160         if color:
161             gc.SetPen(wx.Pen(color, pen_size))
162             gc.SetBrush(wx.Brush(color))
163         else:
164             gc.SetPen(wx.Pen(wx.RED, pen_size))
165             gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
166
167         min_r, theta1 = self.model.toPolar(x, y)
168
169         max_r = min_r + self.model.ring_width
170         
171         # prevent the outmost circle to overflow the canvas
172         if y == self.model.height - 1:
173             max_r -= pen_size
174
175         theta2 = theta1 + self.model.sector_width
176
177         # Draw the circular arc
178         path = gc.CreatePath()
179         path.AddArc(0, 0, min_r, radians(theta1), radians(theta2), True)
180         path.AddLineToPoint(polar2cartesian(max_r, theta2))
181         path.AddArc(0, 0, max_r, radians(theta2), radians(theta1), False)
182         path.AddLineToPoint(polar2cartesian(min_r, theta1))
183         path.CloseSubpath()
184         gc.DrawPath(path)
185
186     def drawAllPixels(self):
187         gc = self.MakePixelsGC(self.pixels_buffer)
188         for y in range(0, self.model.height):
189             for x in range(0, self.model.width):
190                 color = self.model.getPixelColor(x, y)
191                 self._drawPixel(gc, x, y, color=color)
192
193     def drawPixel(self, pixel):
194         if not pixel:
195             return
196
197         x, y = pixel
198
199         gc = self.MakePixelsGC(self.pixels_buffer)
200         color = self.model.getPixelColor(x, y)
201         self._drawPixel(gc, x, y, color)
202
203     def drawPixelMarker(self, gc, pixel):
204         if not pixel:
205             return
206
207         x, y = pixel
208         self._drawPixel(gc, x, y)
209
210     def drawImage(self, gc):
211         gc.SetPen(wx.Pen('#CCCCCC', 1))
212         gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
213
214         w, h = self.GetSize()
215         gc.DrawRectangle(w - self.model.width, 0,
216                          self.model.width, self.model.height)
217
218         bitmap = wx.BitmapFromBuffer(self.model.width, self.model.height,
219                                      self.model.pixels_array)
220         gc.DrawBitmap(bitmap, w - self.model.width, 0,
221                       self.model.width, self.model.height)
222
223     def drawTag(self, gc):
224         font = wx.Font(12, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL,
225                        wx.FONTWEIGHT_BOLD)
226         gc.SetFont(font, "#0099FF")
227         w, h = self.GetSize()
228         text = "ao2.it"
229         twidth, theight, descent, externalLeading = gc.GetFullTextExtent(text)
230         gc.DrawText(text, w - twidth - 4, h - theight - 2)
231
232     def OnPaint(self, event):
233         # make a DC to draw into...
234         if 'wxMSW' in wx.PlatformInfo:
235             dc = wx.BufferedPaintDC(self)
236         else:
237             dc = wx.PaintDC(self)
238
239         dc.SetBackground(wx.Brush(wx.BLACK))
240         dc.Clear()
241
242         gc = wx.GraphicsContext.Create(dc)
243         w, h = self.GetSize()
244
245         gc.DrawBitmap(self.pixels_buffer, 0, 0, w, h)
246
247         if self.draw_grid:
248             gc.DrawBitmap(self.grid_buffer, 0, 0, w, h)
249
250         gc.PushState()
251         self.setPixelCoordinates(gc)
252         self.drawPixelMarker(gc, self.model.last_pixel)
253         gc.PopState()
254
255         self.drawImage(gc)
256         self.drawTag(gc)