castaway 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|