phosphor 0.1.0 → 0.1.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/bin/console +11 -0
  3. data/bin/setup +8 -0
  4. data/lib/phosphor/app.rb +80 -0
  5. data/lib/phosphor/events/input_event_reactor.rb +49 -0
  6. data/lib/phosphor/events/main_reactor.rb +42 -0
  7. data/lib/{razor → phosphor}/events/mouse_event.rb +2 -2
  8. data/lib/{razor → phosphor}/events/ray_collision_event.rb +1 -1
  9. data/lib/{razor → phosphor}/mouse/utils.rb +1 -1
  10. data/lib/{razor → phosphor}/objects/base.rb +13 -3
  11. data/lib/{razor → phosphor}/objects/box.rb +1 -1
  12. data/lib/phosphor/objects/circle.rb +70 -0
  13. data/lib/phosphor/objects/image.rb +62 -0
  14. data/lib/{razor → phosphor}/objects/line.rb +11 -3
  15. data/lib/{razor → phosphor}/objects/ray.rb +2 -2
  16. data/lib/{razor → phosphor}/objects/text.rb +1 -1
  17. data/lib/phosphor/renderers/curses_renderer.rb +58 -0
  18. data/lib/phosphor/rendering/canvas.rb +76 -0
  19. data/lib/phosphor/rendering/color.rb +60 -0
  20. data/lib/phosphor/rendering/color_pair.rb +53 -0
  21. data/lib/phosphor/runners/event_machine_runner.rb +28 -0
  22. data/lib/phosphor/runners/raw_runner.rb +72 -0
  23. data/lib/phosphor/version.rb +1 -3
  24. data/lib/phosphor.rb +36 -3
  25. metadata +25 -26
  26. data/.rspec +0 -3
  27. data/.rubocop.yml +0 -13
  28. data/CODE_OF_CONDUCT.md +0 -84
  29. data/LICENSE.txt +0 -21
  30. data/examples/line_plotter.rb +0 -71
  31. data/examples/mouse_tracker.rb +0 -86
  32. data/examples/rays.rb +0 -136
  33. data/examples/text_event.rb +0 -56
  34. data/lib/razor/app.rb +0 -77
  35. data/lib/razor/canvas.rb +0 -58
  36. data/lib/razor/events/input_event_reactor.rb +0 -77
  37. data/lib/razor.rb +0 -36
  38. data/phosphor.gemspec +0 -41
  39. data/sig/phosphor.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21f8fe8fed4153375b120bb9f15fde3ea1501d225274731dc4f73fefedaa922d
4
- data.tar.gz: b63eb1a1bfa080004ef0cd5c1ffd2feb85f5b04633243c6cd786a135fca355c1
3
+ metadata.gz: 7f86fb424ff88c87d5579e97dbe670fdd81baaa6a6143e125b6d042ce251775d
4
+ data.tar.gz: 4066a49f238b8ad0f96f4e482dabac21c9a99265063bee7944218bdb3ebdee40
5
5
  SHA512:
6
- metadata.gz: d5d0a29df062d80ec4918b9670bdd1db524d6fb330d98d9cc273173b7e283e4a3ce4c7b82258219383954111bd5e0b75446294cf83eff823f75d7ffcf78d70e9
7
- data.tar.gz: 595929050f00724b15b7d26abf4fd8ccbe17f75504593a08c42d327cfa3db4662335e4f215e470c7ce743a68ca2b6d7d087cfdf5a00ec58dcd81183476193255
6
+ metadata.gz: 6ee0f5f2441e5f278d96edbee0f83163a1024942daa4990a9c471620d68e38659f09b7bf217e5ee13b014971f53d1ebee14ac31fa1ad5e1de11a71c11d4bfc04
7
+ data.tar.gz: 777a032f696369294a3f5b2c65bfc336d64f9bea907d6203ad902b4927bbeadc66b63c33f3a6152ad640ca8f27d93e6363c020cd2cd81442cb066d1715a31d27
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "phosphor"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phosphor
4
+ class App
5
+ include Phosphor::Objects
6
+ include Phosphor::Rendering
7
+
8
+ attr_reader :canvas, :runner, :renderer
9
+
10
+ def initialize(
11
+ runner: Runners::RawRunner,
12
+ renderer: Renderers::CursesRenderer
13
+ )
14
+ @runner = runner
15
+ @renderer = renderer
16
+ end
17
+
18
+ def start
19
+ Phosphor::App.instance = self
20
+
21
+ @renderer.setup
22
+
23
+ @canvas = Canvas.new(@renderer.cols, @renderer.lines)
24
+
25
+ @runner.run do
26
+ Phosphor::Events::MainReactor.start
27
+
28
+ @runner.next_tick do
29
+ on_start
30
+ end
31
+
32
+ @runner.add_periodic_timer(1.0 / 1000) do
33
+ @canvas.clear
34
+ update
35
+ render
36
+ after_render
37
+ end
38
+ end
39
+ end
40
+
41
+ def update
42
+ on_update
43
+ end
44
+
45
+ def render
46
+ game_objects.each do |go|
47
+ next unless go.to_render?
48
+
49
+ go.render
50
+ end
51
+
52
+ @canvas.render
53
+
54
+ @renderer.refresh
55
+ end
56
+
57
+ def after_render
58
+ end
59
+
60
+ def stop
61
+ @renderer.close_screen
62
+
63
+ Phosphor::Mouse::Utils.disable_xterm_1003
64
+
65
+ Phosphor::App.instance = nil
66
+ end
67
+
68
+ def game_objects
69
+ @game_objects ||= []
70
+ end
71
+
72
+ def on(event_name, &block)
73
+ Phosphor::Events::MainReactor.on(event_name, &block)
74
+ end
75
+
76
+ class << self
77
+ attr_accessor :instance
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phosphor
4
+ module Events
5
+ class InputEventReactor
6
+ class << self
7
+ def start
8
+ app_instance = Phosphor::App.instance
9
+ app_instance.runner.add_periodic_timer(0.01) do
10
+ loop do
11
+ ch = app_instance.renderer.get_char
12
+ break if ch.nil?
13
+
14
+ case ch
15
+ when "q"
16
+ return stop
17
+ when "\e"
18
+ case app_instance.renderer.get_char
19
+ when "["
20
+ csi = ""
21
+ loop do
22
+ d = app_instance.renderer.get_char
23
+ csi += d
24
+ break if d.ord >= 0x40 && d.ord <= 0x7E
25
+ end
26
+ if /<(\d+);(\d+);(\d+)(m|M)/ =~ csi
27
+ button = Regexp.last_match(1).to_i
28
+ x = Regexp.last_match(2).to_i
29
+ y = Regexp.last_match(3).to_i
30
+ state = Regexp.last_match(4)
31
+
32
+ event = Phosphor::Events::MouseEvent.new(
33
+ raw_button: button,
34
+ raw_state: state,
35
+ x_pos: x,
36
+ y_pos: y
37
+ )
38
+
39
+ MainReactor.queue.push(event)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phosphor
4
+ module Events
5
+ class MainReactor
6
+ class << self
7
+ attr_accessor :queue
8
+
9
+ def event_listeners
10
+ @event_listeners ||= {}
11
+ end
12
+
13
+ def start
14
+ @queue = Phosphor::App.instance.runner.queue_class.new
15
+
16
+ InputEventReactor.start
17
+
18
+ start_events_consumer
19
+ end
20
+
21
+ def on(event_name, &block)
22
+ event_listeners[event_name.to_sym] ||= []
23
+ event_listeners[event_name.to_sym] << block
24
+ end
25
+
26
+ def emit(event)
27
+ @queue.push(event)
28
+ end
29
+
30
+ private
31
+
32
+ def start_events_consumer
33
+ @queue.pop do |event|
34
+ event_listeners[event.name.to_sym]&.each { |b| b.call(event) }
35
+ ensure
36
+ start_events_consumer
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Events
5
5
  class MouseEvent
6
6
  attr_reader :kind, :button, :direction, :x_pos, :y_pos
@@ -57,7 +57,7 @@ module Razor
57
57
  end
58
58
 
59
59
  def pressed?
60
- @raw_state == 'M'
60
+ @raw_state == "M"
61
61
  end
62
62
  end
63
63
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Events
5
5
  class RayCollisionEvent
6
6
  attr_reader :x_pos, :y_pos, :target, :distance, :des_distance
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Mouse
5
5
  class Utils
6
6
  def self.enable_xterm_1003
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Objects
5
5
  class Base
6
6
  attr_accessor :app_instance
7
7
 
8
8
  def initialize(*_)
9
- @app_instance = Razor::App.instance
9
+ @app_instance = Phosphor::App.instance
10
10
  @visible = true
11
11
 
12
12
  @app_instance.game_objects << self
@@ -39,7 +39,7 @@ module Razor
39
39
  end
40
40
 
41
41
  def on(event_name, &block)
42
- Razor::Events::InputEventReactor.on(event_name) do |event|
42
+ Phosphor::Events::MainReactor.on(event_name) do |event|
43
43
  next unless app_instance.canvas.entity_on(event.y_pos, event.x_pos) == self
44
44
 
45
45
  block.call(event, self)
@@ -47,6 +47,16 @@ module Razor
47
47
 
48
48
  self
49
49
  end
50
+
51
+ def self.on(event_name, &block)
52
+ Phosphor::Events::MainReactor.on(event_name) do |event|
53
+ next unless Phosphor::App.instance.canvas.entity_on(event.y_pos, event.x_pos).class == self
54
+
55
+ block.call(event, self)
56
+ end
57
+
58
+ self
59
+ end
50
60
  end
51
61
  end
52
62
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Objects
5
5
  class Box < Base
6
6
  attr_accessor :x_pos, :y_pos,
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phosphor
4
+ module Objects
5
+ class Circle < Base
6
+ attr_accessor :radius, :x_pos, :y_pos,
7
+ :x_anchor, :y_anchor,
8
+ :stroke_char, :fill_char
9
+
10
+ def initialize(
11
+ radius,
12
+ dot_count,
13
+ x_pos = 0,
14
+ y_pos = 0,
15
+ x_anchor: :start,
16
+ y_anchor: :start,
17
+ stroke_char: "*",
18
+ fill_char: nil
19
+ )
20
+ @radius = radius
21
+ @dot_count = dot_count
22
+
23
+ @x_pos = x_pos
24
+ @y_pos = y_pos
25
+
26
+ @x_anchor = x_anchor
27
+ @y_anchor = y_anchor
28
+
29
+ @stroke_char = stroke_char
30
+ @fill_char = fill_char
31
+
32
+ super
33
+ end
34
+
35
+ def anchor_from(anchor, dimension)
36
+ case anchor
37
+ when :start
38
+ 0
39
+ when :middle
40
+ dimension / 2
41
+ when :end
42
+ dimension
43
+ end
44
+ end
45
+
46
+ def x_pos_start = @x_pos - anchor_from(@x_anchor, @width)
47
+ def x_pos_end = x_pos_start + @width
48
+
49
+ def y_pos_start = @y_pos - anchor_from(@x_anchor, @height) / 2
50
+ def y_pos_end = y_pos_start + @height / 2
51
+
52
+ def render
53
+ @dot_count.times do |i|
54
+ percent = i / @dot_count.to_f
55
+
56
+ angle_in_rads = percent * Math::PI * 2
57
+
58
+ new_x_pos = @x_pos + radius * Math.sin(angle_in_rads)
59
+ new_y_pos = @y_pos + radius * Math.cos(angle_in_rads) / 2
60
+
61
+ 3.times do |i|
62
+ 3.times do |j|
63
+ app_instance.canvas.print_at(new_x_pos - 1 + i, new_y_pos + j - 1, @stroke_char, self)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chunky_png"
4
+
5
+ module Phosphor
6
+ module Objects
7
+ class Image < Base
8
+ attr_accessor :x_pos, :y_pos,
9
+ :scale
10
+
11
+ def initialize(
12
+ image_path,
13
+ x_pos = 0,
14
+ y_pos = 0,
15
+ scale: 1
16
+ )
17
+ @x_pos = x_pos
18
+ @y_pos = y_pos
19
+ @scale = scale
20
+
21
+ image = ChunkyPNG::Image.from_file(image_path)
22
+
23
+ @width = image.width
24
+ @height = image.height
25
+
26
+ @pixels = {}
27
+
28
+ @width.times do |image_x|
29
+ @height.times do |image_y|
30
+ pixel = image.get_pixel(image_x, image_y)
31
+
32
+ next unless pixel
33
+
34
+ @pixels[image_x] ||= {}
35
+
36
+ @pixels[image_x][image_y] = Rendering::ColorPair.from_foreground_hex(pixel.to_s(16).rjust(8, "0")[0..5])
37
+ end
38
+ end
39
+
40
+ super
41
+ end
42
+
43
+ def render
44
+ canvas = app_instance.canvas
45
+
46
+ @width.times do |image_x|
47
+ @height.times do |image_y|
48
+ scale.round.times do |scaled_y|
49
+ canvas.print_at(
50
+ (@x_pos + image_x * 2 * scale).round,
51
+ (@y_pos + image_y * scale + scaled_y).round,
52
+ "██" * @scale.round,
53
+ self,
54
+ color_pair: @pixels[image_x][image_y]
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Objects
5
5
  class Line < Base
6
6
  attr_accessor :x1_pos, :y1_pos, :x2_pos, :y2_pos, :stroke_char
@@ -10,7 +10,8 @@ module Razor
10
10
  y1_pos,
11
11
  x2_pos,
12
12
  y2_pos,
13
- stroke_char: '*'
13
+ stroke_char: '*',
14
+ stroke_color_pair: nil
14
15
  )
15
16
  @x1_pos = x1_pos
16
17
  @y1_pos = y1_pos
@@ -19,6 +20,7 @@ module Razor
19
20
  @y2_pos = y2_pos
20
21
 
21
22
  @stroke_char = stroke_char
23
+ @stroke_color_pair = stroke_color_pair
22
24
 
23
25
  super
24
26
  end
@@ -40,7 +42,13 @@ module Razor
40
42
  err = dx - dy
41
43
 
42
44
  loop do
43
- app_instance.canvas.print_at(x0, y0, @stroke_char, self)
45
+ app_instance.canvas.print_at(
46
+ x0,
47
+ y0,
48
+ @stroke_char,
49
+ self,
50
+ color_pair: @stroke_color_pair
51
+ )
44
52
 
45
53
  break if x0 == x1 && y0 == y1
46
54
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Objects
5
5
  class Ray < Base
6
6
  attr_accessor :x1_pos, :y1_pos, :x2_pos, :y2_pos, :stroke_char
@@ -60,7 +60,7 @@ module Razor
60
60
  target = app_instance.canvas.entity_on(x0, y0)
61
61
 
62
62
  if target && target.class != self.class
63
- event = Razor::Events::RayCollisionEvent.new(
63
+ event = Phosphor::Events::RayCollisionEvent.new(
64
64
  x_pos: x0,
65
65
  y_pos: y0,
66
66
  target:,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Razor
3
+ module Phosphor
4
4
  module Objects
5
5
  class Text < Base
6
6
  attr_accessor :text, :x_pos, :y_pos
@@ -0,0 +1,58 @@
1
+ require "curses"
2
+
3
+ module Phosphor
4
+ module Renderers
5
+ class CursesRenderer
6
+ def self.setup
7
+ Curses.init_screen
8
+ Curses.noecho
9
+ Curses.curs_set(0)
10
+ Curses.cbreak
11
+ # Curses.stdscr.keypad(true)
12
+ Curses.stdscr.nodelay = true
13
+
14
+ Curses.start_color
15
+ Curses.use_default_colors
16
+ end
17
+
18
+ def self.print_at(x, y, char, color_pair)
19
+ Curses.attron(Curses.color_pair(color_pair || 0)) do
20
+ Curses.setpos(y, x)
21
+ Curses.addstr(char)
22
+ end
23
+ end
24
+
25
+ def self.cols
26
+ Curses.cols
27
+ end
28
+
29
+ def self.lines
30
+ Curses.lines
31
+ end
32
+
33
+ def self.refresh
34
+ Curses.refresh
35
+ end
36
+
37
+ def self.close_screen
38
+ Curses.close_screen
39
+ end
40
+
41
+ def self.init_color(color_id, red, green, blue)
42
+ Curses.init_color(color_id, red, green, blue)
43
+ end
44
+
45
+ def self.get_char
46
+ Curses.get_char
47
+ end
48
+
49
+ def self.init_pair(pair_id, foreground_color_id, background_color_id)
50
+ Curses.init_pair(
51
+ pair_id,
52
+ foreground_color_id,
53
+ background_color_id
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phosphor
4
+ module Rendering
5
+ class Canvas
6
+ attr_reader :width, :height, :objects_at
7
+
8
+ def initialize(width, height)
9
+ @width = width
10
+ @height = height
11
+ @mutex = Mutex.new
12
+ clear
13
+ end
14
+
15
+ def clear
16
+ @mutex.synchronize do
17
+ @pixels = Array.new(@height) { Array.new(@width, " ") }
18
+ @color_pairs = Array.new(@height) { Array.new(@width, nil) }
19
+ @objects_at = Array.new(@height) { Array.new(@width, nil) }
20
+ end
21
+ end
22
+
23
+ def was_changed?
24
+ @was_changed
25
+ end
26
+
27
+ def print_at(x_pos, y_pos, string, owner = nil, color_pair: nil, foreground_color: nil, background_color: nil)
28
+ @mutex.synchronize do
29
+ return if y_pos.negative? || y_pos >= @height
30
+
31
+ @was_changed = true
32
+
33
+ string.chars.each_with_index do |ch, i|
34
+ px = x_pos + i
35
+ break if px >= @width
36
+ next if px.negative?
37
+
38
+ if color_pair || foreground_color || background_color
39
+ @color_pairs[y_pos][px] = color_pair&.pair_id
40
+ @color_pairs[y_pos][px] ||=
41
+ ColorPair.new(foreground_color, background_color).pair_id
42
+ end
43
+ @pixels[y_pos][px] = ch
44
+ @objects_at[y_pos][px] = owner
45
+ end
46
+ end
47
+ end
48
+
49
+ def entity_on(x_pos, y_pos)
50
+ (objects_at[y_pos] || {})[x_pos]
51
+ end
52
+
53
+ def render
54
+ @mutex.synchronize do
55
+ return unless was_changed?
56
+
57
+ @was_changed = false
58
+
59
+ @pixels.each_with_index do |row, y|
60
+ row.each_with_index do |char, x|
61
+ Phosphor::App.instance.renderer.print_at(x, y, char, @color_pairs[y][x] || 0)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def x_center_pos
68
+ (width / 2).floor
69
+ end
70
+
71
+ def y_center_pos
72
+ (height / 2).floor
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,60 @@
1
+ module Phosphor
2
+ module Rendering
3
+ class Color
4
+ attr_reader :color_id, :red_thousand, :green_thousand, :blue_thousand
5
+
6
+ def initialize(red_thousand, green_thousand, blue_thousand)
7
+ @color_id = self.class.generate_new_color_id
8
+
9
+ @red_thousand = red_thousand
10
+ @green_thousand = green_thousand
11
+ @blue_thousand = blue_thousand
12
+
13
+ Phosphor::App.instance.renderer.init_color(@color_id, red_thousand, green_thousand, blue_thousand)
14
+ end
15
+
16
+ def foreground_pair
17
+ ColorPair.new(self)
18
+ end
19
+
20
+ def hex
21
+ [
22
+ red_thousand,
23
+ green_thousand,
24
+ blue_thousand
25
+ ].map do |a|
26
+ self.class.thousand_to_hex(a).to_s(16)
27
+ end.join.upcase
28
+ end
29
+
30
+ class << self
31
+ def from_rgb(red, green, blue)
32
+ from_hex(rgb_to_hex(red, green, blue))
33
+ end
34
+
35
+ def from_hex(hex)
36
+ red, green, blue = hex.scan(/.{2}/).map { |a| (a.to_i(16) / 255.to_f * 1000).round }
37
+
38
+ colors[hex] ||= new(red, green, blue)
39
+ end
40
+
41
+ def colors
42
+ @colors ||= {}
43
+ end
44
+
45
+ def generate_new_color_id
46
+ @last_color_id ||= 16
47
+ @last_color_id += 1
48
+ end
49
+
50
+ def thousand_to_hex(value)
51
+ (value / 1000.to_f * 255).round
52
+ end
53
+
54
+ def rgb_to_hex(red, green, blue)
55
+ [red, green, blue].map { |a| (a / 100.to_f * 255).round.to_s(16).rjust(2, '0') }.join.upcase
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end