whirled_peas 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|