Merge lp:~brunogirin/python-snippets/cairo-easter-egg into lp:~jonobacon/python-snippets/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
Reviewer Review Type Date Requested Status
Jono Bacon Pending
Review via email: mp+22916@code.launchpad.net

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+

Subscribers

People subscribed via source and target branches