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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +116 -88
  5. data/Rakefile +1 -20
  6. data/bin/reset_cursor +11 -0
  7. data/bin/screen_test +68 -0
  8. data/examples/intro.rb +3 -3
  9. data/examples/scrolling.rb +5 -4
  10. data/lib/whirled_peas.rb +2 -4
  11. data/lib/whirled_peas/animator.rb +5 -0
  12. data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
  13. data/lib/whirled_peas/animator/easing.rb +72 -0
  14. data/lib/whirled_peas/animator/frame.rb +5 -0
  15. data/lib/whirled_peas/animator/frameset.rb +33 -0
  16. data/lib/whirled_peas/animator/producer.rb +35 -0
  17. data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
  18. data/lib/whirled_peas/command.rb +5 -0
  19. data/lib/whirled_peas/command/base.rb +86 -0
  20. data/lib/whirled_peas/command/config_command.rb +44 -0
  21. data/lib/whirled_peas/command/debug.rb +21 -0
  22. data/lib/whirled_peas/command/fonts.rb +22 -0
  23. data/lib/whirled_peas/command/frame_command.rb +34 -0
  24. data/lib/whirled_peas/command/frames.rb +24 -0
  25. data/lib/whirled_peas/command/help.rb +38 -0
  26. data/lib/whirled_peas/command/play.rb +108 -0
  27. data/lib/whirled_peas/command/record.rb +57 -0
  28. data/lib/whirled_peas/command/still.rb +29 -0
  29. data/lib/whirled_peas/command_line.rb +22 -212
  30. data/lib/whirled_peas/config.rb +56 -6
  31. data/lib/whirled_peas/device.rb +5 -0
  32. data/lib/whirled_peas/device/null_device.rb +8 -0
  33. data/lib/whirled_peas/device/output_file.rb +19 -0
  34. data/lib/whirled_peas/device/screen.rb +26 -0
  35. data/lib/whirled_peas/graphics/container_painter.rb +91 -0
  36. data/lib/whirled_peas/graphics/painter.rb +10 -0
  37. data/lib/whirled_peas/graphics/renderer.rb +8 -2
  38. data/lib/whirled_peas/utils/ansi.rb +13 -0
  39. data/lib/whirled_peas/utils/file_handler.rb +57 -0
  40. data/lib/whirled_peas/version.rb +1 -1
  41. data/tools/whirled_peas/tools/screen_tester.rb +117 -65
  42. metadata +27 -8
  43. data/lib/whirled_peas/frame.rb +0 -6
  44. data/lib/whirled_peas/frame/consumer.rb +0 -30
  45. data/lib/whirled_peas/frame/debug_consumer.rb +0 -30
  46. data/lib/whirled_peas/frame/event_loop.rb +0 -90
  47. data/lib/whirled_peas/frame/producer.rb +0 -67
  48. data/lib/whirled_peas/graphics/screen.rb +0 -70
@@ -40,13 +40,13 @@ class TemplateFactory
40
40
  end
41
41
  end
42
42
 
43
- class Driver
43
+ class Application
44
44
  def start(producer)
45
- producer.send_frame('intro', duration: 5)
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.driver = Driver.new
51
+ config.application = Application.new
52
52
  end
@@ -39,15 +39,16 @@ class TemplateFactory
39
39
  end
40
40
  end
41
41
 
42
- class Driver
42
+ class Application
43
43
  def start(producer)
44
- 53.times do |i|
45
- producer.send_frame('intro', duration: 0.3, args: { top: -i })
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.driver = Driver.new
53
+ config.application = Application.new
53
54
  end
@@ -1,8 +1,6 @@
1
- require 'logger'
2
-
3
- require 'whirled_peas/errors'
1
+ require 'whirled_peas/animator'
4
2
  require 'whirled_peas/config'
5
- require 'whirled_peas/frame'
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,5 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ end
4
+ private_constant :Animator
5
+ end
@@ -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,5 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ Frame = Struct.new(:name, :args)
4
+ end
5
+ 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,5 @@
1
+ module WhirledPeas
2
+ module Command
3
+ end
4
+ private_constant :Command
5
+ 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