graphics 1.0.0b1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ ##
2
+ # The top-level namespace.
3
+
4
+ class Graphics
5
+ VERSION = "1.0.0b1" # :nodoc:
6
+ end
7
+
8
+ require "graphics/simulation"
9
+ require "graphics/body"
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "graphics/v"
4
+ require "graphics/extensions"
5
+
6
+ ##
7
+ # A body in the simulation.
8
+ #
9
+ # All bodies know their position, their angle, goal angle (optional),
10
+ # and momentum.
11
+
12
+ class Graphics::Body
13
+
14
+ # degrees to radians
15
+ D2R = Graphics::Simulation::D2R
16
+
17
+ # radians to degrees
18
+ R2D = Graphics::Simulation::R2D
19
+
20
+ ##
21
+ # The normals for the cardinal directions.
22
+
23
+ NORMAL = {
24
+ :north => 270,
25
+ :south => 90,
26
+ :east => 180,
27
+ :west => 0,
28
+ }
29
+
30
+ attr_accessor :x, :y, :a, :ga, :m, :w # :nodoc:
31
+
32
+ ##
33
+ # Create a new body in windowing system +w+ with a random x/y and
34
+ # everything else zero'd out.
35
+
36
+ def initialize w
37
+ self.w = w
38
+
39
+ self.x, self.y = rand(w.w), rand(w.h)
40
+ self.a = 0.0
41
+ self.ga = 0.0
42
+ self.m = 0.0
43
+ end
44
+
45
+ def inspect # :nodoc:
46
+ "%s(%.2fx%.2f @ %.2f°x%.2f == %p @ %p)" %
47
+ [self.class, x, y, a, m, position, velocity]
48
+ end
49
+
50
+ ##
51
+ # Convert the body to a vector representing its velocity.
52
+ #
53
+ # DO NOT modify this vector expecting it to modify the body. It is a
54
+ # copy.
55
+
56
+ def velocity
57
+ x, y = dx_dy
58
+ V[x, y]
59
+ end
60
+
61
+ ##
62
+ # Set the body's magnitude and angle from a velocity vector.
63
+
64
+ def velocity= o
65
+ dx, dy = o.x, o.y
66
+ self.m = Math.sqrt(dx*dx + dy*dy)
67
+ self.a = Math.atan2(dy, dx) * R2D
68
+ end
69
+
70
+ ##
71
+ # Convert the body to a vector representing its position.
72
+ #
73
+ # DO NOT modify this vector expecting it to modify the body. It is a
74
+ # copy.
75
+
76
+ def position
77
+ V[x, y]
78
+ end
79
+
80
+ ##
81
+ # Set the body's position from a velocity vector.
82
+
83
+ def position= o
84
+ self.x = o.x
85
+ self.y = o.y
86
+ end
87
+
88
+ def dx_dy # :nodoc:
89
+ rad = a * D2R
90
+ dx = Math.cos(rad) * m
91
+ dy = Math.sin(rad) * m
92
+ [dx, dy]
93
+ end
94
+
95
+ def m_a # :nodoc:
96
+ [m, a]
97
+ end
98
+
99
+ ##
100
+ # Turn the body +dir+ degrees.
101
+
102
+ def turn dir
103
+ self.a = (a + dir).degrees if dir
104
+ end
105
+
106
+ ##
107
+ # Move the body via its current angle and momentum.
108
+
109
+ def move
110
+ move_by a, m
111
+ end
112
+
113
+ ##
114
+ # Move the body by a specified angle and momentum.
115
+
116
+ def move_by a, m
117
+ rad = a * D2R
118
+ self.x += Math.cos(rad) * m
119
+ self.y += Math.sin(rad) * m
120
+ end
121
+
122
+ ##
123
+ # Keep the body in bounds of the window. If it went out of bounds,
124
+ # set its position to be on that bound and return the cardinal
125
+ # direction of the wall it hit.
126
+ #
127
+ # See also: NORMALS
128
+
129
+ def clip
130
+ max_h, max_w = w.h, w.w
131
+
132
+ if x < 0 then
133
+ self.x = 0
134
+ return :west
135
+ elsif x > max_w then
136
+ self.x = max_w
137
+ return :east
138
+ end
139
+
140
+ if y < 0 then
141
+ self.y = 0
142
+ return :north
143
+ elsif y > max_h then
144
+ self.y = max_h
145
+ return :south
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ ##
152
+ # Return a random angle 0...360.
153
+
154
+ def random_angle
155
+ 360 * rand
156
+ end
157
+
158
+ ###
159
+ # Randomly turn the body inside an arc of +deg+ degrees from where
160
+ # it is currently facing.
161
+
162
+ def random_turn deg
163
+ rand(deg) - (deg/2)
164
+ end
165
+
166
+ ##
167
+ # clip and then set the goal angle to the normal plus or minus a
168
+ # random 45 degrees.
169
+
170
+ def clip_off_wall
171
+ if wall = clip then
172
+ normal = NORMAL[wall]
173
+ self.ga = (normal + random_turn(90)).degrees unless (normal - ga).abs < 45
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Like clip, keep the body in bounds of the window, but set the
179
+ # angle to the angle of reflection. Also slows momentum by 20%.
180
+
181
+ def bounce
182
+ # TODO: rewrite this using clip + NORMAL to clean it up
183
+ max_h, max_w = w.h, w.w
184
+ normal = nil
185
+
186
+ if x < 0 then
187
+ self.x, normal = 0, 0
188
+ elsif x > max_w then
189
+ self.x, normal = max_w, 180
190
+ end
191
+
192
+ if y < 0 then
193
+ self.y, normal = 0, 90
194
+ elsif y > max_h then
195
+ self.y, normal = max_h, 270
196
+ end
197
+
198
+ if normal then
199
+ self.a = (2 * normal - 180 - a).degrees
200
+ self.m *= 0.8
201
+ end
202
+ end
203
+
204
+ ##
205
+ # Wrap the body if it hits an edge.
206
+
207
+ def wrap
208
+ max_h, max_w = w.h, w.w
209
+
210
+ self.x = max_w if x < 0
211
+ self.y = max_h if y < 0
212
+
213
+ self.x = 0 if x > max_w
214
+ self.y = 0 if y > max_h
215
+ end
216
+ end
@@ -0,0 +1,48 @@
1
+ class Integer
2
+ ##
3
+ # Calculate a random chance using easy notation: 1 =~ 50 :: 1 in 50 chance
4
+
5
+ def =~ n #
6
+ rand(n) <= (self - 1)
7
+ end
8
+ end
9
+
10
+ class Numeric
11
+ ##
12
+ # Is M close to N within a certain delta?
13
+
14
+ def close_to? n, delta = 0.01
15
+ (self - n).abs < delta
16
+ end
17
+
18
+ ##
19
+ # Normalize a number to be within 0...360
20
+
21
+ def degrees
22
+ (self < 0 ? self + 360 : self) % 360
23
+ end
24
+
25
+ ##
26
+ # I am honestly befuddled by this code, and I wrote it.
27
+ #
28
+ # I should probably remove it and start over.
29
+ #
30
+ # Consider this method private, even tho it is in use by the demos.
31
+
32
+ def relative_angle n, max
33
+ deltaCW = (self - n).degrees
34
+ deltaCC = (n - self).degrees
35
+
36
+ return if deltaCC < 0.1 || deltaCW < 0.1
37
+
38
+ if deltaCC.abs < max then
39
+ deltaCC
40
+ elsif deltaCW.close_to? 180 then
41
+ [-max, max].sample
42
+ elsif deltaCW < deltaCC then
43
+ -max
44
+ else
45
+ max
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,377 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "sdl"
4
+
5
+ module SDL; end # :nodoc: -- stupid rdoc :(
6
+
7
+ ##
8
+ # A simulation. This ties everything together and provides a bunch of
9
+ # convenience methods to make life easier.
10
+
11
+ class Graphics::Simulation
12
+
13
+ # degrees to radians
14
+ D2R = Math::PI / 180.0
15
+
16
+ # radians to degrees
17
+ R2D = 1 / D2R
18
+
19
+ # The window the simulation is drawing in.
20
+ attr_accessor :screen
21
+
22
+ # The window width.
23
+ attr_accessor :w
24
+
25
+ # The window height.
26
+ attr_accessor :h
27
+
28
+ # Pause the simulation.
29
+ attr_accessor :paused
30
+
31
+ # The current font for rendering text.
32
+ attr_accessor :font
33
+
34
+ # A hash of color names to their values.
35
+
36
+ attr_accessor :color
37
+
38
+ # A hash of color values to their rgb values. For text, apparently. *shrug*
39
+ attr_accessor :rgb
40
+
41
+ ##
42
+ # Create a new simulation of a certain width and height. Optionally,
43
+ # you can set the bits per pixel (0 for current screen settings),
44
+ # the name of the window, and whether or not to run in full screen mode.
45
+ #
46
+ # This also names a bunch colors and hues for convenience.
47
+
48
+ def initialize w, h, bpp = 0, name = self.class.name, full = false
49
+ SDL.init SDL::INIT_VIDEO
50
+ SDL::TTF.init
51
+
52
+ full = full ? SDL::FULLSCREEN : 0
53
+
54
+ self.font = SDL::TTF.open("/System/Library/Fonts/Menlo.ttc", 32, 0)
55
+
56
+ SDL::WM.set_caption name, name
57
+
58
+ self.screen = SDL::Screen.open w, h, bpp, SDL::HWSURFACE|SDL::DOUBLEBUF|full
59
+ self.w, self.h = screen.w, screen.h
60
+
61
+ self.color = {}
62
+ self.rgb = Hash.new { |hash, k| hash[k] = screen.get_rgb(color[k]) }
63
+
64
+ register_color :black, 0, 0, 0
65
+ register_color :white, 255, 255, 255
66
+ register_color :red, 255, 0, 0
67
+ register_color :green, 0, 255, 0
68
+ register_color :blue, 0, 0, 255
69
+ register_color :gray, 127, 127, 127
70
+ register_color :yellow, 255, 255, 0
71
+ register_color :alpha, 0, 0, 0, 0
72
+
73
+ (0..99).each do |n|
74
+ m = (255 * (n / 100.0)).to_i
75
+ register_color ("gray%02d" % n).to_sym, m, m, m
76
+ register_color ("red%02d" % n).to_sym, m, 0, 0
77
+ register_color ("green%02d" % n).to_sym, 0, m, 0
78
+ register_color ("blue%02d" % n).to_sym, 0, 0, m
79
+ end
80
+
81
+ self.paused = false
82
+ end
83
+
84
+ ##
85
+ # Name a color w/ rgba values.
86
+
87
+ def register_color name, r, g, b, a = 255
88
+ color[name] = screen.format.map_rgba r, g, b, a
89
+ end
90
+
91
+ ##
92
+ # Return an array populated by instances of +klass+. You can specify
93
+ # how many to create here or it will access +klass::COUNT+ as the
94
+ # default.
95
+
96
+ def populate klass, n = klass::COUNT
97
+ n.times.map {
98
+ o = klass.new self
99
+ yield o if block_given?
100
+ o }
101
+ end
102
+
103
+ ##
104
+ # Handle an event. By default only handles the Quit event. Override
105
+ # if you want to add more handlers. Be sure to call super or you
106
+ # won't be able to quit.
107
+
108
+ def handle_event event, n
109
+ exit if SDL::Event::Quit === event
110
+ end
111
+
112
+ ##
113
+ # Handle key events. By default handles ESC & Q (quit) and P
114
+ # (pause). Override this if you want to handle more key events. Be
115
+ # sure to call super or provide your own means of quitting and/or
116
+ # pausing.
117
+
118
+ def handle_keys
119
+ exit if SDL::Key.press? SDL::Key::ESCAPE
120
+ exit if SDL::Key.press? SDL::Key::Q
121
+ self.paused = !paused if SDL::Key.press? SDL::Key::P
122
+ end
123
+
124
+ ##
125
+ # Run the simulation. This handles all events by polling and
126
+ # scanning for key presses (multiple keys at once are possible).
127
+ #
128
+ # On each tick, call update, then draw the scene.
129
+
130
+ def run
131
+ self.start_time = Time.now
132
+ n = 0
133
+ event = nil
134
+ loop do
135
+ handle_event event, n while event = SDL::Event.poll
136
+ SDL::Key.scan
137
+ handle_keys
138
+
139
+ unless paused then
140
+ update n unless paused
141
+
142
+ draw_and_flip n
143
+
144
+ n += 1 unless paused
145
+ end
146
+ end
147
+ end
148
+
149
+ def draw_and_flip n # :nodoc:
150
+ self.draw n
151
+ screen.flip
152
+ end
153
+
154
+ ##
155
+ # Draw the scene. This is a subclass responsibility and must draw
156
+ # the entire window (including calling clear).
157
+
158
+ def draw n
159
+ raise NotImplementedError, "Subclass Responsibility"
160
+ end
161
+
162
+ ##
163
+ # Update the simulation. This does nothing by default and must be
164
+ # overridden by the subclass.
165
+
166
+ def update n
167
+ # do nothing
168
+ end
169
+
170
+ ##
171
+ # Clear the whole screen
172
+
173
+ def clear c = :black
174
+ fast_rect 0, 0, w, h, c
175
+ end
176
+
177
+ ##
178
+ # Draw an antialiased line from x1/y1 to x2/y2 in color c.
179
+
180
+ def line x1, y1, x2, y2, c
181
+ h = self.h
182
+ screen.draw_line x1, h-y1, x2, h-y2, color[c], :antialiased
183
+ end
184
+
185
+ ##
186
+ # Draw a horizontal line from x1 to x2 at y in color c.
187
+
188
+ def hline y, c, x1 = 0, x2 = h
189
+ line x1, y, x2, y, c
190
+ end
191
+
192
+ ##
193
+ # Draw a vertical line from y1 to y2 at y in color c.
194
+
195
+ def vline x, c, y1 = 0, y2 = w
196
+ line x, y1, x, y2, c
197
+ end
198
+
199
+ ##
200
+ # Draw a closed form polygon from an array of points in a particular
201
+ # color.
202
+
203
+ def polygon *points, color
204
+ points << points.first
205
+ points.each_cons(2) do |p1, p2|
206
+ w.line(*p1, *p2, color)
207
+ end
208
+ end
209
+
210
+ ##
211
+ # Draw a line from x1/y1 to a particular magnitude and angle in color c.
212
+
213
+ def angle x1, y1, a, m, c
214
+ x2, y2 = project x1, y1, a, m
215
+ line x1, y1, x2, y2, c
216
+ end
217
+
218
+ ##
219
+ # Draw a rect at x/y with w by h dimensions in color c. Ignores blending.
220
+
221
+ def fast_rect x, y, w, h, c
222
+ screen.fill_rect x, self.h-y-h, w, h, color[c]
223
+ end
224
+
225
+ ##
226
+ # Draw a point at x/y w/ color c.
227
+
228
+ def point x, y, c
229
+ screen[x, h-y] = color[c]
230
+ end
231
+
232
+ ##
233
+ # Calculate the x/y coordinate offset from x1/y1 with an angle and a
234
+ # magnitude.
235
+
236
+ def project x1, y1, a, m
237
+ rad = a * D2R
238
+ [x1 + Math.cos(rad) * m, y1 + Math.sin(rad) * m]
239
+ end
240
+
241
+ ##
242
+ # Draw a rect at x/y with w by h dimensions in color c.
243
+
244
+ def rect x, y, w, h, c, fill = false
245
+ screen.draw_rect x, self.h-y-h, w, h, color[c], fill
246
+ end
247
+
248
+ ##
249
+ # Draw a circle at x/y with radius r in color c.
250
+
251
+ def circle x, y, r, c, fill = false
252
+ screen.draw_circle x, h-y, r, color[c], fill, :antialiased
253
+ end
254
+
255
+ ##
256
+ # Draw a circle at x/y with radiuses w/h in color c.
257
+
258
+ def ellipse x, y, w, h, c, fill = false
259
+ screen.draw_ellipse x, self.h-y, w, h, color[c], fill, :antialiased
260
+ end
261
+
262
+ ##
263
+ # Draw an antialiased curve from x1/y1 to x2/y2 via control points
264
+ # cx1/cy1 & cx2/cy2 in color c.
265
+
266
+ def bezier x1, y1, cx1, cy1, cx2, cy2, x2, y2, c, l = 7
267
+ h = self.h
268
+ screen.draw_bezier x1, h-y1, cx1, h-cy1, cx2, h-cy2, x2, h-y2, l, color[c], :antialiased
269
+ end
270
+
271
+ ## Text
272
+
273
+ ##
274
+ # Return the w/h of the text s in font f.
275
+
276
+ def text_size s, f = font
277
+ f.text_size s
278
+ end
279
+
280
+ ##
281
+ # Return the rendered text s in color c in font f.
282
+
283
+ def render_text s, c, f = font
284
+ f.render_solid_utf8 s, *rgb[c]
285
+ end
286
+
287
+ ##
288
+ # Draw text s at x/y in color c in font f.
289
+
290
+ def text s, x, y, c, f = font
291
+ f.draw_solid_utf8 screen, s, x, self.h-y-f.height, *rgb[c]
292
+ end
293
+
294
+ ##
295
+ # Print out some extra debugging information underneath the fps line
296
+ # (if any).
297
+
298
+ def debug fmt, *args
299
+ s = fmt % args
300
+ text s, 10, h-40-font.height, :white
301
+ end
302
+
303
+ attr_accessor :start_time # :nodoc:
304
+
305
+ ##
306
+ # Draw the current frames-per-second in the top left corner in green.
307
+
308
+ def fps n
309
+ secs = Time.now - start_time
310
+ fps = "%5.1f fps" % [n / secs]
311
+ text fps, 10, h-font.height, :green
312
+ end
313
+
314
+ ### Blitting Methods:
315
+
316
+ ## utilities for later
317
+
318
+ # put_pixel(x, y, color)
319
+ # []=(x, y, color)
320
+ # get_pixel(x, y)
321
+ # [](x, y)
322
+ # put(src, x, y) # see blit
323
+ # copy_rect(x,y,w,h)
324
+ # transform_surface(bgcolor,angle,xscale,yscale,flags)
325
+
326
+ ##
327
+ # Load an image at path into a new surface.
328
+
329
+ def image path
330
+ SDL::Surface.load path
331
+ end
332
+
333
+ ##
334
+ # Return the current mouse state: x, y, buttons.
335
+
336
+ def mouse
337
+ r = SDL::Mouse.state
338
+ r[1] = h-r[1]
339
+ r
340
+ end
341
+
342
+ ##
343
+ # Draw a bitmap at x/y with an angle and optional x/y scale.
344
+
345
+ def blit o, x, y, a°, xs=1, ys=1, opt=0
346
+ SDL::Surface.transform_blit o, screen, -a°, 1, 1, o.w/2, o.h/2, x, h-y, opt
347
+ end
348
+
349
+ ##
350
+ # Create a new sprite with a given width and height and yield to a
351
+ # block with the new sprite as the current screen. All drawing
352
+ # primitives will work and the resulting surface is returned.
353
+
354
+ def sprite w, h
355
+ new_screen = SDL::Surface.new SDL::SWSURFACE, w, h, screen
356
+ old_screen = screen
357
+ old_w, old_h = self.w, self.h
358
+ self.w, self.h = w, h
359
+
360
+ self.screen = new_screen
361
+ yield if block_given?
362
+
363
+ new_screen.set_color_key SDL::SRCCOLORKEY, 0
364
+
365
+ new_screen
366
+ ensure
367
+ self.screen = old_screen
368
+ self.w, self.h = old_w, old_h
369
+ end
370
+ end
371
+
372
+ if $0 == __FILE__ then
373
+ SDL.init SDL::INIT_EVERYTHING
374
+ SDL.set_video_mode(640, 480, 16, SDL::SWSURFACE)
375
+ sleep 1
376
+ puts "if you saw a window, it was working"
377
+ end