rbgl 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +130 -74
  5. data/Rakefile +7 -0
  6. data/bench/renderer_bench.rb +283 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/index.html +38 -0
  9. data/examples/array_test.rb +1 -1
  10. data/examples/color_test.rb +1 -1
  11. data/examples/cube_spinning.rb +1 -1
  12. data/examples/gradient.rb +1 -1
  13. data/examples/multi_return_test.rb +1 -1
  14. data/examples/plasma.rb +1 -1
  15. data/examples/sphere_raymarch.rb +1 -1
  16. data/examples/triangle_window.rb +1 -1
  17. data/exe/rbgl +42 -0
  18. data/lib/rbgl/cli/doctor.rb +155 -0
  19. data/lib/rbgl/engine/buffer.rb +150 -40
  20. data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
  21. data/lib/rbgl/engine/context.rb +133 -63
  22. data/lib/rbgl/engine/framebuffer.rb +153 -48
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/pipeline.rb +38 -7
  25. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
  26. data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
  27. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  28. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
  29. data/lib/rbgl/engine/rasterizer.rb +65 -171
  30. data/lib/rbgl/engine/shader/base_shader.rb +51 -0
  31. data/lib/rbgl/engine/shader/builtins.rb +257 -0
  32. data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
  33. data/lib/rbgl/engine/shader.rb +5 -318
  34. data/lib/rbgl/engine/texture.rb +264 -51
  35. data/lib/rbgl/engine.rb +4 -1
  36. data/lib/rbgl/gui/backend.rb +21 -34
  37. data/lib/rbgl/gui/backend_factory.rb +89 -0
  38. data/lib/rbgl/gui/cocoa/backend.rb +59 -46
  39. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  40. data/lib/rbgl/gui/event.rb +4 -2
  41. data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
  45. data/lib/rbgl/gui/file_backend.rb +50 -55
  46. data/lib/rbgl/gui/wayland/backend.rb +214 -60
  47. data/lib/rbgl/gui/wayland/codec.rb +50 -0
  48. data/lib/rbgl/gui/wayland/connection.rb +188 -231
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
  50. data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
  53. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  55. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  57. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
  58. data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
  59. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  60. data/lib/rbgl/gui/window/render_loop.rb +111 -0
  61. data/lib/rbgl/gui/window.rb +74 -84
  62. data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
  63. data/lib/rbgl/gui/x11/backend.rb +73 -49
  64. data/lib/rbgl/gui/x11/connection.rb +284 -219
  65. data/lib/rbgl/gui/x11/event_parser.rb +71 -0
  66. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  67. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  68. data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
  69. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  70. data/lib/rbgl/gui/x11/transport.rb +81 -0
  71. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  72. data/lib/rbgl/gui.rb +3 -5
  73. data/lib/rbgl/version.rb +1 -1
  74. metadata +63 -24
  75. data/examples/chemical_heartbeat.rb +0 -166
  76. data/examples/dark_transit.rb +0 -166
  77. data/examples/fractured_orb.rb +0 -428
  78. data/examples/fractured_orb_rb.rb +0 -598
  79. data/examples/hexagonal_flow.rb +0 -333
  80. data/examples/teapot.rb +0 -362
  81. data/examples/teapot_mcu.rb +0 -344
data/lib/rbgl/engine.rb CHANGED
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "version"
4
+ require_relative "engine/immutable_color"
3
5
  require_relative "engine/framebuffer"
4
6
  require_relative "engine/buffer"
5
7
  require_relative "engine/shader"
6
8
  require_relative "engine/texture"
7
9
  require_relative "engine/rasterizer"
8
10
  require_relative "engine/pipeline"
11
+ require_relative "engine/clip_space_clipper"
9
12
  require_relative "engine/context"
10
13
 
11
14
  module RBGL
12
15
  module Engine
13
- VERSION = "0.1.0"
16
+ VERSION = RBGL::VERSION
14
17
  end
15
18
  end
@@ -2,16 +2,19 @@
2
2
 
3
3
  module RBGL
4
4
  module GUI
5
+ class BackendUnavailable < StandardError
6
+ end
7
+
8
+ class BackendSelectionError < ArgumentError
9
+ end
10
+
5
11
  class Backend
6
12
  attr_reader :width, :height, :title
7
13
 
8
- def initialize(width, height, title = "RBGL")
14
+ def initialize(width, height, title = "RBGL", **_options)
9
15
  @width = width
10
16
  @height = height
11
17
  @title = title
12
- @key_callback = nil
13
- @mouse_callback = nil
14
- @resize_callback = nil
15
18
  end
16
19
 
17
20
  def present(_framebuffer)
@@ -23,7 +26,12 @@ module RBGL
23
26
  end
24
27
 
25
28
  def poll_events_raw
26
- []
29
+ poll_events.filter_map { |event| event.to_h if event.respond_to?(:to_h) }
30
+ end
31
+
32
+ def resize(width, height)
33
+ @width = width
34
+ @height = height
27
35
  end
28
36
 
29
37
  def should_close?
@@ -35,41 +43,20 @@ module RBGL
35
43
  end
36
44
 
37
45
  def set_pixels(buffer, width, height)
38
- raise NotImplementedError
39
- end
40
-
41
- def metal_available?
42
- false
43
- end
44
-
45
- def native_handle
46
- nil
47
- end
48
-
49
- def on_key(&block)
50
- @key_callback = block
51
- end
52
-
53
- def on_mouse(&block)
54
- @mouse_callback = block
55
- end
56
-
57
- def on_resize(&block)
58
- @resize_callback = block
46
+ bytes = validate_rgba_buffer(buffer, width, height)
47
+ present(Engine::Framebuffer.from_rgba_bytes(width, height, bytes))
59
48
  end
60
49
 
61
50
  protected
62
51
 
63
- def emit_key(key, action)
64
- @key_callback&.call(key, action)
65
- end
52
+ def validate_rgba_buffer(buffer, width, height)
53
+ bytes = String.try_convert(buffer)
54
+ raise ArgumentError, "Pixel buffer must be a String" unless bytes
66
55
 
67
- def emit_mouse(x, y, button, action)
68
- @mouse_callback&.call(x, y, button, action)
69
- end
56
+ expected_size = width * height * 4
57
+ return bytes if bytes.bytesize == expected_size
70
58
 
71
- def emit_resize(width, height)
72
- @resize_callback&.call(width, height)
59
+ raise ArgumentError, "Pixel buffer size mismatch: expected #{expected_size}, got #{bytes.bytesize}"
73
60
  end
74
61
  end
75
62
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class BackendFactory
6
+ AUTO_BACKEND_ERRORS = [LoadError, SystemCallError, BackendUnavailable].freeze
7
+
8
+ class << self
9
+ def build(backend, width:, height:, title:, platform: RUBY_PLATFORM, env: ENV, **options)
10
+ case backend
11
+ when :auto
12
+ build_auto_backend(
13
+ width: width,
14
+ height: height,
15
+ title: title,
16
+ platform: platform,
17
+ env: env,
18
+ **options
19
+ )
20
+ else
21
+ build_specific_backend(backend, width: width, height: height, title: title, env: env, **options)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def build_auto_backend(width:, height:, title:, platform:, env:, builder: nil, **options)
28
+ builder ||= method(:build_specific_backend)
29
+ errors = []
30
+
31
+ native_backend_candidates(platform: platform, env: env).each do |candidate|
32
+ return builder.call(candidate, width: width, height: height, title: title, env: env, **options)
33
+ rescue *AUTO_BACKEND_ERRORS => error
34
+ errors << [candidate, error]
35
+ end
36
+
37
+ raise_auto_backend_error(errors)
38
+ end
39
+
40
+ def build_specific_backend(backend, width:, height:, title:, env: ENV, **options)
41
+ case backend
42
+ when :file
43
+ FileBackend.new(width, height, title, **options)
44
+ when :x11
45
+ require_relative "x11/backend"
46
+ X11::Backend.new(width, height, title, env: env, **options)
47
+ when :wayland
48
+ require_relative "wayland/backend"
49
+ Wayland::Backend.new(width, height, title, env: env, **options)
50
+ when :cocoa
51
+ require_relative "cocoa/backend"
52
+ Cocoa::Backend.new(width, height, title, env: env, **options)
53
+ when Backend
54
+ unless options.empty?
55
+ raise ArgumentError, "Backend instances do not accept construction options: #{options.keys.join(', ')}"
56
+ end
57
+
58
+ backend
59
+ else
60
+ raise BackendSelectionError, "Unknown backend: #{backend}"
61
+ end
62
+ end
63
+
64
+ def raise_auto_backend_error(errors)
65
+ return if errors.empty?
66
+
67
+ details = errors.map { |backend, error| "#{backend}: #{error.class}: #{error.message}" }.join(", ")
68
+ raise BackendUnavailable, "Failed to initialize any native backend (#{details})"
69
+ end
70
+
71
+ def native_backend_candidates(platform:, env:)
72
+ case platform
73
+ when /darwin/
74
+ [:cocoa]
75
+ when /linux/
76
+ candidates = []
77
+ candidates << :wayland if env["WAYLAND_DISPLAY"]
78
+ candidates << :x11 if env["DISPLAY"]
79
+ return candidates unless candidates.empty?
80
+
81
+ raise BackendUnavailable, "No display server found (DISPLAY or WAYLAND_DISPLAY not set)"
82
+ else
83
+ raise BackendUnavailable, "Unsupported platform: #{platform}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,58 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "metaco"
5
- METACO_AVAILABLE = true
6
- rescue LoadError
7
- METACO_AVAILABLE = false
8
- end
3
+ require_relative "key_mapper"
9
4
 
10
5
  module RBGL
11
6
  module GUI
12
7
  module Cocoa
8
+ METACO_AVAILABLE = begin
9
+ require "metaco"
10
+ true
11
+ rescue LoadError
12
+ false
13
+ end
14
+
13
15
  class Backend < GUI::Backend
14
- def initialize(width, height, title = "RBGL")
16
+ def initialize(width, height, title = "RBGL", env: ENV, resizable: false, high_dpi: false, **_options)
17
+ @env = env
15
18
  unless METACO_AVAILABLE
16
19
  raise LoadError, "metaco gem is required for Cocoa backend. Install it with: gem install metaco"
17
20
  end
18
21
 
19
- super
22
+ super(width, height, title)
20
23
  Metaco.init
21
- @handle = Metaco.window_create(width, height, title)
24
+ @handle = Metaco.window_create(width, height, title, resizable: resizable, high_dpi: high_dpi)
25
+ @framebuffer_width, @framebuffer_height = Metaco.framebuffer_size(@handle)
22
26
  end
23
27
 
24
28
  def present(framebuffer)
25
- return unless @handle
29
+ return false unless @handle
26
30
 
27
- Metaco.set_pixels(@handle, framebuffer.to_rgba_bytes, framebuffer.width, framebuffer.height)
31
+ bytes = framebuffer.to_rgba_bytes
32
+ width = @framebuffer_width
33
+ height = @framebuffer_height
34
+ bytes = scale_pixels(bytes, framebuffer.width, framebuffer.height, width, height) if [width, height] != [framebuffer.width, framebuffer.height]
35
+ Metaco.set_pixels(@handle, bytes, width, height)
28
36
  Metaco.present(@handle)
37
+ true
29
38
  end
30
39
 
31
40
  def poll_events
32
41
  return [] unless @handle
33
42
 
34
43
  raw_events = Metaco.poll_events(@handle)
35
- events = []
36
-
37
- raw_events.each do |e|
38
- event = convert_event(e)
39
- if event
40
- events << event
41
- emit_from_event(event)
42
- end
43
- end
44
-
45
- events
46
- end
47
-
48
- def poll_events_raw
49
- return [] unless @handle
50
-
51
- Metaco.poll_events(@handle)
44
+ raw_events.filter_map { |event| convert_event(event) }
52
45
  end
53
46
 
54
47
  def should_close?
55
- return false unless @handle
48
+ return true unless @handle
56
49
 
57
50
  Metaco.should_close?(@handle)
58
51
  end
@@ -67,10 +60,17 @@ module RBGL
67
60
  def set_pixels(buffer, width, height)
68
61
  return unless @handle
69
62
 
70
- Metaco.set_pixels(@handle, buffer, width, height)
63
+ bytes = validate_rgba_buffer(buffer, width, height)
64
+ bytes = scale_pixels(bytes, width, height, @framebuffer_width, @framebuffer_height) if [width, height] != [@framebuffer_width, @framebuffer_height]
65
+ Metaco.set_pixels(@handle, bytes, @framebuffer_width, @framebuffer_height)
71
66
  Metaco.present(@handle)
72
67
  end
73
68
 
69
+ def resize(width, height)
70
+ super
71
+ @framebuffer_width, @framebuffer_height = Metaco.framebuffer_size(@handle)
72
+ end
73
+
74
74
  def metal_available?
75
75
  return false unless @handle
76
76
 
@@ -88,32 +88,45 @@ module RBGL
88
88
 
89
89
  case type
90
90
  when :key_press
91
- Event.new(:key_press, key: raw[:key], char: raw[:char])
91
+ Event.new(:key_press, key: normalize_key(raw[:key], raw[:char]), keycode: raw[:key], char: raw[:char], modifiers: raw[:modifiers] || [])
92
92
  when :key_release
93
- Event.new(:key_release, key: raw[:key])
93
+ Event.new(:key_release, key: normalize_key(raw[:key]), keycode: raw[:key], modifiers: raw[:modifiers] || [])
94
94
  when :mouse_press
95
- Event.new(:mouse_press, x: raw[:x], y: raw[:y], button: raw[:button])
95
+ Event.new(:mouse_press, x: raw[:x], y: mouse_y(raw[:y]), button: raw[:button], modifiers: raw[:modifiers] || [])
96
96
  when :mouse_release
97
- Event.new(:mouse_release, x: raw[:x], y: raw[:y], button: raw[:button])
97
+ Event.new(:mouse_release, x: raw[:x], y: mouse_y(raw[:y]), button: raw[:button], modifiers: raw[:modifiers] || [])
98
98
  when :mouse_move
99
- Event.new(:mouse_move, x: raw[:x], y: raw[:y])
99
+ Event.new(:mouse_move, x: raw[:x], y: mouse_y(raw[:y]), modifiers: raw[:modifiers] || [])
100
+ when :scroll
101
+ Event.new(:scroll, dx: raw[:dx], dy: raw[:dy], modifiers: raw[:modifiers] || [])
102
+ when :focus, :blur
103
+ Event.new(type)
104
+ when :resize
105
+ Event.new(:resize, width: raw[:width], height: raw[:height],
106
+ framebuffer_width: raw[:framebuffer_width], framebuffer_height: raw[:framebuffer_height])
100
107
  else
101
108
  nil
102
109
  end
103
110
  end
104
111
 
105
- def emit_from_event(event)
106
- case event.type
107
- when :key_press, :key_release
108
- emit_key(event.key, event.type == :key_press ? :press : :release)
109
- when :mouse_press, :mouse_release, :mouse_move
110
- action = case event.type
111
- when :mouse_press then :press
112
- when :mouse_release then :release
113
- else :move
114
- end
115
- emit_mouse(event.x, event.y, event[:button], action)
112
+ def normalize_key(key, char = nil)
113
+ KeyMapper.key(key, char)
114
+ end
115
+
116
+ def mouse_y(y)
117
+ @height ? @height - 1 - y : y
118
+ end
119
+
120
+ def scale_pixels(bytes, source_width, source_height, target_width, target_height)
121
+ scaled = String.new(capacity: target_width * target_height * 4, encoding: Encoding::BINARY)
122
+ target_height.times do |y|
123
+ source_y = y * source_height / target_height
124
+ target_width.times do |x|
125
+ source_x = x * source_width / target_width
126
+ scaled << bytes.byteslice(((source_y * source_width) + source_x) * 4, 4)
127
+ end
116
128
  end
129
+ scaled
117
130
  end
118
131
  end
119
132
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module Cocoa
6
+ module KeyMapper
7
+ KEY_CODES = {
8
+ 0 => :a, 1 => :s, 2 => :d, 3 => :f, 4 => :h, 5 => :g, 6 => :z, 7 => :x,
9
+ 8 => :c, 9 => :v, 11 => :b, 12 => :q, 13 => :w, 14 => :e, 15 => :r,
10
+ 16 => :y, 17 => :t, 18 => :one, 19 => :two, 20 => :three, 21 => :four,
11
+ 22 => :six, 23 => :five, 24 => :equal, 25 => :nine, 26 => :seven,
12
+ 27 => :minus, 28 => :eight, 29 => :zero, 30 => :right_bracket, 31 => :o,
13
+ 32 => :u, 33 => :left_bracket, 34 => :i, 35 => :p, 36 => :enter, 37 => :l,
14
+ 38 => :j, 39 => :quote, 40 => :k, 41 => :semicolon, 42 => :backslash,
15
+ 43 => :comma, 44 => :slash, 45 => :n, 46 => :m, 47 => :period, 48 => :tab,
16
+ 49 => :space, 50 => :grave, 51 => :backspace, 53 => :escape, 54 => :right_meta,
17
+ 55 => :left_meta, 56 => :left_shift, 57 => :caps_lock, 58 => :left_alt,
18
+ 59 => :left_control, 60 => :right_shift, 61 => :right_alt, 62 => :right_control,
19
+ 63 => :function, 64 => :f17, 65 => :keypad_decimal, 67 => :keypad_multiply,
20
+ 69 => :keypad_plus, 71 => :keypad_clear, 72 => :volume_up, 73 => :volume_down,
21
+ 74 => :mute, 75 => :keypad_divide, 76 => :keypad_enter, 78 => :keypad_minus,
22
+ 79 => :f18, 80 => :f19, 81 => :keypad_equal, 82 => :keypad_zero,
23
+ 83 => :keypad_one, 84 => :keypad_two, 85 => :keypad_three, 86 => :keypad_four,
24
+ 87 => :keypad_five, 88 => :keypad_six, 89 => :keypad_seven,
25
+ 91 => :keypad_eight, 92 => :keypad_nine, 96 => :f5, 97 => :f6, 98 => :f7,
26
+ 99 => :f3, 100 => :f8, 101 => :f9, 103 => :f11, 105 => :f13,
27
+ 106 => :f16, 107 => :f14, 109 => :f10, 111 => :f12, 113 => :f15,
28
+ 114 => :help, 115 => :home, 116 => :page_up, 117 => :delete,
29
+ 118 => :f4, 119 => :end, 120 => :f2, 121 => :page_down, 122 => :f1,
30
+ 123 => :left, 124 => :right, 125 => :down, 126 => :up
31
+ }.freeze
32
+
33
+ module_function
34
+
35
+ def key(code, char = nil)
36
+ return code if code.is_a?(Symbol)
37
+ return KEY_CODES.fetch(code, code) if code.is_a?(Integer)
38
+
39
+ text = char.to_s.empty? ? code.to_s : char.to_s
40
+ return :escape if text == "\e" || text.casecmp?("escape")
41
+ return text.downcase.to_sym if text.length == 1
42
+
43
+ code
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,8 +14,10 @@ module RBGL
14
14
  @data[key]
15
15
  end
16
16
 
17
- def method_missing(name, *args)
18
- @data.key?(name) ? @data[name] : super
17
+ def method_missing(name, *args, &block)
18
+ return @data[name] if args.empty? && block.nil? && @data.key?(name)
19
+
20
+ super
19
21
  end
20
22
 
21
23
  def respond_to_missing?(name, include_private = false)
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend < Backend
6
+ class BmpWriter < FrameWriter
7
+ def extension
8
+ "bmp"
9
+ end
10
+
11
+ def write(filename, framebuffer)
12
+ File.binwrite(filename, encode(framebuffer))
13
+ end
14
+
15
+ private
16
+
17
+ def encode(framebuffer)
18
+ width = framebuffer.width
19
+ height = framebuffer.height
20
+ row_size = (((24 * width) + 31) / 32) * 4
21
+ pixel_data_size = row_size * height
22
+ file_size = 54 + pixel_data_size
23
+
24
+ header = [
25
+ 0x42, 0x4D,
26
+ file_size,
27
+ 0, 0,
28
+ 54
29
+ ].pack("CCVvvV")
30
+
31
+ dib = [
32
+ 40,
33
+ width, height,
34
+ 1,
35
+ 24,
36
+ 0,
37
+ pixel_data_size,
38
+ 2835, 2835,
39
+ 0, 0
40
+ ].pack("VVVvvVVVVVV")
41
+
42
+ header + dib + pixel_rows(framebuffer, width, height, row_size)
43
+ end
44
+
45
+ def pixel_rows(framebuffer, width, height, row_size)
46
+ pixels = +""
47
+
48
+ (height - 1).downto(0) do |y|
49
+ row = +""
50
+ width.times do |x|
51
+ bytes = framebuffer.get_pixel(x, y).to_bytes
52
+ row << [bytes[2], bytes[1], bytes[0]].pack("CCC")
53
+ end
54
+ row << ("\x00" * (row_size - (width * 3)))
55
+ pixels << row
56
+ end
57
+
58
+ pixels
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend < Backend
6
+ class FrameWriter
7
+ def self.build(format, ppm_mode: :ascii)
8
+ case format
9
+ when :ppm
10
+ PpmWriter.new(binary: ppm_mode == :binary)
11
+ when :bmp
12
+ BmpWriter.new
13
+ when :png
14
+ begin
15
+ require "tessel"
16
+ rescue LoadError
17
+ raise ArgumentError, "format: :png requires the tessel gem"
18
+ end
19
+ PngWriter.new
20
+ else
21
+ raise ArgumentError, "Unsupported file backend format: #{format}"
22
+ end
23
+ end
24
+
25
+ def extension
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def write(_filename, _framebuffer)
30
+ raise NotImplementedError
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend
6
+ class PngWriter < FrameWriter
7
+ def extension
8
+ "png"
9
+ end
10
+
11
+ def write(filename, framebuffer)
12
+ image = Tessel::Image.from_rgba(framebuffer.width, framebuffer.height, framebuffer.to_rgba_bytes)
13
+ image.write(filename)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ class FileBackend < Backend
6
+ class PpmWriter < FrameWriter
7
+ def initialize(binary:)
8
+ super()
9
+ @binary = binary
10
+ end
11
+
12
+ def extension
13
+ "ppm"
14
+ end
15
+
16
+ def write(filename, framebuffer)
17
+ if @binary
18
+ File.binwrite(filename, framebuffer.to_ppm_binary)
19
+ else
20
+ File.write(filename, framebuffer.to_ppm)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end