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.
- checksums.yaml +4 -4
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/phosphor/app.rb +80 -0
- data/lib/phosphor/events/input_event_reactor.rb +49 -0
- data/lib/phosphor/events/main_reactor.rb +42 -0
- data/lib/{razor → phosphor}/events/mouse_event.rb +2 -2
- data/lib/{razor → phosphor}/events/ray_collision_event.rb +1 -1
- data/lib/{razor → phosphor}/mouse/utils.rb +1 -1
- data/lib/{razor → phosphor}/objects/base.rb +13 -3
- data/lib/{razor → phosphor}/objects/box.rb +1 -1
- data/lib/phosphor/objects/circle.rb +70 -0
- data/lib/phosphor/objects/image.rb +62 -0
- data/lib/{razor → phosphor}/objects/line.rb +11 -3
- data/lib/{razor → phosphor}/objects/ray.rb +2 -2
- data/lib/{razor → phosphor}/objects/text.rb +1 -1
- data/lib/phosphor/renderers/curses_renderer.rb +58 -0
- data/lib/phosphor/rendering/canvas.rb +76 -0
- data/lib/phosphor/rendering/color.rb +60 -0
- data/lib/phosphor/rendering/color_pair.rb +53 -0
- data/lib/phosphor/runners/event_machine_runner.rb +28 -0
- data/lib/phosphor/runners/raw_runner.rb +72 -0
- data/lib/phosphor/version.rb +1 -3
- data/lib/phosphor.rb +36 -3
- metadata +25 -26
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/examples/line_plotter.rb +0 -71
- data/examples/mouse_tracker.rb +0 -86
- data/examples/rays.rb +0 -136
- data/examples/text_event.rb +0 -56
- data/lib/razor/app.rb +0 -77
- data/lib/razor/canvas.rb +0 -58
- data/lib/razor/events/input_event_reactor.rb +0 -77
- data/lib/razor.rb +0 -36
- data/phosphor.gemspec +0 -41
- data/sig/phosphor.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f86fb424ff88c87d5579e97dbe670fdd81baaa6a6143e125b6d042ce251775d
|
|
4
|
+
data.tar.gz: 4066a49f238b8ad0f96f4e482dabac21c9a99265063bee7944218bdb3ebdee40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lib/phosphor/app.rb
ADDED
|
@@ -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
|
|
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 ==
|
|
60
|
+
@raw_state == "M"
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
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 =
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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(
|
|
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
|
|
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 =
|
|
63
|
+
event = Phosphor::Events::RayCollisionEvent.new(
|
|
64
64
|
x_pos: x0,
|
|
65
65
|
y_pos: y0,
|
|
66
66
|
target:,
|
|
@@ -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
|