Merge lp:~brunogirin/python-snippets/cairo-easter-egg into lp:~jonobacon/python-snippets/trunk
- cairo-easter-egg
- Merge into trunk
Proposed by
Bruno Girin
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~brunogirin/python-snippets/cairo-easter-egg |
Merge into: | lp:~jonobacon/python-snippets/trunk |
Diff against target: |
336 lines (+332/-0) 1 file modified
cairo/cairo_easter_egg.py (+332/-0) |
To merge this branch: | bzr merge lp:~brunogirin/python-snippets/cairo-easter-egg |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jono Bacon | Pending | ||
Review via email: mp+22916@code.launchpad.net |
Commit message
Description of the change
As I didn't have enough chocolate eggs over Easter, I created a Cairo snippet that generates some. They even change colour depending on the time of day for added psychedelic effect.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'cairo/cairo_easter_egg.py' |
2 | --- cairo/cairo_easter_egg.py 1970-01-01 00:00:00 +0000 |
3 | +++ cairo/cairo_easter_egg.py 2010-04-06 22:20:32 +0000 |
4 | @@ -0,0 +1,332 @@ |
5 | +#!/usr/bin/env python |
6 | +# |
7 | +# [SNIPPET_NAME: Cairo Easter Eggs] |
8 | +# [SNIPPET_CATEGORIES: Cairo, PyGTK] |
9 | +# [SNIPPET_DESCRIPTION: Simple Cairo exmple that displays colourful Easter eggs] |
10 | +# [SNIPPET_AUTHOR: Bruno Girin <brunogirin@gmail.com>] |
11 | +# [SNIPPET_LICENSE: GPL] |
12 | +# |
13 | +# This snippet is derived from laszlok's example introduced during the |
14 | +# Opportunistic Developer Week on 3rd March 2010. IRC logs are available here: |
15 | +# http://irclogs.ubuntu.com/2010/03/03/%23ubuntu-classroom.html |
16 | +# It demonstrates how to create a simple drawing using Cairo and how to display |
17 | +# it in a GTK window. |
18 | +# Although quite simple, this drawing demonstrates the following Cairo features: |
19 | +# * Drawing lines with different colours and widths |
20 | +# * Filling an area with a simple RGB colour |
21 | +# * Using a path for clipping |
22 | +# * Positioning objects in a window based on the window's geometry |
23 | +# |
24 | +# NOTE: Creating a QT version of this snippet would require changing the |
25 | +# implementation of the DrawingInterface class and replacing the use of the |
26 | +# gtk.gdk.Rectangle class throughout so should be fairly straightfoward. |
27 | + |
28 | +import gtk |
29 | +import cairo |
30 | +from datetime import datetime |
31 | + |
32 | +class DrawingInterface: |
33 | + """ |
34 | + The GTK interface to the drawing. |
35 | + |
36 | + This class creates a GTK window, adds a drawing area to it and handles |
37 | + GTK's destroy and expose events in order to close the application and |
38 | + re-draw the area using Cairo. |
39 | + """ |
40 | + def __init__(self, area): |
41 | + self.main_window = gtk.Window() |
42 | + self.draw_area = gtk.DrawingArea() |
43 | + |
44 | + self.main_window.connect("destroy", self.on_destroy) |
45 | + self.draw_area.connect("expose-event", self.on_expose) |
46 | + |
47 | + self.main_window.add(self.draw_area) |
48 | + self.main_window.set_size_request(area.width, area.height) |
49 | + |
50 | + self.drawing = Drawing() |
51 | + |
52 | + self.main_window.show_all() |
53 | + |
54 | + def on_destroy(self, widget): |
55 | + gtk.main_quit() |
56 | + |
57 | + def on_expose(self, widget, event): |
58 | + ctx = widget.window.cairo_create() |
59 | + self.drawing.draw(ctx, widget.get_allocation()) |
60 | + |
61 | +class Drawing: |
62 | + """ |
63 | + Top-level drawing class that contains a list of all the objects to draw. |
64 | + """ |
65 | + def __init__(self): |
66 | + """Initialise the class by creating an instance of each object.""" |
67 | + self.objects = [ |
68 | + Background(), |
69 | + Egg(), |
70 | + SmallEgg() |
71 | + ] |
72 | + |
73 | + def draw(self, ctx, area): |
74 | + """Draw the complete drawing by drawing each object in turn.""" |
75 | + for o in self.objects: |
76 | + o.draw(ctx, area) |
77 | + |
78 | +class Background: |
79 | + """ |
80 | + A simple background that draws a white rectangle the size of the area. |
81 | + """ |
82 | + def draw(self, ctx, area): |
83 | + ctx.rectangle(area.x, area.y, |
84 | + area.width, area.height) |
85 | + ctx.set_source_rgb(1, 1, 1) # white |
86 | + ctx.fill() |
87 | + |
88 | +class Egg(object): |
89 | + """ |
90 | + An object that draws an egg with colourful stripes in the center of the area. |
91 | + |
92 | + Note that this class extends object, so that sub-classes can call the |
93 | + super() method. |
94 | + All attributes of the egg are generated pseudo-randomly by using the |
95 | + current local date and time: |
96 | + * Hour, minute and second are used to calculate the hue of the wobbly |
97 | + stripes and egg background: the egg will be red at midnight, yellow |
98 | + at 4am, green at 8am, cyan at midday, blue at 4pm, magenta at 8pm. |
99 | + * The day of the month deicides on how wobbly the stripes are: nearly |
100 | + straight on the 1st up to very wobbly on the 31st. |
101 | + * The month in the year drives the direction of the wobbliness: |
102 | + negative for even numbered months, positive for odd numbered months. |
103 | + * The year drives the number of wobbles in each stripe: 2 for even |
104 | + numbered years, 3 for odd numbered years. |
105 | + """ |
106 | + def __init__(self): |
107 | + n = datetime.now() |
108 | + self.padding = 0.1 # 10% padding on all sides |
109 | + self.wobbly_sign = (n.month % 2) * 2 - 1 |
110 | + self.wobbliness = n.day / 31.0 |
111 | + self.wobbly_lines = 3 |
112 | + self.wobbles = n.year % 2 + 2 |
113 | + hue = ((((n.second / 60.0) + n.minute) / 60.0) + n.hour) / 24.0 |
114 | + self.rgb = self.hsv_to_rgb((hue, 1.0, 1.0)) |
115 | + self.whratio = 2.0 / 3.0 # the egg has a constant ratio |
116 | + self.outline_width_ratio = 0.01 |
117 | + |
118 | + def hsv_to_rgb(self, hsv): |
119 | + """ |
120 | + Transforms an HSV triple into an RGB triple. |
121 | + |
122 | + This method uses the algorithm detailed on Wikipedia: |
123 | + http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV |
124 | + with the difference that the hue is a number between 0.0 and 1.0, |
125 | + not between 0 and 360. |
126 | + """ |
127 | + (h, s, v) = hsv |
128 | + # Calculate the chroma |
129 | + c = v * s |
130 | + # Adjust the hue to a value between 0.0 and 6.0 |
131 | + hp = (h % 1.0) * 6 |
132 | + x = c * (1 - abs(hp % 2 - 1)) |
133 | + m = v - c |
134 | + if(hp < 1.0): |
135 | + return (c+m, x+m, m) |
136 | + elif(hp < 2.0): |
137 | + return (x+m, c+m, m) |
138 | + elif(hp < 3.0): |
139 | + return (m, c+m, x+m) |
140 | + elif(hp < 4.0): |
141 | + return (m, x+m, c+m) |
142 | + elif(hp < 5.0): |
143 | + return (x+m, m, c+m) |
144 | + else: |
145 | + return (c+m, m, x+m) |
146 | + |
147 | + def draw(self, ctx, area): |
148 | + """ |
149 | + Draw the egg. |
150 | + |
151 | + This method first calculates the area in which the egg will be drawn. |
152 | + It then draws colourful wobbly lines and the egg's outline. |
153 | + """ |
154 | + egg_area = self.calculate_egg_area(area) |
155 | + self.draw_wobbly_lines(ctx, egg_area) |
156 | + self.draw_egg_outline(ctx, egg_area) |
157 | + |
158 | + def calculate_egg_area(self, area): |
159 | + """ |
160 | + Calculate the egg's area. |
161 | + |
162 | + The area is calculated based on the fact that the egg should keep a |
163 | + constant ratio, be in the centre of the drawing area and have some |
164 | + padding around it. |
165 | + """ |
166 | + if(float(area.width) / float(area.height) > self.whratio): |
167 | + # If the area's width / height ratio is higher than the egg's |
168 | + # width / height ratio, the size of the egg is constrained by the |
169 | + # height of the area. So calculate the height of the egg first by |
170 | + # shaving the padding off the area's height and derive the other |
171 | + # values from it. |
172 | + h = area.height * (1 - 2 * self.padding) |
173 | + y = area.y + area.height * self.padding |
174 | + w = h * self.whratio |
175 | + x = area.x + (area.width - w) / 2 |
176 | + else: |
177 | + # Otherwise, the size of the egg is constrained by the width of the |
178 | + # area. So calculate the width of the egg first by shaving the |
179 | + # padding off the area's width and derive the other values from it. |
180 | + w = area.width * (1 - 2 * self.padding) |
181 | + x = area.x + area.width * self.padding |
182 | + h = w / self.whratio |
183 | + y = area.y + (area.height - h) / 2 |
184 | + return gtk.gdk.Rectangle(int(x), int(y), int(w), int(h)) |
185 | + |
186 | + def draw_wobbly_lines(self, ctx, area): |
187 | + """ |
188 | + Draw colourful wobbly lines going through the egg. |
189 | + |
190 | + This method first sets a clip area that has the shape of the egg and |
191 | + draws colourful wobbly lines using cubic bezier curves that are |
192 | + constrained by the clip area. |
193 | + """ |
194 | + line_y_interval = area.height / (self.wobbly_lines + 1) |
195 | + line_y = line_y_interval |
196 | + wobble_x_interval = area.width / self.wobbles |
197 | + wobble_x_offset = wobble_x_interval / 2 |
198 | + wobble_y_offset = wobble_x_offset * self.wobbliness * self.wobbly_sign |
199 | + # Save the context so that we can restore it |
200 | + ctx.save() |
201 | + # Set the clip area for all subsequent drawing primitives |
202 | + self.draw_egg_path(ctx, area) |
203 | + ctx.clip() |
204 | + # Draw a background to the egg in a colour that is an off-white tint |
205 | + # of the stripes' colour |
206 | + ctx.rectangle(area.x, area.y, |
207 | + area.width, area.height) |
208 | + ctx.set_source_rgb(*[(v + 15.0)/16.0 for v in self.rgb]) |
209 | + ctx.fill() |
210 | + # Start the wobbly line path |
211 | + ctx.new_path() |
212 | + for l in range(self.wobbly_lines): |
213 | + ctx.move_to(area.x, area.y + line_y) |
214 | + for w in range(self.wobbles): |
215 | + ctx.rel_curve_to( |
216 | + wobble_x_offset, wobble_y_offset, # P1 |
217 | + wobble_x_interval - wobble_x_offset, -wobble_y_offset, # P2 |
218 | + wobble_x_interval, 0 # P3 |
219 | + ) |
220 | + line_y += line_y_interval |
221 | + # Draw the created path |
222 | + ctx.set_source_rgb(*self.rgb) |
223 | + ctx.set_line_width(area.height / self.wobbly_lines / 5.0) |
224 | + # Set the line cap to square so that the lines extend slightly beyond |
225 | + # the boundary of the clip area; this prevents white space from |
226 | + # appearing at the end of the stripes when wobbliness is high |
227 | + ctx.set_line_cap(cairo.LINE_CAP_SQUARE) |
228 | + ctx.stroke() |
229 | + # Restore the context to the state it had when save() was called, in |
230 | + # effect removing the clip |
231 | + ctx.restore() |
232 | + |
233 | + def draw_egg_outline(self, ctx, area): |
234 | + """ |
235 | + Draw the egg's outline in black. |
236 | + """ |
237 | + self.draw_egg_path(ctx, area) |
238 | + ctx.set_source_rgb(0.0, 0.0, 0.0) |
239 | + ctx.set_line_width(area.height * self.outline_width_ratio) |
240 | + ctx.stroke() |
241 | + |
242 | + def draw_egg_path(self, ctx, area): |
243 | + """ |
244 | + Draw the path for the egg outline. |
245 | + |
246 | + This is in its own separate function because this path is used both |
247 | + for clipping the wobbly lines and for drawing the egg's outline. |
248 | + This path is composed of four cubic bezier curves laid out on a 4x6 |
249 | + grid. In the diagram below, curve end points are represented by E and |
250 | + curve control points are represented by C. |
251 | + |
252 | + .---C---E---C---. |
253 | + | | | | | |
254 | + .---.---.---.---. |
255 | + | | | | | |
256 | + C---.---.---.---C |
257 | + | | | | | |
258 | + .---.---.---.---. |
259 | + | | | | | |
260 | + E---.---.---.---E |
261 | + | | | | | |
262 | + C---.---.---.---C |
263 | + | | | | | |
264 | + .---C---E---C---. |
265 | + |
266 | + This layout results in a roughly egg-shaped curve. It could be improved |
267 | + but I reckon that's close enough. |
268 | + """ |
269 | + ctx.move_to(area.x + area.width / 2, area.y) |
270 | + ctx.curve_to( |
271 | + area.x + 3 * area.width / 4, area.y, |
272 | + area.x + area.width, area.y + area.height / 3, |
273 | + area.x + area.width, area.y + 2 * area.height / 3 |
274 | + ) |
275 | + ctx.curve_to( |
276 | + area.x + area.width, area.y + 5 * area.height / 6, |
277 | + area.x + 3 * area.width / 4, area.y + area.height, |
278 | + area.x + area.width / 2, area.y + area.height |
279 | + ) |
280 | + ctx.curve_to( |
281 | + area.x + area.width / 4, area.y + area.height, |
282 | + area.x, area.y + 5 * area.height / 6, |
283 | + area.x, area.y + 2 * area.height / 3 |
284 | + ) |
285 | + ctx.curve_to( |
286 | + area.x, area.y + area.height / 3, |
287 | + area.x + area.width / 4, area.y, |
288 | + area.x + area.width / 2, area.y |
289 | + ) |
290 | + |
291 | +class SmallEgg(Egg): |
292 | + """ |
293 | + A small version of the egg that will be positioned in front. |
294 | + """ |
295 | + def __init__(self): |
296 | + """ |
297 | + Create a small egg. |
298 | + |
299 | + This constructor calls the super class's constructor to set most |
300 | + attributes. It adds a size ratio and updates the outline width ratio |
301 | + correspondingly to ensure that the width of the stroke is the same for |
302 | + the small egg as it is for the large egg, despite the difference in |
303 | + overall size. It also swaps round the components of the RGB colour used |
304 | + to draw the egg so that the colour of the small egg is always different |
305 | + from the colour of the big egg. |
306 | + """ |
307 | + super(SmallEgg, self).__init__() |
308 | + self.size_ratio = 0.25 |
309 | + self.outline_width_ratio /= self.size_ratio |
310 | + self.rgb = (self.rgb[1], self.rgb[2], self.rgb[0]) |
311 | + |
312 | + def calculate_egg_area(self, area): |
313 | + """ |
314 | + Calculate the egg's area. |
315 | + |
316 | + This method calls the sper-class's method and modifies the resulting |
317 | + area so that the size of the small egg is adjusted based on the ratio |
318 | + and it's horizontal and vertical position is adjusted so that it lays |
319 | + in the bottom left corner of the window. Because of the way this |
320 | + position is calculated, the position of the small egg relative to the |
321 | + big egg changes as the width / height ratio of the window changes. |
322 | + The closer the window's ratio is to the egg's ratio (2/3), the more |
323 | + overlap between the eggs. |
324 | + """ |
325 | + super_area = super(SmallEgg, self).calculate_egg_area(area) |
326 | + w = super_area.width * self.size_ratio |
327 | + h = super_area.height * self.size_ratio |
328 | + x = self.padding * area.width |
329 | + y = (1 - 0.5 * self.padding) * area.height - h |
330 | + return gtk.gdk.Rectangle(int(x), int(y), int(w), int(h)) |
331 | + |
332 | +if __name__ == "__main__": |
333 | + area = gtk.gdk.Rectangle(0, 0, 300, 400) |
334 | + DrawingInterface(area) |
335 | + gtk.main() |
336 | + |