castaway 1.0.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.
@@ -0,0 +1,39 @@
1
+ require 'castaway/scene'
2
+
3
+ module Castaway
4
+ class Production
5
+ module Scenes
6
+
7
+ # Returns the duration of the production, in seconds.
8
+ def duration
9
+ @scenes.last.finish
10
+ end
11
+
12
+ # Returns the first scene with the given title.
13
+ def scene(title)
14
+ @scenes.find { |s| s.title == title }
15
+ end
16
+
17
+ def resource(name)
18
+ self.class.resource(name)
19
+ end
20
+
21
+ def pointers
22
+ self.class.pointers
23
+ end
24
+
25
+ def _build_scenes
26
+ @scenes = self.class.scenes.map do |(name, config)|
27
+ Castaway::Scene.new(name, self).configure(&config)
28
+ end
29
+
30
+ @scenes = @scenes.sort_by(&:start)
31
+
32
+ @scenes.each.with_index do |scene, index|
33
+ scene.update_from_next(@scenes[index + 1])
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ require 'castaway/times'
2
+
3
+ module Castaway
4
+
5
+ class Range
6
+ include Castaway::Times
7
+
8
+ attr_accessor :start_frame, :end_frame
9
+
10
+ def self.at_frame(production, frame)
11
+ new(production).tap do |range|
12
+ range.start_frame = frame
13
+ range.end_frame = frame
14
+ end
15
+ end
16
+
17
+ def self.at_time(production, time)
18
+ new(production).tap do |range|
19
+ range.start_time = time
20
+ range.end_time = time
21
+ end
22
+ end
23
+
24
+ def self.at_scene(production, title)
25
+ new(production).tap do |range|
26
+ range.start_scene = title
27
+ range.end_scene = title
28
+ end
29
+ end
30
+
31
+ def initialize(production)
32
+ @production = production
33
+ @start_frame = 0
34
+ self.end_time = production.duration
35
+ end
36
+
37
+ def truncated?
38
+ start_frame > 0 || end_time < @production.duration
39
+ end
40
+
41
+ def start_time=(t)
42
+ @start_frame = (_parse_time(t) * @production.fps).floor
43
+ end
44
+
45
+ def start_time
46
+ @start_frame / @production.fps.to_f
47
+ end
48
+
49
+ def end_time=(t)
50
+ @end_frame = (_parse_time(t) * @production.fps).ceil
51
+ end
52
+
53
+ def end_time
54
+ @end_frame / @production.fps.to_f
55
+ end
56
+
57
+ def start_scene=(title)
58
+ scene = @production.scene(title)
59
+ raise ArgumentError, "no scene named #{title.inspect}" unless scene
60
+ self.start_time = scene.start
61
+ end
62
+
63
+ def end_scene=(title)
64
+ scene = @production.scene(title)
65
+ raise ArgumentError, "no scene named #{title.inspect}" unless scene
66
+ self.end_time = scene.finish
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,18 @@
1
+ require 'mini_magick'
2
+ require 'castaway/point'
3
+
4
+ module Castaway
5
+ class RelativeTo
6
+ def initialize(image_file_name, production)
7
+ path = production.resource(image_file_name)
8
+ image = MiniMagick::Image.new(path)
9
+ @width = image.width.to_f
10
+ @height = image.height.to_f
11
+ @production = production
12
+ end
13
+
14
+ def position(x, y)
15
+ Castaway::Point.new(x / @width, y / @height) * @production.resolution
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,145 @@
1
+ require 'castaway/element/matte'
2
+ require 'castaway/element/still'
3
+ require 'castaway/element/pointer'
4
+ require 'castaway/element/text'
5
+ require 'castaway/relative_to'
6
+ require 'castaway/times'
7
+
8
+ module Castaway
9
+ class Scene
10
+ include Castaway::Times
11
+
12
+ attr_reader :title
13
+ attr_reader :production
14
+
15
+ attr_reader :finish, :duration
16
+
17
+ attr_reader :_timeline
18
+
19
+ def initialize(title, production)
20
+ @title = title
21
+ @production = production
22
+ end
23
+
24
+ def configure(&block)
25
+ instance_eval(&block)
26
+ self
27
+ end
28
+
29
+ # Declares (or returns) the time value (in seconds) for the start of this
30
+ # scene. Any value parsable by Castaway::Times will be accepted.
31
+ def start(value = nil)
32
+ return @start unless value
33
+ @start = _parse_time(value)
34
+ end
35
+
36
+ # Parses and returns the seconds corresponding to the given value. See
37
+ # Castaway::Times.
38
+ def time(value)
39
+ _parse_time(value)
40
+ end
41
+
42
+ # Returns a new Castaway::RelativeTo instance for the resource with the
43
+ # given file name. This is useful for positioning pointers in a
44
+ # resolution-independent way.
45
+ def relative_to_image(name)
46
+ RelativeTo.new(name, production)
47
+ end
48
+
49
+ # Sets (or returns) the script corresponding to the current scene.
50
+ # This is not used, except informationally.
51
+ def script(*args)
52
+ if args.empty?
53
+ @script
54
+ elsif args.length == 1
55
+ @script = _strip(args.first)
56
+ else
57
+ raise ArgumentError, 'script expects 0 or 1 argument'
58
+ end
59
+ end
60
+
61
+ # Declare the plan to be used for constructing the scene. Within the plan,
62
+ # scene elements are declared and configured.
63
+ #
64
+ # plan do
65
+ # matte(:black).enter(-0.5).in(:dissolve, speed: 0.5)
66
+ # end
67
+ #
68
+ # See #matte, #still, #text, #sprite, and #pointer.
69
+ def plan(&block)
70
+ @plan = block
71
+ end
72
+
73
+ def construct(timeline)
74
+ @_timeline = timeline
75
+ instance_eval(&@plan) if @plan
76
+ ensure
77
+ remove_instance_variable :@_timeline
78
+ end
79
+
80
+ # Returns a new Castaway::Element::Matte element with the given color, and
81
+ # adds it to the timeline.
82
+ def matte(color)
83
+ Element::Matte.new(production, self, color).tap do |element|
84
+ _timeline.add(element)
85
+ end
86
+ end
87
+
88
+ # Returns a new Castaway::Element::Still element for the given filename,
89
+ # and adds it to the timeline. It will be forced to fill the entire frame.
90
+ def still(filename)
91
+ _still(filename, true)
92
+ end
93
+
94
+ # Returns a new Castaway::Element::Still element for the given filename,
95
+ # and adds it to the timeline. It's native dimensions will be preserved.
96
+ def sprite(filename)
97
+ _still(filename, false)
98
+ end
99
+
100
+ # Returns a new Castaway::Element::Pointer element and adds it to the
101
+ # timeline. If an `id` is given, the pointer declared with that `id` is
102
+ # used.
103
+ def pointer(id = :default)
104
+ Element::Pointer.new(production, self, id).tap do |pointer|
105
+ _timeline.add(pointer)
106
+ end
107
+ end
108
+
109
+ # Returns a new Castaway::Element::Text element with the given text, and
110
+ # adds it to the timeline.
111
+ def text(string)
112
+ Element::Text.new(production, self, string).tap do |text|
113
+ _timeline.add(text)
114
+ end
115
+ end
116
+
117
+ # Returns a Castaway::Point with the given coordinates multiplied by the
118
+ # resolution. This let's you declare coordinates as fractions of the frame
119
+ # size, so that they work regardless of the final rendered resolution.
120
+ def relative_position(x, y)
121
+ Castaway::Point.new(x, y) * production.resolution
122
+ end
123
+
124
+ def update_from_next(neighbor)
125
+ @finish = neighbor.nil? ? @start : neighbor.start
126
+ @duration = @finish - @start
127
+ end
128
+
129
+ def _still(filename, full)
130
+ Element::Still.new(production, self, filename, full: full).
131
+ tap do |element|
132
+ _timeline.add(element)
133
+ end
134
+ end
135
+
136
+ def _strip(text)
137
+ if text =~ /^(\s+)\S/
138
+ indent = Regexp.last_match(1)
139
+ text.gsub(/^#{indent}/, '')
140
+ else
141
+ text
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,47 @@
1
+ module Castaway
2
+
3
+ class Size < Struct.new(:width, :height)
4
+ def *(factor)
5
+ if factor.is_a?(Size)
6
+ Size.new(width * factor.width, height * factor.height)
7
+ else
8
+ Size.new(width * factor, height * factor)
9
+ end
10
+ end
11
+
12
+ def present?
13
+ width || height
14
+ end
15
+
16
+ def empty?
17
+ (width || 0).zero? && (height || 0).zero?
18
+ end
19
+
20
+ def aspect_ratio
21
+ @aspect_ratio ||= width.to_f / height
22
+ end
23
+
24
+ def with_height(height)
25
+ Size.new((aspect_ratio * height).to_i, height.to_i)
26
+ end
27
+
28
+ def with_width(width)
29
+ Size.new(width.to_i, (width / aspect_ratio).to_i)
30
+ end
31
+
32
+ def to_s
33
+ format('(%.2f, %.2f)', width, height)
34
+ end
35
+
36
+ def to_geometry
37
+ format('%.2fx%.2f', width, height).tap do |geometry|
38
+ geometry << '!' if width && height
39
+ end
40
+ end
41
+
42
+ def to_resolution
43
+ format('%dx%d', width || 0, height || 0)
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,80 @@
1
+ require 'logger'
2
+ require 'fileutils'
3
+
4
+ module Castaway
5
+
6
+ class Timeline
7
+ attr_reader :resolution, :fps
8
+
9
+ def initialize(resolution, fps)
10
+ @resolution = resolution
11
+ @elements = []
12
+ @fps = fps
13
+
14
+ @cached_command = nil
15
+ @cached_file = nil
16
+ end
17
+
18
+ def add(element)
19
+ @elements << element
20
+ end
21
+
22
+ def duration
23
+ @elements.map(&:t2).max
24
+ end
25
+
26
+ def render_frame(frame, name: 'frame')
27
+ t = frame / fps.to_f
28
+
29
+ signature = nil
30
+ tool = MiniMagick::Tool::Convert.new.tap do |convert|
31
+ convert << '-size' << resolution.to_geometry
32
+ convert.xc 'black'
33
+
34
+ @elements.sort_by(&:t1).each do |element|
35
+ next unless element.alive_at?(t)
36
+ element.render_at(t, convert)
37
+ end
38
+
39
+ convert.colorspace 'sRGB'
40
+ convert.type 'TrueColor'
41
+ convert.depth '16'
42
+
43
+ signature = convert.command
44
+ convert << "PNG48:#{name}.png"
45
+ end
46
+
47
+ if signature != @cached_command
48
+ _log frame, t, tool.command.join(' ')
49
+ @cached_command = signature
50
+ @cached_file = name
51
+ tool.call
52
+ else
53
+ old = "#{@cached_file}.png"
54
+ new = "#{name}.png"
55
+ _log frame, t, "duplicate #{old} as #{new}"
56
+ _link old, new
57
+ end
58
+ end
59
+
60
+ def _link(old, new)
61
+ # FIXME: detect whether linking is supported, and fallback to copy
62
+ # if not.
63
+
64
+ FileUtils.ln(old, new)
65
+ end
66
+
67
+ def _logger
68
+ @_logger ||= Logger.new('build.log').tap do |logger|
69
+ logger.formatter = lambda do |severity, _datetime, _progname, msg|
70
+ "#{severity}: #{msg}\n"
71
+ end
72
+ end
73
+ end
74
+
75
+ def _log(frame, t, msg)
76
+ _logger.info { format('[%d:%.2fs] %s', frame, t, msg) }
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,33 @@
1
+ module Castaway
2
+ module Times
3
+ def _parse_time(spec)
4
+ _parse_numeric_time(spec) ||
5
+ _parse_timespec_time(spec) ||
6
+ raise(ArgumentError, "unsupported time #{spec.inspect}")
7
+ end
8
+
9
+ def _parse_numeric_time(spec)
10
+ if spec.is_a?(Numeric)
11
+ spec
12
+ elsif spec =~ /^\d+(\.\d+)?s?$/
13
+ spec.to_f
14
+ end
15
+ end
16
+
17
+ def _parse_timespec_time(spec)
18
+ return unless spec =~ /^(\d+)(?::(\d+))?(?::(\d+))?(\.\d+)?$/
19
+
20
+ a = Regexp.last_match(1)
21
+ b = Regexp.last_match(2)
22
+ c = Regexp.last_match(3)
23
+ d = Regexp.last_match(4)
24
+
25
+ time = [c, b, a].compact.each.with_index.reduce(0) do |m, (v, i)|
26
+ m + v.to_i * 60**i
27
+ end
28
+
29
+ time += d.to_f if d
30
+ time
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Castaway
2
+ VERSION = '1.0.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: castaway
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.14'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_magick
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-progressbar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: chaussettes
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ description: |2
70
+ Construct screencasts in Ruby! Write your script, declare your timeline,
71
+ mix your audio, and render your video in an easily-edited, easily-repeated
72
+ DSL. (Depends on ImageMagick, Sox, and FFMPEG for the heavy-lifting.)
73
+ email:
74
+ - jamis@jamisbuck.org
75
+ executables:
76
+ - castaway
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - MIT-LICENSE
81
+ - README.md
82
+ - bin/castaway
83
+ - lib/castaway.rb
84
+ - lib/castaway/animation.rb
85
+ - lib/castaway/box.rb
86
+ - lib/castaway/cli/build.rb
87
+ - lib/castaway/cli/main.rb
88
+ - lib/castaway/cli/script.rb
89
+ - lib/castaway/cli/version.rb
90
+ - lib/castaway/effect.rb
91
+ - lib/castaway/element/base.rb
92
+ - lib/castaway/element/matte.rb
93
+ - lib/castaway/element/pointer.rb
94
+ - lib/castaway/element/still.rb
95
+ - lib/castaway/element/text.rb
96
+ - lib/castaway/interpolation.rb
97
+ - lib/castaway/interpolation/linear.rb
98
+ - lib/castaway/point.rb
99
+ - lib/castaway/production.rb
100
+ - lib/castaway/production/audio.rb
101
+ - lib/castaway/production/class_methods.rb
102
+ - lib/castaway/production/scenes.rb
103
+ - lib/castaway/range.rb
104
+ - lib/castaway/relative_to.rb
105
+ - lib/castaway/scene.rb
106
+ - lib/castaway/size.rb
107
+ - lib/castaway/timeline.rb
108
+ - lib/castaway/times.rb
109
+ - lib/castaway/version.rb
110
+ homepage: https://github.com/jamis/castaway
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.5.1
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: System for building screencasts and video presentations
134
+ test_files: []