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,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'