whirled_peas 0.7.1 → 0.8.0
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/.gitignore +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +116 -88
- data/Rakefile +1 -20
- data/bin/reset_cursor +11 -0
- data/bin/screen_test +68 -0
- data/examples/intro.rb +3 -3
- data/examples/scrolling.rb +5 -4
- data/lib/whirled_peas.rb +2 -4
- data/lib/whirled_peas/animator.rb +5 -0
- data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
- data/lib/whirled_peas/animator/easing.rb +72 -0
- data/lib/whirled_peas/animator/frame.rb +5 -0
- data/lib/whirled_peas/animator/frameset.rb +33 -0
- data/lib/whirled_peas/animator/producer.rb +35 -0
- data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
- data/lib/whirled_peas/command.rb +5 -0
- data/lib/whirled_peas/command/base.rb +86 -0
- data/lib/whirled_peas/command/config_command.rb +44 -0
- data/lib/whirled_peas/command/debug.rb +21 -0
- data/lib/whirled_peas/command/fonts.rb +22 -0
- data/lib/whirled_peas/command/frame_command.rb +34 -0
- data/lib/whirled_peas/command/frames.rb +24 -0
- data/lib/whirled_peas/command/help.rb +38 -0
- data/lib/whirled_peas/command/play.rb +108 -0
- data/lib/whirled_peas/command/record.rb +57 -0
- data/lib/whirled_peas/command/still.rb +29 -0
- data/lib/whirled_peas/command_line.rb +22 -212
- data/lib/whirled_peas/config.rb +56 -6
- data/lib/whirled_peas/device.rb +5 -0
- data/lib/whirled_peas/device/null_device.rb +8 -0
- data/lib/whirled_peas/device/output_file.rb +19 -0
- data/lib/whirled_peas/device/screen.rb +26 -0
- data/lib/whirled_peas/graphics/container_painter.rb +91 -0
- data/lib/whirled_peas/graphics/painter.rb +10 -0
- data/lib/whirled_peas/graphics/renderer.rb +8 -2
- data/lib/whirled_peas/utils/ansi.rb +13 -0
- data/lib/whirled_peas/utils/file_handler.rb +57 -0
- data/lib/whirled_peas/version.rb +1 -1
- data/tools/whirled_peas/tools/screen_tester.rb +117 -65
- metadata +27 -8
- data/lib/whirled_peas/frame.rb +0 -6
- data/lib/whirled_peas/frame/consumer.rb +0 -30
- data/lib/whirled_peas/frame/debug_consumer.rb +0 -30
- data/lib/whirled_peas/frame/event_loop.rb +0 -90
- data/lib/whirled_peas/frame/producer.rb +0 -67
- data/lib/whirled_peas/graphics/screen.rb +0 -70
data/examples/intro.rb
CHANGED
@@ -40,13 +40,13 @@ class TemplateFactory
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
class
|
43
|
+
class Application
|
44
44
|
def start(producer)
|
45
|
-
producer.
|
45
|
+
producer.add_frame('intro', duration: 5)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
WhirledPeas.configure do |config|
|
50
50
|
config.template_factory = TemplateFactory.new
|
51
|
-
config.
|
51
|
+
config.application = Application.new
|
52
52
|
end
|
data/examples/scrolling.rb
CHANGED
@@ -39,15 +39,16 @@ class TemplateFactory
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
class
|
42
|
+
class Application
|
43
43
|
def start(producer)
|
44
|
-
|
45
|
-
|
44
|
+
producer.frameset(5, easing: :bezier) do |fs|
|
45
|
+
53.times { |i| fs.add_frame('intro', { top: -i }) }
|
46
46
|
end
|
47
|
+
producer.add_frame('hold', duration: 1, args: { top: -52})
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
51
|
WhirledPeas.configure do |config|
|
51
52
|
config.template_factory = TemplateFactory.new
|
52
|
-
config.
|
53
|
+
config.application = Application.new
|
53
54
|
end
|
data/lib/whirled_peas.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
require 'whirled_peas/errors'
|
1
|
+
require 'whirled_peas/animator'
|
4
2
|
require 'whirled_peas/config'
|
5
|
-
require 'whirled_peas/
|
3
|
+
require 'whirled_peas/errors'
|
6
4
|
require 'whirled_peas/graphics'
|
7
5
|
require 'whirled_peas/settings'
|
8
6
|
require 'whirled_peas/utils'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
module Animator
|
3
|
+
class DebugConsumer
|
4
|
+
def add_frameset(frameset)
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
frameset.each_frame do |frame, args|
|
8
|
+
puts [frame, *(JSON.generate(args) unless args.empty?)].join(' ')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def process
|
13
|
+
# no op
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
module Animator
|
3
|
+
class Easing
|
4
|
+
# Implementations of available ease-in functions. Ease-out and ease-in-out can all be
|
5
|
+
# derived from ease-in.
|
6
|
+
EASING = {
|
7
|
+
bezier: proc do |value|
|
8
|
+
value /= 2
|
9
|
+
2 * value * value * (3 - 2 * value)
|
10
|
+
end,
|
11
|
+
linear: proc { |value| value },
|
12
|
+
parametric: proc do |value|
|
13
|
+
value /= 2
|
14
|
+
squared = value * value
|
15
|
+
2 * squared / (2 * (squared - value) + 1)
|
16
|
+
end,
|
17
|
+
quadratic: proc do |value|
|
18
|
+
value * value
|
19
|
+
end
|
20
|
+
}
|
21
|
+
|
22
|
+
EFFECTS = %i[in out in_out]
|
23
|
+
|
24
|
+
def initialize(easing=:linear, effect=:in_out)
|
25
|
+
unless EASING.key?(easing)
|
26
|
+
raise ArgumentError,
|
27
|
+
"Invalid easing function: #{easing}, expecting one of #{EASING.keys.join(', ')}"
|
28
|
+
end
|
29
|
+
unless EFFECTS.include?(effect)
|
30
|
+
raise ArgumentError,
|
31
|
+
"Invalid effect: #{effect}, expecting one of #{EFFECTS.join(', ')}"
|
32
|
+
end
|
33
|
+
@easing = easing
|
34
|
+
@effect = effect
|
35
|
+
end
|
36
|
+
|
37
|
+
def ease(value)
|
38
|
+
case effect
|
39
|
+
when :in
|
40
|
+
ease_in(value)
|
41
|
+
when :out
|
42
|
+
ease_out(value)
|
43
|
+
else
|
44
|
+
ease_in_out(value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :easing, :effect
|
51
|
+
|
52
|
+
# The procs in EASING define the ease-in functions, so we simply need
|
53
|
+
# to invoke the function with the given normalized value
|
54
|
+
def ease_in(value)
|
55
|
+
EASING[easing].call(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
# The ease-out function will be the ease-in function rotated 180 degrees.
|
59
|
+
def ease_out(value)
|
60
|
+
1 - EASING[easing].call(1 - value)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ease_in_out(value)
|
64
|
+
if value < 0.5
|
65
|
+
ease_in(value * 2) / 2
|
66
|
+
else
|
67
|
+
0.5 + ease_out(2 * value - 1) / 2
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'easing'
|
2
|
+
require_relative 'frame'
|
3
|
+
|
4
|
+
module WhirledPeas
|
5
|
+
module Animator
|
6
|
+
class Frameset
|
7
|
+
def initialize(frame_slots, easing, effect)
|
8
|
+
@frame_slots = frame_slots
|
9
|
+
@easing = Easing.new(easing, effect)
|
10
|
+
@frames = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_frame(name, args={})
|
14
|
+
frames << [name, args]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Yield each frame in an "eased" order
|
18
|
+
def each_frame(&block)
|
19
|
+
frame_slots.times do |i|
|
20
|
+
input = i.to_f / (frame_slots - 1)
|
21
|
+
eased_value = @easing.ease(input)
|
22
|
+
index = (eased_value * (frames.length - 1)).floor
|
23
|
+
yield *frames[index]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :frame_slots, :easing, :frames
|
30
|
+
end
|
31
|
+
private_constant :Frameset
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'easing'
|
2
|
+
require_relative 'frameset'
|
3
|
+
|
4
|
+
module WhirledPeas
|
5
|
+
module Animator
|
6
|
+
class Producer
|
7
|
+
def self.produce(consumer, refresh_rate)
|
8
|
+
producer = new(consumer, refresh_rate)
|
9
|
+
yield producer
|
10
|
+
consumer.process
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(consumer, refresh_rate)
|
14
|
+
@consumer = consumer
|
15
|
+
@refresh_rate = refresh_rate
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_frame(name, duration: nil, args: {})
|
19
|
+
frameset(duration || 1 / refresh_rate) do |fs|
|
20
|
+
fs.add_frame(name, args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def frameset(duration, easing: :linear, effect: :in_out, &block)
|
25
|
+
fs = Frameset.new((duration * refresh_rate).round, easing, effect)
|
26
|
+
yield fs
|
27
|
+
consumer.add_frameset(fs)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :consumer, :refresh_rate
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'whirled_peas/graphics/renderer'
|
2
|
+
require 'whirled_peas/utils/ansi'
|
3
|
+
|
4
|
+
module WhirledPeas
|
5
|
+
module Animator
|
6
|
+
class RendererConsumer
|
7
|
+
def initialize(template_factory, device, width, height)
|
8
|
+
@template_factory = template_factory
|
9
|
+
@device = device
|
10
|
+
@width = width
|
11
|
+
@height = height
|
12
|
+
@renders = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_frameset(frameset)
|
16
|
+
frameset.each_frame do |frame, args|
|
17
|
+
template = template_factory.build(frame, args)
|
18
|
+
renders << Graphics::Renderer.new(template, width, height).paint
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def process
|
23
|
+
device.handle_renders(renders)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :template_factory, :device, :width, :height, :renders
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
module Command
|
3
|
+
# Abstract base class for all commands
|
4
|
+
class Base
|
5
|
+
# Returns the name of the command as expected by the command line script. By convention,
|
6
|
+
# this name is snake case version of the class name, e.g. the command `do_something`
|
7
|
+
# would be implemented by `WhirledPeas::Command::DoSomething`, which needs to inherit
|
8
|
+
# from this class.
|
9
|
+
def self.command_name
|
10
|
+
name.split('::').last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.description
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.print_usage
|
17
|
+
puts description
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :args
|
21
|
+
|
22
|
+
def initialize(args, config)
|
23
|
+
@args = args
|
24
|
+
@config = config
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the name of the command as expected by the command line script. By convention,
|
28
|
+
# this name is snake case version of the class name, e.g. the command `do_something`
|
29
|
+
# would be implemented by `WhirledPeas::Command::DoSomething`, which needs to inherit
|
30
|
+
# from this class.
|
31
|
+
def command_name
|
32
|
+
self.class.command_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_logger
|
36
|
+
require 'logger'
|
37
|
+
|
38
|
+
if config.log_file.is_a?(IO)
|
39
|
+
output = config.log_file
|
40
|
+
else
|
41
|
+
File.open(config.log_file, 'a')
|
42
|
+
end
|
43
|
+
|
44
|
+
logger = Logger.new(output)
|
45
|
+
logger.level = config.log_level
|
46
|
+
logger.formatter = config.log_formatter
|
47
|
+
logger
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [true|false] true if all of the required options were provided
|
51
|
+
def valid?
|
52
|
+
@error_text = nil
|
53
|
+
validate!
|
54
|
+
@error_text.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Display the validation error and print a usage statement
|
58
|
+
def print_error
|
59
|
+
puts @error_text if @error_text
|
60
|
+
print_usage
|
61
|
+
end
|
62
|
+
|
63
|
+
# Commands that inherit from this class must override this method
|
64
|
+
def start
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
attr_reader :config
|
70
|
+
|
71
|
+
def print_usage
|
72
|
+
puts ["Usage: #{$0} #{command_name}", *options_usage].join(' ')
|
73
|
+
end
|
74
|
+
|
75
|
+
# Commands that inherit from this class can override this method to validate
|
76
|
+
# command line options
|
77
|
+
def validate!
|
78
|
+
# Set @error_text if the options are not valid
|
79
|
+
end
|
80
|
+
|
81
|
+
def options_usage
|
82
|
+
end
|
83
|
+
end
|
84
|
+
private_constant :Base
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Command
|
5
|
+
# Abstract command that expects a config file as an argument and then requires the
|
6
|
+
# specified file. All implementing classes must call `super` if they override `start`
|
7
|
+
# or `validate!`
|
8
|
+
class ConfigCommand < Base
|
9
|
+
def start
|
10
|
+
require config_file
|
11
|
+
rescue => e
|
12
|
+
puts "Error loading #{config_file}"
|
13
|
+
puts e.message
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :config_file
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
super
|
23
|
+
# Note that the main script consumes the <command> argument from ARGV, so we
|
24
|
+
# expect the config file to be at index 0.
|
25
|
+
config_file = args.shift
|
26
|
+
if config_file.nil?
|
27
|
+
@error_text = "#{command_name} requires a config file"
|
28
|
+
elsif !File.exist?(config_file)
|
29
|
+
@error_text = "File not found: #{config_file}"
|
30
|
+
elsif config_file[-3..-1] != '.rb'
|
31
|
+
@error_text = 'Config file should be a .rb file'
|
32
|
+
else
|
33
|
+
# We think we have a valid ruby config file, set the absolute path to @config
|
34
|
+
@config_file = config_file[0] == '/' ? config_file : File.join(Dir.pwd, config_file)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def options_usage
|
39
|
+
'<config file>'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
private_constant :ConfigCommand
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'frame_command'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Command
|
5
|
+
# Display the template tree for a single frame with the specified arguments.
|
6
|
+
class Debug < FrameCommand
|
7
|
+
def self.description
|
8
|
+
'Print template tree for specified frame'
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
super
|
13
|
+
|
14
|
+
require 'whirled_peas/graphics/debugger'
|
15
|
+
|
16
|
+
template = config.template_factory.build(frame, frame_args)
|
17
|
+
puts Graphics::Debugger.new(template).debug
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Command
|
5
|
+
# List title fonts installed on the user's system and print sample text in each.
|
6
|
+
class Fonts < Base
|
7
|
+
def self.description
|
8
|
+
'List installed title fonts with sample text'
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
require 'whirled_peas/utils/title_font'
|
13
|
+
|
14
|
+
Utils::TitleFont.fonts.keys.each do |key|
|
15
|
+
puts Utils::TitleFont.to_s(key.to_s, key)
|
16
|
+
puts key.inspect
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'config_command'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Command
|
5
|
+
class FrameCommand < ConfigCommand
|
6
|
+
private
|
7
|
+
|
8
|
+
attr_reader :frame, :frame_args
|
9
|
+
|
10
|
+
def validate!
|
11
|
+
super
|
12
|
+
frame = args.shift
|
13
|
+
raw_args = args.shift
|
14
|
+
if frame.nil?
|
15
|
+
@error_text = "#{command_name} requires a frame name"
|
16
|
+
else
|
17
|
+
@frame = frame
|
18
|
+
@frame_args = {}
|
19
|
+
return if raw_args.nil?
|
20
|
+
|
21
|
+
require 'json'
|
22
|
+
|
23
|
+
JSON.parse(raw_args || '{}').each do |key, value|
|
24
|
+
@frame_args[key.to_sym] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def options_usage
|
30
|
+
"#{super} <frame> [args as a JSON string]"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|