castaway 1.0.0

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