Class: Graphics::AbstractSimulation

Inherits:
Object
  • Object
show all
Defined in:
lib/graphics/simulation.rb

Overview

An abstract simulation. See Graphics::Simulation and Graphics::Drawing.

Direct Known Subclasses

Drawing, Simulation

Constant Summary collapse

SCREEN_FLAGS =

Flags to be used when initializing a window. Defaults to 0. See SDL doco for more.

0
CLEAR_COLOR =

The default color to clear the window.

:black
DEBUG_COLOR =

The default font color for ‘debug` calls.

:white
D2R =

degrees to radians

Math::PI / 180.0
R2D =

radians to degrees

1 / D2R
LOG_INTERVAL =

Call log every N ticks, if log is defined.

60
DEFAULT_FONT =

The default font. Menlo on OS X, Deja Vu Sans Mono on linux.

case RUBY_PLATFORM
when /darwin/ then "Menlo"
when /linux/  then "DejaVuSansMono"
else
  raise "Unsupported platform #{RUBY_PLATFORM}. Please fix."
end
FONT_GLOB =

:nodoc:

"{#{font_dirs.join(",")}}"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(w = nil, h = nil, name = self.class.name, full = false) ⇒ AbstractSimulation

Create a new simulation of a certain width and height. Optionally, you can set the bits per pixel (0 for current screen settings), the name of the window, and whether or not to run in full screen mode.

This also names a bunch colors and hues for convenience.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/graphics/simulation.rb', line 120

def initialize w=nil, h=nil, name=self.class.name, full=false
  w ||= SDL::Screen::W/2
  h ||= SDL::Screen::H/2

  # TODO: remove for 1.0.0 final
  raise "Do NOT pass bpp to Simulation anymore" if !name || Integer === name

  full = full ? SDL::FULLSCREEN : 0

  self._bodies = []

  self.font = find_font(DEFAULT_FONT, 32)

  name ||= "Unknown"
  name = name.gsub(/[A-Z]/, ' \0').strip

  self.renderer = SDL::Screen.open w, h, 32, self.class::SCREEN_FLAGS|full
  self.w, self.h = w, h

  renderer.title = name

  self.color = {}
  self.paused = false

  self.iter_per_tick = 1

  self.key_handler = []
  self.keydown_handler = {}

  initialize_keys
  initialize_colors

  clear # so you start with the right color blank window on frame 0
  renderer.present
end

Instance Attribute Details

#_bodiesObject

Collection of collections of Bodies to auto-update and draw.



81
82
83
# File 'lib/graphics/simulation.rb', line 81

def _bodies
  @_bodies
end

#colorObject

A hash of color names to their values.



99
100
101
# File 'lib/graphics/simulation.rb', line 99

def color
  @color
end

#doneObject

Is the application done?



111
112
113
# File 'lib/graphics/simulation.rb', line 111

def done
  @done
end

#fontObject

The current font for rendering text.



96
97
98
# File 'lib/graphics/simulation.rb', line 96

def font
  @font
end

#hObject

The window height.



90
91
92
# File 'lib/graphics/simulation.rb', line 90

def h
  @h
end

#iter_per_tickObject

Number of update iterations per drawing tick.



102
103
104
# File 'lib/graphics/simulation.rb', line 102

def iter_per_tick
  @iter_per_tick
end

#key_handlerObject

Procs registered to handle key events.



105
106
107
# File 'lib/graphics/simulation.rb', line 105

def key_handler
  @key_handler
end

#keydown_handlerObject

Procs registered to handle keydown events.



108
109
110
# File 'lib/graphics/simulation.rb', line 108

def keydown_handler
  @keydown_handler
end

#pausedObject

Pause the simulation.



93
94
95
# File 'lib/graphics/simulation.rb', line 93

def paused
  @paused
end

#rendererObject

The renderer (software or hardware backed) the simulation is drawing in.



84
85
86
# File 'lib/graphics/simulation.rb', line 84

def renderer
  @renderer
end

#start_timeObject

:nodoc:



619
620
621
# File 'lib/graphics/simulation.rb', line 619

def start_time
  @start_time
end

#wObject

The window width.



87
88
89
# File 'lib/graphics/simulation.rb', line 87

def w
  @w
end

Instance Method Details

#add_key_handler(k, remove = nil, &b) ⇒ Object

Register a block to run for a particular key-press. This allows you to register multiple blocks for the same key and also to handle multiple keys down at the same time.



351
352
353
354
355
# File 'lib/graphics/simulation.rb', line 351

def add_key_handler k, remove = nil, &b
  k = SDL::Key.const_get k
  key_handler.delete_if { |a, _| k==a } if remove
  key_handler.unshift [k, b]
end

#add_keydown_handler(k, &b) ⇒ Object

Register a block to run for a particular keydown event. This is a single key handler per tick and only on a key-down event.



361
362
363
# File 'lib/graphics/simulation.rb', line 361

def add_keydown_handler k, &b
  keydown_handler[k] = b
end

#angle(x1, y1, a, m, c) ⇒ Object

Draw a line from x1/y1 to a particular magnitude and angle in color c.



513
514
515
516
# File 'lib/graphics/simulation.rb', line 513

def angle x1, y1, a, m, c
  x2, y2 = project x1, y1, a, m
  line x1, y1, x2, y2, c
end

#audio(path) ⇒ Object

Load an audio file at path



642
643
644
# File 'lib/graphics/simulation.rb', line 642

def audio path
  SDL::Audio.load path
end

#bezier(*points, c) ⇒ Object

Draw an antialiased curve from x1/y1 to x2/y2 via control points cx1/cy1 & cx2/cy2 in color c.



576
577
578
579
580
581
582
583
584
# File 'lib/graphics/simulation.rb', line 576

def bezier *points, c
  h = self.h-1

  # TODO: there is probably a cleaner way... or move entirely into C
  xs, ys = points.each_slice(2).to_a.transpose
  ys.map! { |y| h-y }

  renderer.draw_bezier xs, ys, 5, color[c]
end

#blit(src, x, y, a° = nil, xscale = nil, yscale = nil, flags = nil) ⇒ Object

Draw a bitmap centered at x/y with optional angle, x/y scale, and flags.



665
666
667
# File 'lib/graphics/simulation.rb', line 665

def blit src, x, y,  = nil, xscale = nil, yscale = nil, flags = nil
  renderer.blit src, x-src.w/2, h-y-src.h/2, , xscale, yscale, :center
end

#circle(x, y, r, c, fill = false, aa = true) ⇒ Object

Draw a circle at x/y with radius r in color c.



559
560
561
562
# File 'lib/graphics/simulation.rb', line 559

def circle x, y, r, c, fill = false, aa = true
  y = h-y-1
  renderer.draw_circle x, y, r, color[c], aa, fill
end

#clear(c = self.class::CLEAR_COLOR) ⇒ Object

Clear the whole window. Defaults to CLEAR_COLOR.



465
466
467
468
469
470
471
472
# File 'lib/graphics/simulation.rb', line 465

def clear c = self.class::CLEAR_COLOR
  cc = color[c]
  if cc then
    renderer.clear cc
  else
    warn "Color #{c} doesn't appear to be registered. Skipping clear."
  end
end

#debug(fmt, *args) ⇒ Object

Print out some extra debugging information underneath the fps line (if any).



614
615
616
617
# File 'lib/graphics/simulation.rb', line 614

def debug fmt, *args
  s = fmt % args
  text s, 10, h-40-font.height, self.class::DEBUG_COLOR
end

#draw(n) ⇒ Object

Draw the scene by clearing the window and drawing all registered bodies. You are free to completely override this or call super and add any extras at the end.



415
416
417
418
# File 'lib/graphics/simulation.rb', line 415

def draw n
  pre_draw n
  post_draw n
end

#draw_and_flip(n) ⇒ Object

:nodoc:



405
406
407
408
# File 'lib/graphics/simulation.rb', line 405

def draw_and_flip n # :nodoc:
  self.draw n
  renderer.present
end

#draw_collection(ary) ⇒ Object

Draw a homogeneous collection of bodies. This assumes that the MVC pattern described on this class is being used.



441
442
443
444
445
446
447
448
449
# File 'lib/graphics/simulation.rb', line 441

def draw_collection ary
  return if ary.empty?

  cls = ary.first.class.const_get :View

  ary.each do |obj|
    cls.draw self, obj
  end
end

#ellipse(x, y, w, h, c, fill = false, aa = true) ⇒ Object

Draw a circle at x/y with radiuses w/h in color c.



567
568
569
570
# File 'lib/graphics/simulation.rb', line 567

def ellipse x, y, w, h, c, fill = false, aa = true
  y = self.h-y-1
  renderer.draw_ellipse x, y, w, h, color[c], aa, fill
end

#fast_rect(x, y, w, h, c) ⇒ Object

Draw a rect at x/y with w by h dimensions in color c. Ignores blending.



521
522
523
524
# File 'lib/graphics/simulation.rb', line 521

def fast_rect x, y, w, h, c
  y = self.h-y-h # TODO: -1???
  renderer.fast_rect x, y, w, h, color[c]
end

#find_font(name, size = 16) ⇒ Object

Find and open a (TTF) font. Should be as system agnostic as possible.

Raises:

  • (ArgumentError)


216
217
218
219
220
221
222
# File 'lib/graphics/simulation.rb', line 216

def find_font name, size = 16
  font = Dir["#{FONT_GLOB}/#{name}.{ttc,ttf}"].first

  raise ArgumentError, "Can't find font named '#{name}'" unless font

  SDL::TTF.open(font, size)
end

#fps(n, color = :green) ⇒ Object

Draw the current frames-per-second in the top left corner. Defaults to green.



624
625
626
627
628
# File 'lib/graphics/simulation.rb', line 624

def fps n, color = :green
  secs = Time.now - start_time
  fps = "%5.1f fps" % [n / secs]
  text fps, 10, h-font.height, color
end

#from_hsl(h, s, l) ⇒ Object

Raises:

  • (ArgumentError)


266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/graphics/simulation.rb', line 266

def from_hsl h, s, l # 0..360, 0..1, 0..1
  raise ArgumentError, "%f, %f, %f out of range" % [h, s, v] unless
    h.between?(0, 360) && s.between?(0, 1) && l.between?(0, 1)

  c  = (1 - (2*l - 1).abs) * s
  h2 = h / 60.0
  x  = c * (1 - (h2 % 2 - 1).abs)
  m  = l - c/2

  r, g, b = case
            when 0 <= h2 && h2 < 1 then [c+m, x+m, 0+m]
            when 1 <= h2 && h2 < 2 then [x+m, c+m, 0+m]
            when 2 <= h2 && h2 < 3 then [0+m, c+m, x+m]
            when 3 <= h2 && h2 < 4 then [0+m, x+m, c+m]
            when 4 <= h2 && h2 < 5 then [x+m, 0+m, c+m]
            when 5 <= h2 && h2 < 6 then [c+m, 0+m, x+m]
            else
              raise [h, s, v, h2, x, m].inspect
            end

  [(r*255).round, (g*255).round, (b*255).round]
end

#from_hsv(h, s, v) ⇒ Object

Raises:

  • (ArgumentError)


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/graphics/simulation.rb', line 294

def from_hsv h, s, v # 0..360, 0..1, 0..1
  raise ArgumentError, "%f, %f, %f out of range" % [h, s, v] unless
    h.between?(0, 360) && s.between?(0, 1) && v.between?(0, 1)

  c  = v * s
  h2 = h / 60.0
  x  = c * (1 - (h2 % 2 - 1).abs)
  m  = v - c

  r, g, b = case
            when 0 <= h2 && h2 < 1 then [c+m, x+m, 0+m]
            when 1 <= h2 && h2 < 2 then [x+m, c+m, 0+m]
            when 2 <= h2 && h2 < 3 then [0+m, c+m, x+m]
            when 3 <= h2 && h2 < 4 then [0+m, x+m, c+m]
            when 4 <= h2 && h2 < 5 then [x+m, 0+m, c+m]
            when 5 <= h2 && h2 < 6 then [c+m, 0+m, x+m]
            else
              raise [h, s, v, h2, x, m].inspect
            end

  [(r*255).round, (g*255).round, (b*255).round]
end

#handle_event(event, n) ⇒ Object

Handle an event. By default only handles the Quit event. Override if you want to add more handlers. Be sure to call super or you won’t be able to quit.



335
336
337
338
339
340
341
342
343
344
# File 'lib/graphics/simulation.rb', line 335

def handle_event event, n
  case event
  when SDL::Event::Quit then
    exit
  when SDL::Event::Keydown then
    c = event.sym.chr rescue nil
    b = keydown_handler[c]
    b[self] if b
  end
end

#handle_keysObject

Handle key events by looking through key_handler and running any blocks that match the key(s) being pressed.



369
370
371
372
373
374
# File 'lib/graphics/simulation.rb', line 369

def handle_keys
  SDL::Key.scan
  key_handler.each do |k, blk|
    blk[self] if SDL::Key.press? k
  end
end

#hline(y, c, x1 = 0, x2 = w) ⇒ Object

Draw a horizontal line from x1 to x2 at y in color c.



485
486
487
# File 'lib/graphics/simulation.rb', line 485

def hline y, c, x1 = 0, x2 = w
  line x1, y, x2, y, c
end

#image(path) ⇒ Object

Load an image at path into a new surface.



635
636
637
# File 'lib/graphics/simulation.rb', line 635

def image path
  SDL::Surface.load path
end

#initialize_colorsObject

:nodoc:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/graphics/simulation.rb', line 167

def initialize_colors # :nodoc:
  register_color :black,     0,   0,   0
  register_color :white,     255, 255, 255
  register_color :gray,      127, 127, 127
  register_color :red,       255, 0,   0
  register_color :green,     0,   255, 0
  register_color :blue,      0,   0,   255
  register_color :cyan,      0,   255, 255
  register_color :magenta,   255, 0,   255
  register_color :yellow,    255, 255, 0
  register_color :alpha,     0,   0,   0,   0

  (0..99).each do |n|
    m = (255 * (n / 100.0)).to_i
    register_color(("gray%02d"    % n).to_sym, m, m, m)
    register_color(("red%02d"     % n).to_sym, m, 0, 0)
    register_color(("green%02d"   % n).to_sym, 0, m, 0)
    register_color(("blue%02d"    % n).to_sym, 0, 0, m)
    register_color(("cyan%02d"    % n).to_sym, 0, m, m)
    register_color(("magenta%02d" % n).to_sym, m, 0, m)
    register_color(("yellow%02d"  % n).to_sym, m, m, 0)
  end

  (0...256).each do |n|
    m = (256 * n / 255.0).to_i
    register_color(("gray%03d"    % n).to_sym, m, m, m)
    register_color(("red%03d"     % n).to_sym, m, 0, 0)
    register_color(("green%03d"   % n).to_sym, 0, m, 0)
    register_color(("blue%03d"    % n).to_sym, 0, 0, m)
    register_color(("cyan%03d"    % n).to_sym, 0, m, m)
    register_color(("magenta%03d" % n).to_sym, m, 0, m)
    register_color(("yellow%03d"  % n).to_sym, m, m, 0)
  end
end

#initialize_keysObject

Register default key events. Handles ESC & Q (quit) and P (pause).



159
160
161
162
163
164
165
# File 'lib/graphics/simulation.rb', line 159

def initialize_keys
  add_keydown_handler("\e") { self.done = true }
  add_keydown_handler("q")  { self.done = true }
  add_keydown_handler("p")  { self.paused = !paused }
  add_keydown_handler("/")  { self.iter_per_tick += 1 }
  add_keydown_handler("-")  { self.iter_per_tick -= 1; self.iter_per_tick = 1  if iter_per_tick < 1 }
end

#line(x1, y1, x2, y2, c, aa = true) ⇒ Object

Draw an antialiased line from x1/y1 to x2/y2 in color c.



477
478
479
480
# File 'lib/graphics/simulation.rb', line 477

def line x1, y1, x2, y2, c, aa = true
  h = self.h
  renderer.draw_line x1, h-y1-1, x2, h-y2-1, color[c], aa
end

#mouseObject

Return the current mouse state: x, y, buttons.



656
657
658
659
660
# File 'lib/graphics/simulation.rb', line 656

def mouse
  r = SDL::Mouse.state
  r[1] = h-r[1]
  r
end

#open_mixer(channels = 1) ⇒ Object

Open the audio mixer with a number of channels open.



649
650
651
# File 'lib/graphics/simulation.rb', line 649

def open_mixer channels = 1
  SDL::Audio.open channels
end

#point(x, y, c = nil) ⇒ Object

Read or write a color to x/y. If c is given, write, otherwise read.

Reading is pretty slow. Try to avoid.



531
532
533
534
535
536
537
# File 'lib/graphics/simulation.rb', line 531

def point x, y, c = nil
  if c then
    renderer[x, h-y-1] = color[c]
  else
    renderer[x, h-y-1]
  end
end

#polygon(points, c) ⇒ Object

Draw a closed form polygon from an array of points in a particular color.



500
501
502
503
504
505
506
507
508
# File 'lib/graphics/simulation.rb', line 500

def polygon points, c
  return unless points.size > 1

  points << points.first

  xs, ys = points.transpose

  renderer.draw_polygon xs, ys.map { |y| h-y-1 }, c, :aa
end

#populate(klass, n = klass::COUNT) ⇒ Object

Return an array populated by instances of klass. You can specify how many to create here or it will access klass::COUNT as the default.



322
323
324
325
326
327
328
# File 'lib/graphics/simulation.rb', line 322

def populate klass, n = klass::COUNT
  n.times.map {
    o = klass.new self
    yield o if block_given?
    o
  }
end

#post_draw(n) ⇒ Object

The post-draw phase. Defaults to having all bodies draw themselves.



431
432
433
434
435
# File 'lib/graphics/simulation.rb', line 431

def post_draw n
  _bodies.each do |ary|
    draw_collection ary
  end
end

#pre_draw(n) ⇒ Object

The pre-draw phase. Defaults to clearing.



423
424
425
# File 'lib/graphics/simulation.rb', line 423

def pre_draw n
  clear
end

#project(x1, y1, a, m) ⇒ Object

Calculate the x/y coordinate offset from x1/y1 with an angle and a magnitude.



543
544
545
546
# File 'lib/graphics/simulation.rb', line 543

def project x1, y1, a, m
  rad = a * D2R
  [x1 + Math.cos(rad) * m, y1 + Math.sin(rad) * m]
end

#put(src, x, y, a° = nil, xscale = nil, yscale = nil, flags = nil) ⇒ Object

Draw a bitmap at x/y with optional angle, x/y scale, and flags.



672
673
674
# File 'lib/graphics/simulation.rb', line 672

def put src, x, y,  = nil, xscale = nil, yscale = nil, flags = nil
  renderer.blit src, x, h-y-src.h, , xscale, yscale, false
end

#rect(x, y, w, h, c, fill = false) ⇒ Object

Draw a rect at x/y with w by h dimensions in color c.



551
552
553
554
# File 'lib/graphics/simulation.rb', line 551

def rect x, y, w, h, c, fill = false
  y = self.h-y-h # TODO: -1???
  renderer.draw_rect x, y, w, h, color[c], fill
end

#register_bodies(ary) ⇒ Object

Register a collection of bodies to be auto-updated and drawn.



227
228
229
230
# File 'lib/graphics/simulation.rb', line 227

def register_bodies ary
  _bodies << ary
  ary
end

#register_body(obj) ⇒ Object

Register a single Body to be auto-updated and drawn.



235
236
237
238
# File 'lib/graphics/simulation.rb', line 235

def register_body obj
  register_bodies Array(obj)
  obj
end

#register_color(name, r, g, b, a = 255) ⇒ Object

Name a color w/ rgba values.



243
244
245
# File 'lib/graphics/simulation.rb', line 243

def register_color name, r, g, b, a = 255
  color[name] = renderer.format.map_rgba r, g, b, a
end

#register_hsla(n, h, s, l, a = 1.0) ⇒ Object

Name a color w/ HSL values.



250
251
252
# File 'lib/graphics/simulation.rb', line 250

def register_hsla n, h, s, l, a = 1.0
  register_color n, *from_hsl(h, s, l), (a*255).round
end

#register_hsva(n, h, s, v, a = 1.0) ⇒ Object

Name a color w/ HSV values.



257
258
259
# File 'lib/graphics/simulation.rb', line 257

def register_hsva n, h, s, v, a = 1.0
  register_color n, *from_hsv(h, s, v), (a*255).round
end

#render_text(s, c, f = font) ⇒ Object

Return the rendered text s in color c in font f.



598
599
600
# File 'lib/graphics/simulation.rb', line 598

def render_text s, c, f = font
  f.render renderer, s, color[c]
end

#runObject

Run the simulation. This handles all events by polling and scanning for key presses (multiple keys at once are possible).

On each tick, call update, then draw the scene.



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/graphics/simulation.rb', line 382

def run
  self.start_time = Time.now
  n = 0
  event = nil
  self.done = false

  logger = respond_to? :log
  log_interval = self.class::LOG_INTERVAL

  loop do
    handle_event event, n while event = SDL::Event.poll
    handle_keys

    break if done
    next  if paused

    iter_per_tick.times { update n; n += 1 }
    draw_and_flip n

    log if logger and n % log_interval == 0
  end
end

#save(path) ⇒ Object

Save the current window to a png.



679
680
681
# File 'lib/graphics/simulation.rb', line 679

def save path
  renderer.save path
end

#sprite(w, h) ⇒ Object

Create a new renderer with a given width and height and yield to a block for drawing. The resulting surface is returned.



687
688
689
690
691
692
693
694
695
696
697
698
699
700
# File 'lib/graphics/simulation.rb', line 687

def sprite w, h
  old_renderer   = renderer
  new_renderer   = renderer.sprite w, h
  old_w, old_h   = renderer.w, renderer.h
  self.w, self.h = w, h
  self.renderer  = new_renderer

  yield if block_given?

  new_renderer.surface
ensure
  self.renderer  = old_renderer
  self.w, self.h = old_w, old_h
end

#text(s, x, y, c, f = font) ⇒ Object

Draw text s at x/y in color c in font f.



605
606
607
608
# File 'lib/graphics/simulation.rb', line 605

def text s, x, y, c, f = font
  y = self.h-y-f.height-1
  f.draw renderer, s, x, y, color[c]
end

#text_size(s, f = font) ⇒ Object

Return the w/h of the text s in font f.



591
592
593
# File 'lib/graphics/simulation.rb', line 591

def text_size s, f = font
  f.text_size s.to_s
end

#update(n) ⇒ Object

Update the simulation by telling all registered bodies to update. You are free to completely override this or call super and add any extras at the end.



456
457
458
459
460
# File 'lib/graphics/simulation.rb', line 456

def update n
  _bodies.each do |ary|
    ary.each(&:update)
  end
end

#vline(x, c, y1 = h-1, y2 = 0) ⇒ Object

Draw a vertical line from y1 to y2 at y in color c.



492
493
494
# File 'lib/graphics/simulation.rb', line 492

def vline x, c, y1 = h-1, y2 = 0
  line x, y1, x, y2, c
end