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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +123 -0
- data/bin/castaway +5 -0
- data/lib/castaway.rb +1 -0
- data/lib/castaway/animation.rb +37 -0
- data/lib/castaway/box.rb +77 -0
- data/lib/castaway/cli/build.rb +83 -0
- data/lib/castaway/cli/main.rb +24 -0
- data/lib/castaway/cli/script.rb +28 -0
- data/lib/castaway/cli/version.rb +17 -0
- data/lib/castaway/effect.rb +59 -0
- data/lib/castaway/element/base.rb +280 -0
- data/lib/castaway/element/matte.rb +20 -0
- data/lib/castaway/element/pointer.rb +35 -0
- data/lib/castaway/element/still.rb +31 -0
- data/lib/castaway/element/text.rb +76 -0
- data/lib/castaway/interpolation.rb +23 -0
- data/lib/castaway/interpolation/linear.rb +26 -0
- data/lib/castaway/point.rb +63 -0
- data/lib/castaway/production.rb +124 -0
- data/lib/castaway/production/audio.rb +161 -0
- data/lib/castaway/production/class_methods.rb +148 -0
- data/lib/castaway/production/scenes.rb +39 -0
- data/lib/castaway/range.rb +70 -0
- data/lib/castaway/relative_to.rb +18 -0
- data/lib/castaway/scene.rb +145 -0
- data/lib/castaway/size.rb +47 -0
- data/lib/castaway/timeline.rb +80 -0
- data/lib/castaway/times.rb +33 -0
- data/lib/castaway/version.rb +3 -0
- metadata +134 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'castaway/animation'
|
2
|
+
require 'castaway/effect'
|
3
|
+
require 'castaway/point'
|
4
|
+
|
5
|
+
module Castaway
|
6
|
+
module Element
|
7
|
+
|
8
|
+
class Base
|
9
|
+
attr_reader :production, :scene
|
10
|
+
attr_reader :position, :size
|
11
|
+
|
12
|
+
# reevaluated at each render, represents the value of the attributes at
|
13
|
+
# the current point in time.
|
14
|
+
attr_reader :attributes
|
15
|
+
|
16
|
+
class Attribute < Struct.new(:_initial, :fn)
|
17
|
+
def initial
|
18
|
+
if _initial.respond_to?(:call)
|
19
|
+
_initial.call
|
20
|
+
else
|
21
|
+
_initial
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](memo, value)
|
26
|
+
fn[memo, value]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Tail < Struct.new(:owner, :amount)
|
31
|
+
def to_f
|
32
|
+
owner.duration - amount
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(production, scene)
|
37
|
+
@production = production
|
38
|
+
@scene = scene
|
39
|
+
|
40
|
+
@enter = 0
|
41
|
+
@exit = scene.duration
|
42
|
+
@position = Castaway::Point.new(0, 0)
|
43
|
+
@size = production.resolution
|
44
|
+
|
45
|
+
@animations = Hash.new { |h, k| h[k] = [] }
|
46
|
+
@attribute_defs = {}
|
47
|
+
|
48
|
+
attribute(:alpha, 1.0) { |memo, value| memo * value }
|
49
|
+
attribute(:position, -> { position }) { |memo, value| memo + value }
|
50
|
+
attribute(:size, -> { @size }) { |memo, value| memo * value }
|
51
|
+
end
|
52
|
+
|
53
|
+
# `t` is relative to the beginning of the production
|
54
|
+
def alive_at?(t)
|
55
|
+
t.between?(t1, t2)
|
56
|
+
end
|
57
|
+
|
58
|
+
def attribute(name, initial, fn = nil, &block)
|
59
|
+
@attribute_defs[name] = Attribute.new(initial, fn || block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return start time for this element, relative to the beginning of the
|
63
|
+
# production.
|
64
|
+
def t1
|
65
|
+
_absolute(enter)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return exit time for this element, relative to the beginning of the
|
69
|
+
# production.
|
70
|
+
def t2
|
71
|
+
_absolute(exit)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Specify or return the start time for this element, relative to the
|
75
|
+
# beginning of the scene.
|
76
|
+
def enter(t = nil)
|
77
|
+
if t
|
78
|
+
@enter = _convert(t)
|
79
|
+
self
|
80
|
+
else
|
81
|
+
@enter
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specify or return the exit time for this element, relative to the
|
86
|
+
# beginning of the scene.
|
87
|
+
def exit(t = nil)
|
88
|
+
if t
|
89
|
+
@exit = _convert(t)
|
90
|
+
self
|
91
|
+
else
|
92
|
+
@exit
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def duration
|
97
|
+
@exit - @enter
|
98
|
+
end
|
99
|
+
|
100
|
+
def at(*args)
|
101
|
+
if args.length == 1
|
102
|
+
@position = args.first
|
103
|
+
elsif args.length == 2
|
104
|
+
@position = Castaway::Point.new(args[0], args[1])
|
105
|
+
else
|
106
|
+
raise ArgumentError, 'expected 1 or 2 arguments to #at'
|
107
|
+
end
|
108
|
+
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
def gravity(specification)
|
113
|
+
s = size
|
114
|
+
|
115
|
+
left = 0
|
116
|
+
hcenter = (production.resolution.width - s.width) / 2.0
|
117
|
+
right = production.resolution.width - s.width
|
118
|
+
|
119
|
+
top = 0
|
120
|
+
vcenter = (production.resolution.height - s.height) / 2.0
|
121
|
+
bottom = production.resolution.height - s.height
|
122
|
+
|
123
|
+
x, y = case specification
|
124
|
+
when :northwest then [left, top ]
|
125
|
+
when :north then [hcenter, top ]
|
126
|
+
when :northeast then [right, top ]
|
127
|
+
when :west then [left, vcenter]
|
128
|
+
when :center then [hcenter, vcenter]
|
129
|
+
when :east then [right, vcenter]
|
130
|
+
when :southwest then [left, bottom ]
|
131
|
+
when :south then [hcenter, bottom ]
|
132
|
+
when :southeast then [right, bottom ]
|
133
|
+
else
|
134
|
+
raise ArgumentError, "invalid gravity #{specification.inspect}"
|
135
|
+
end
|
136
|
+
|
137
|
+
at(x, y)
|
138
|
+
end
|
139
|
+
|
140
|
+
def size(*args)
|
141
|
+
if args.empty?
|
142
|
+
@size
|
143
|
+
elsif args.length == 2
|
144
|
+
width, height = args
|
145
|
+
@size = if width.nil?
|
146
|
+
@size.with_height(height)
|
147
|
+
elsif height.nil?
|
148
|
+
@size.with_width(width)
|
149
|
+
else
|
150
|
+
Castaway::Size.new(width, height)
|
151
|
+
end
|
152
|
+
self
|
153
|
+
else
|
154
|
+
raise ArgumentError, 'expected 0 or 2 arguments to #size'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def scale(scale)
|
159
|
+
@scale = scale
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
def rotate(angle)
|
164
|
+
@angle = angle
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def in(type, options = {})
|
169
|
+
effect(:"#{type}_in", options)
|
170
|
+
end
|
171
|
+
|
172
|
+
def out(type, options = {})
|
173
|
+
effect(:"#{type}_out", options)
|
174
|
+
end
|
175
|
+
|
176
|
+
def effect(type, options = {})
|
177
|
+
Castaway::Effect.invoke(type, self, options)
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
def animate(attribute, options = {})
|
182
|
+
options = options.dup
|
183
|
+
%i( from to ).each { |a| options[a] = _convert(options[a]) }
|
184
|
+
@animations[attribute] << Animation.from_options(options)
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def path(points)
|
189
|
+
current = @position # not #position, which may give us a translated point
|
190
|
+
prior_t = 0
|
191
|
+
p0 = Castaway::Point.new(0, 0)
|
192
|
+
|
193
|
+
points.keys.sort.each do |time|
|
194
|
+
delta = points[time] - current
|
195
|
+
|
196
|
+
animate(:position, type: :linear, from: prior_t, to: time,
|
197
|
+
initial: p0, final: delta)
|
198
|
+
|
199
|
+
current = points[time]
|
200
|
+
prior_t = time
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def tail(value = 0.0)
|
205
|
+
Tail.new(self, value)
|
206
|
+
end
|
207
|
+
|
208
|
+
# `t` is the global time value, relative to the beginning of the
|
209
|
+
# production.
|
210
|
+
def render_at(t, canvas)
|
211
|
+
_evaluate_attributes!(t)
|
212
|
+
|
213
|
+
alpha = attributes[:alpha] || 1.0
|
214
|
+
size = attributes[:size] || production.resolution
|
215
|
+
position = attributes[:position] || Castaway::Point.new(0, 0)
|
216
|
+
|
217
|
+
return if alpha <= 0.0 || size.empty?
|
218
|
+
|
219
|
+
canvas.stack do |stack|
|
220
|
+
_prepare_canvas(t, stack)
|
221
|
+
stack.geometry size.to_geometry
|
222
|
+
_transform(stack)
|
223
|
+
stack.geometry position.to_geometry unless position.zero?
|
224
|
+
end
|
225
|
+
|
226
|
+
_composite(canvas, alpha)
|
227
|
+
end
|
228
|
+
|
229
|
+
def _transform(canvas)
|
230
|
+
return unless @scale || @angle
|
231
|
+
|
232
|
+
canvas.virtual_pixel 'transparent'
|
233
|
+
|
234
|
+
distort = "#{@scale || '1'} #{@angle || '0'}"
|
235
|
+
canvas.distort.+('ScaleRotateTranslate', distort.strip)
|
236
|
+
end
|
237
|
+
|
238
|
+
def _composite(canvas, alpha)
|
239
|
+
if alpha < 0.99995 # the point where %.2f rounds alpha*100 to 100
|
240
|
+
canvas.compose 'blend'
|
241
|
+
canvas.define format('compose:args=%.2f', alpha * 100)
|
242
|
+
else
|
243
|
+
canvas.compose 'src-over'
|
244
|
+
end
|
245
|
+
|
246
|
+
canvas.composite
|
247
|
+
end
|
248
|
+
|
249
|
+
def _evaluate_attributes!(t)
|
250
|
+
@attributes = @attribute_defs.keys.each.with_object({}) do |type, map|
|
251
|
+
list = @animations[type]
|
252
|
+
map[type] = _evaluate_animation_list(type, t, list)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def _evaluate_animation_list(type, t, list)
|
257
|
+
list.reduce(@attribute_defs[type].initial) do |memo, animation|
|
258
|
+
# animations are always specified relative to the "enter" time of
|
259
|
+
# element they are attached to.
|
260
|
+
relative_t = t - t1
|
261
|
+
if relative_t < animation.from
|
262
|
+
memo
|
263
|
+
else
|
264
|
+
result = animation[relative_t]
|
265
|
+
@attribute_defs[type][memo, result]
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def _absolute(t)
|
271
|
+
scene.start + t
|
272
|
+
end
|
273
|
+
|
274
|
+
def _convert(t)
|
275
|
+
t && t.to_f
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'castaway/element/base'
|
2
|
+
|
3
|
+
module Castaway
|
4
|
+
module Element
|
5
|
+
|
6
|
+
class Matte < Element::Base
|
7
|
+
attr_reader :color
|
8
|
+
|
9
|
+
def initialize(production, scene, color)
|
10
|
+
super(production, scene)
|
11
|
+
@color = color
|
12
|
+
end
|
13
|
+
|
14
|
+
def _prepare_canvas(_t, canvas)
|
15
|
+
canvas.xc @color
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'castaway/element/still'
|
2
|
+
require 'castaway/box'
|
3
|
+
require 'castaway/point'
|
4
|
+
|
5
|
+
module Castaway
|
6
|
+
module Element
|
7
|
+
|
8
|
+
class Pointer < Element::Still
|
9
|
+
def initialize(production, scene, id)
|
10
|
+
path, options = production.pointers.fetch(id)
|
11
|
+
super(production, scene, path)
|
12
|
+
|
13
|
+
@box = Box.from_size(@size)
|
14
|
+
@box[:hotspot] = Castaway::Point.make(options[:hotspot] || [0, 0])
|
15
|
+
|
16
|
+
ideal_width = production.resolution.width * options[:scale]
|
17
|
+
sx = ideal_width.to_f / @size.width
|
18
|
+
|
19
|
+
scale(sx)
|
20
|
+
end
|
21
|
+
|
22
|
+
def hotspot
|
23
|
+
@box.
|
24
|
+
scale(@scale || 0).
|
25
|
+
rotate(@angle || 0).
|
26
|
+
bounds[:hotspot]
|
27
|
+
end
|
28
|
+
|
29
|
+
def position
|
30
|
+
@position - hotspot
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'castaway/element/base'
|
2
|
+
require 'mini_magick'
|
3
|
+
|
4
|
+
module Castaway
|
5
|
+
module Element
|
6
|
+
|
7
|
+
class Still < Element::Base
|
8
|
+
attr_reader :filename, :info
|
9
|
+
|
10
|
+
def initialize(production, scene, filename, full: false)
|
11
|
+
super(production, scene)
|
12
|
+
|
13
|
+
@filename = production.resource(filename)
|
14
|
+
@info = MiniMagick::Image.new(@filename)
|
15
|
+
|
16
|
+
@size = if full
|
17
|
+
# scale to production resolution
|
18
|
+
production.resolution
|
19
|
+
else
|
20
|
+
# use native image size
|
21
|
+
Castaway::Size.new(@info.width, @info.height)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def _prepare_canvas(_t, canvas)
|
26
|
+
canvas << @filename
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'castaway/element/base'
|
2
|
+
|
3
|
+
module Castaway
|
4
|
+
module Element
|
5
|
+
|
6
|
+
class Text < Element::Base
|
7
|
+
def initialize(production, scene, string)
|
8
|
+
super(production, scene)
|
9
|
+
|
10
|
+
@string = string
|
11
|
+
@gravity = 'Center'
|
12
|
+
@font = 'TimesNewRoman'
|
13
|
+
@font_size = 24
|
14
|
+
@background = 'transparent'
|
15
|
+
@kerning = 0
|
16
|
+
@fill = @stroke = nil
|
17
|
+
|
18
|
+
attribute(:font_size, 1) { |memo, value| memo * value }
|
19
|
+
attribute(:kerning, -> { @kerning }) { |memo, value| memo + value }
|
20
|
+
end
|
21
|
+
|
22
|
+
def fill(color)
|
23
|
+
@fill = color
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def stroke(color)
|
28
|
+
@stroke = color
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def background(color)
|
33
|
+
@background = color
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def kerning(kerning)
|
38
|
+
@kerning = kerning
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def gravity(gravity)
|
43
|
+
@gravity = gravity
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def font(font)
|
48
|
+
@font = font
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def font_size(size)
|
53
|
+
@font_size = size
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def _prepare_canvas(t, canvas)
|
58
|
+
canvas.xc @background
|
59
|
+
|
60
|
+
font_size = @font_size * attributes[:font_size]
|
61
|
+
kerning = attributes[:kerning]
|
62
|
+
|
63
|
+
canvas.pointsize font_size
|
64
|
+
|
65
|
+
commands = [ "gravity #{@gravity}", "font '#{@font}'" ]
|
66
|
+
commands << "fill #{@fill}" if @fill
|
67
|
+
commands << "stroke #{@stroke}" if @stroke
|
68
|
+
commands << format('kerning %.1f', kerning) if kerning
|
69
|
+
commands << "text 0,0 '#{@string}'"
|
70
|
+
|
71
|
+
canvas.draw commands.join(' ')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Castaway
|
2
|
+
|
3
|
+
module Interpolation
|
4
|
+
def self.lookup(options)
|
5
|
+
_lookup_by_class(options) ||
|
6
|
+
_lookup_by_type(options) ||
|
7
|
+
raise(ArgumentError, "cannot find interpolation for #{value.inspect}")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self._lookup_by_class(options)
|
11
|
+
options[:interpolator]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self._lookup_by_type(options)
|
15
|
+
case options[:type]
|
16
|
+
when :linear, nil then Castaway::Interpolation::Linear
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'castaway/interpolation/linear'
|