flame_channel_parser 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
File without changes
@@ -0,0 +1,10 @@
1
+ === 1.1.1 / 2011-05-17
2
+
3
+ * Added support for 2012 Bezier splines
4
+
5
+ === 1.0.0 / 2011-03-21
6
+
7
+ * 1 major enhancement
8
+
9
+ * Birthday!
10
+
@@ -0,0 +1,27 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/flame_channel_parser.rb
7
+ lib/interpolator.rb
8
+ lib/parser_2011.rb
9
+ lib/parser_2012.rb
10
+ lib/segments.rb
11
+ plots.numbers
12
+ plots_2012.numbers
13
+ test/baked.csv
14
+ test/channel_with_constants.dat
15
+ test/curve.csv
16
+ test/sample_channel.dat
17
+ test/snaps/FLEM_BrokenTangents.action
18
+ test/snaps/FLEM_advanced_curve.png
19
+ test/snaps/FLEM_advanced_curve_example_FL2012.action
20
+ test/snaps/FLEM_baked_curve.png
21
+ test/snaps/FLEM_curves_example.action
22
+ test/snaps/FLEM_curves_example_migrated_to_2012.action
23
+ test/snaps/FLEM_std_curve.png
24
+ test/snaps/TW.timewarp
25
+ test/test_flame_channel_parser.rb
26
+ test/test_interpolator.rb
27
+ test/test_segments.rb
@@ -0,0 +1,65 @@
1
+ = flame_channel_parser
2
+
3
+ * http://guerilla-di.org/flame-channel-parser
4
+
5
+ == DESCRIPTION:
6
+
7
+ Includes a small library for parsing and baking anmation curves made on Discrodesk Floke/Inflinto, also known as flame.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Only constant extrapolation for now, no looping or pingpong or linear
12
+ * Expressions on channels won't be evaluated (obviously!)
13
+
14
+ == SYNOPSIS:
15
+
16
+ require "flame_channel_parser"
17
+ channels = File.open("TW_Setup.timewarp") do | f |
18
+ FlameChannelParser.parse(f)
19
+ end
20
+
21
+ # Find the channel that we are interested in, in this case
22
+ # this is the "Timing" channel from any Timewarp setup
23
+ frame_channel = channels.find{|c| c.name == "Timing/Timing" }
24
+
25
+ # Grab the interpolator object for this channel.
26
+ interpolator = FlameChannelParser::Interpolator.new(frame_channel)
27
+
28
+ # Now sample from frame 20 to frame 250.
29
+ # You can also sample at fractional frames if you want to.
30
+ (20..250).each do | frame_in_setup |
31
+ p interpolator.value_at(frame_in_setup)
32
+ end
33
+
34
+ == REQUIREMENTS:
35
+
36
+ * FIX (list of requirements)
37
+
38
+ == INSTALL:
39
+
40
+ * FIX (sudo gem install, anything else)
41
+
42
+ == LICENSE:
43
+
44
+ (The MIT License)
45
+
46
+ Copyright (c) 2011 Julik Tarkhanov
47
+
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of this software and associated documentation files (the
50
+ 'Software'), to deal in the Software without restriction, including
51
+ without limitation the rights to use, copy, modify, merge, publish,
52
+ distribute, sublicense, and/or sell copies of the Software, and to
53
+ permit persons to whom the Software is furnished to do so, subject to
54
+ the following conditions:
55
+
56
+ The above copyright notice and this permission notice shall be
57
+ included in all copies or substantial portions of the Software.
58
+
59
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
60
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
62
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
63
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
64
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
65
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'flame_channel_parser' do | p |
7
+ p.readme_file = 'README.rdoc'
8
+ p.extra_rdoc_files = FileList['*.rdoc'] + FileList['*.txt']
9
+ p.developer('Julik Tarkhanov', 'me@julik.nl')
10
+ p.clean_globs = %w( **/.DS_Store coverage.info **/*.rbc .idea .yardoc)
11
+ end
12
+
13
+ # vim: syntax=ruby
@@ -0,0 +1,24 @@
1
+ require "delegate"
2
+
3
+ module FlameChannelParser
4
+ VERSION = '1.1.1'
5
+
6
+ # Parse a Flame setup into an array of ChannelBlock objects
7
+ def self.parse(io)
8
+ # Scan the IO
9
+ parser_class = Parser2011
10
+ until io.eof?
11
+ str = io.gets
12
+ if str =~ /RHandleX/ # Flame 2012, use that parser
13
+ parser_class = Parser2012
14
+ break
15
+ end
16
+ end
17
+ io.rewind
18
+ parser_class.new.parse(io)
19
+ end
20
+ end
21
+
22
+ require File.dirname(__FILE__) + "/parser_2011"
23
+ require File.dirname(__FILE__) + "/parser_2012"
24
+ require File.dirname(__FILE__) + "/interpolator"
@@ -0,0 +1,92 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + "/segments"
2
+
3
+ # Used to sample Flame animation curves. Pass a Channel
4
+ # object to the interpolator and you can then sample values at arbitrary
5
+ # frames.
6
+ #
7
+ # i = Interpolator.new(parsed_channel)
8
+ # i.value_at(245.5) # => will interpolate and return the value
9
+ #
10
+ class FlameChannelParser::Interpolator
11
+ include FlameChannelParser::Segments
12
+
13
+ attr_reader :segments
14
+
15
+ NEG_INF = (-1.0/0.0)
16
+ POS_INF = (1.0/0.0)
17
+
18
+ # The constructor will accept a ChannelBlock object and convert it internally to a number of
19
+ # segments from which samples can be made
20
+ def initialize(channel)
21
+
22
+ # Edge case - channel has no anim at all
23
+ if (channel.length == 0)
24
+ @segments = [ConstantFunction.new(channel.base_value)]
25
+ elsif (channel.length == 1)
26
+ @segments = [ConstantFunction.new(channel[0].value)]
27
+ else
28
+ @segments = []
29
+
30
+ # TODO: extrapolation is set for the whole channel, both begin and end.
31
+ # First the prepolating segment
32
+ @segments << ConstantPrepolate.new(channel[0].frame, channel[0].value)
33
+
34
+ # The last key defines extrapolation for the rest of the curve...
35
+ channel[0..-2].each_with_index do | key, index |
36
+ @segments << key_pair_to_segment(key, channel[index + 1])
37
+ end
38
+
39
+ # so we just output it separately
40
+ @segments << ConstantExtrapolate.new(@segments[-1].end_frame, channel[-1].value)
41
+ end
42
+ end
43
+
44
+ # Sample the value of the animation curve at this frame
45
+ def sample_at(frame)
46
+ segment = @segments.find{|s| s.defines?(frame) }
47
+ segment.value_at(frame)
48
+ end
49
+
50
+ # Returns the first frame number that is concretely defined as a keyframe
51
+ # after the prepolation ends
52
+ def first_defined_frame
53
+ first_f = @segments[0].end_frame
54
+ return 1 if first_f == NEG_INF
55
+ return first_f
56
+ end
57
+
58
+ # Returns the last frame number that is concretely defined as a keyframe
59
+ # before the extrapolation starts
60
+ def last_defined_frame
61
+ last_f = @segments[-1].start_frame
62
+ return 100 if last_f == POS_INF
63
+ return last_f
64
+ end
65
+
66
+ private
67
+
68
+ # We need both the preceding and the next key
69
+ def key_pair_to_segment(key, next_key)
70
+ case key.interpolation
71
+ when :bezier
72
+ BezierSegment.new(key.frame, next_key.frame,
73
+ key.value, next_key.value,
74
+ key.r_handle_x,
75
+ key.r_handle_y,
76
+ next_key.l_handle_x, next_key.l_handle_y)
77
+ when :natural, :hermite
78
+ HermiteSegment.new(key.frame, next_key.frame, key.value, next_key.value, key.right_slope, incoming_slope(next_key))
79
+ when :constant
80
+ ConstantSegment.new(key.frame, next_key.frame, key.value)
81
+ else # Linear and safe
82
+ LinearSegment.new(key.frame, next_key.frame, key.value, next_key.value)
83
+ end
84
+ end
85
+
86
+ # Flame uses the right slope for both left and right unless the BrokenSlope tag is set
87
+ def incoming_slope(key)
88
+ key.broken? ? key.left_slope : key.right_slope
89
+ end
90
+
91
+ end
92
+
@@ -0,0 +1,121 @@
1
+ require "delegate"
2
+
3
+ class FlameChannelParser::Parser2011
4
+
5
+ class Key
6
+ attr_accessor :frame, :value, :interpolation, :extrapolation, :left_slope, :right_slope, :break_slope
7
+ alias_method :to_s, :inspect
8
+
9
+ def broken?
10
+ break_slope
11
+ end
12
+ end
13
+
14
+ def matchers
15
+ [
16
+ [:frame, :to_i, /Frame ([\-\d\.]+)/],
17
+ [:value, :to_f, /Value ([\-\d\.]+)/],
18
+ [:left_slope, :to_f, /LeftSlope ([\-\d\.]+)/],
19
+ [:right_slope, :to_f, /RightSlope ([\-\d\.]+)/],
20
+ [:interpolation, :to_s, /Interpolation (\w+)/],
21
+ [:extrapolation, :to_s, /Extrapolation (\w+)/],
22
+ [:break_slope, :to_s, /BreakSlope (\w+)/]
23
+ ]
24
+ end
25
+
26
+ def create_key
27
+ Key.new
28
+ end
29
+
30
+ class ChannelBlock < DelegateClass(Array)
31
+ attr_accessor :base_value
32
+ attr_accessor :name
33
+ def initialize(io, channel_name, parent_parser)
34
+ super([])
35
+
36
+ @parser = parent_parser
37
+ @name = channel_name.strip
38
+
39
+ base_value_matcher = /Value ([\-\d\.]+)/
40
+ keyframe_count_matcher = /Size (\d+)/
41
+ indent = nil
42
+
43
+ while line = io.gets
44
+
45
+ unless indent
46
+ indent = line.scan(/^(\s+)/)[1]
47
+ end_mark = "#{indent}End"
48
+ end
49
+
50
+ if line =~ keyframe_count_matcher
51
+ $1.to_i.times { push(extract_key_from(io)) }
52
+ elsif line =~ base_value_matcher && empty?
53
+ self.base_value = $1.to_f
54
+ elsif line.strip == end_mark
55
+ break
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+
62
+
63
+ def create_key
64
+ Key.new
65
+ end
66
+
67
+ INTERPS = [:constant, :linear, :hermite, :natural, :bezier]
68
+
69
+ def extract_key_from(io)
70
+ frame = nil
71
+ end_matcher = /End/
72
+
73
+ key = @parser.create_key
74
+ matchers = @parser.matchers
75
+
76
+ until io.eof?
77
+ line = io.gets
78
+ if line =~ end_matcher
79
+ return key
80
+ else
81
+ matchers.each do | property, cast_method, pattern |
82
+ if line =~ pattern
83
+ v = symbolize_literal($1.send(cast_method))
84
+ key.send("#{property}=", v)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ raise "Did not detect any keyframes!"
90
+ end
91
+
92
+ LITERALS = %w( linear constant natural hermite)
93
+
94
+ def symbolize_literal(v)
95
+ LITERALS.include?(v) ? v.to_sym : v
96
+ end
97
+
98
+ end
99
+
100
+ CHANNEL_MATCHER = /Channel (.+)\n/
101
+
102
+ def parse(io)
103
+ channels = []
104
+ until io.eof?
105
+ line = io.gets
106
+ if line =~ CHANNEL_MATCHER && channel_is_useful?($1)
107
+ report_progress("Extracting channel #{$1}")
108
+ channels << ChannelBlock.new(io, $1, self)
109
+ end
110
+ end
111
+ channels
112
+ end
113
+
114
+ def channel_is_useful?(channel_name)
115
+ true
116
+ end
117
+
118
+ def report_progress(message)
119
+ # flunk
120
+ end
121
+ end
@@ -0,0 +1,57 @@
1
+ require "delegate"
2
+ require File.dirname(__FILE__) + "/interpolator"
3
+
4
+ class FlameChannelParser::Parser2012 < FlameChannelParser::Parser2011
5
+
6
+ class ModernKey
7
+ attr_accessor :frame, :value, :r_handle_x, :l_handle_x, :r_handle_y, :l_handle_y, :curve_mode, :curve_order, :break_slope
8
+ alias_method :to_s, :inspect
9
+
10
+ # Adapter for old interpolation
11
+ def interpolation
12
+ return :constant if @curve_order.to_s == "constant"
13
+ return :hermite if @curve_order.to_s == "cubic" && (@curve_mode.to_s == "hermite" || @curve_mode.to_s == "natural")
14
+ return :bezier if @curve_order.to_s == "cubic" && @curve_mode.to_s == "bezier"
15
+ return :linear if @curve_order.to_s == "linear"
16
+
17
+ raise "Cannot determine interpolation for #{self.inspect}"
18
+ end
19
+
20
+ # Compute pre-212 slope which we use for interpolations
21
+ def left_slope
22
+ dy = value - l_handle_y
23
+ dx = l_handle_x - frame
24
+ dy / dx * -1
25
+ end
26
+
27
+ # Compute pre-212 slope which we use for interpolations
28
+ def right_slope
29
+ dy = value - r_handle_y
30
+ dx = frame - r_handle_x
31
+ dy / dx
32
+ end
33
+
34
+ def broken?
35
+ break_slope
36
+ end
37
+ end
38
+
39
+ def matchers
40
+ [
41
+ [:frame, :to_i, /Frame ([\-\d\.]+)/],
42
+ [:value, :to_f, /Value ([\-\d\.]+)/],
43
+ [:r_handle_x, :to_f, /RHandleX ([\-\d\.]+)/],
44
+ [:l_handle_x, :to_f, /LHandleX ([\-\d\.]+)/],
45
+ [:r_handle_y, :to_f, /RHandleY ([\-\d\.]+)/],
46
+ [:l_handle_y, :to_f, /LHandleY ([\-\d\.]+)/],
47
+ [:curve_mode, :to_s, /CurveMode (\w+)/],
48
+ [:curve_order, :to_s, /CurveOrder (\w+)/],
49
+ [:break_slope, :to_s, /BreakSlope (\w+)/],
50
+ ]
51
+ end
52
+
53
+
54
+ def create_key
55
+ ModernKey.new
56
+ end
57
+ end
@@ -0,0 +1,231 @@
1
+ require "matrix"
2
+
3
+ module FlameChannelParser::Segments
4
+
5
+ # This segment just stays on the value of it's keyframe
6
+ class ConstantSegment
7
+
8
+ NEG_INF = (-1.0/0.0)
9
+ POS_INF = (1.0/0.0)
10
+
11
+ attr_reader :start_frame, :end_frame
12
+
13
+ # Tells whether this segment defines the value of the function at this time T
14
+ def defines?(frame)
15
+ (frame < end_frame) && (frame >= start_frame)
16
+ end
17
+
18
+ # Returns the value at this time T
19
+ def value_at(frame)
20
+ @v1
21
+ end
22
+
23
+ def initialize(from_frame, to_frame, value)
24
+ @start_frame = from_frame
25
+ @end_frame = to_frame
26
+
27
+ @v1 = value
28
+ end
29
+ end
30
+
31
+ # This segment linearly interpolates
32
+ class LinearSegment < ConstantSegment
33
+
34
+ def initialize(from_frame, to_frame, value1, value2)
35
+ @vint = (value2 - value1)
36
+ super(from_frame, to_frame, value1)
37
+ end
38
+
39
+ # Returns the value at this time T
40
+ def value_at(frame)
41
+ on_t_interval = (frame - @start_frame).to_f / (@end_frame - @start_frame)
42
+ @v1 + (on_t_interval * @vint)
43
+ end
44
+ end
45
+
46
+ # This segment does Hermite interpolation
47
+ # using the Flame algo.
48
+ class HermiteSegment < LinearSegment
49
+
50
+ # In Ruby matrix columns are arrays, so here we go
51
+ HERMATRIX = Matrix[
52
+ [2, -3, 0, 1],
53
+ [-2, 3, 0, 0],
54
+ [1, -2, 1, 0],
55
+ [1, -1, 0, 0]
56
+ ].transpose
57
+
58
+ def initialize(from_frame, to_frame, value1, value2, tangent1, tangent2)
59
+
60
+ @start_frame, @end_frame = from_frame, to_frame
61
+
62
+ frame_interval = (@end_frame - @start_frame)
63
+
64
+ # Default tangents in flame are 0, so when we do nil.to_f this is what we will get
65
+ # CC = {P1, P2, T1, T2}
66
+ p1, p2, t1, t2 = value1, value2, tangent1.to_f * frame_interval, tangent2.to_f * frame_interval
67
+ @hermite = Vector[p1, p2, t1, t2]
68
+ end
69
+
70
+ # P[s_] = S[s].h.CC where s is 0..1 float interpolant on T (interval)
71
+ def value_at(frame)
72
+
73
+ # Q[frame_] = P[ ( frame - 149 ) / (time_to - time_from)]
74
+ on_t_interval = (frame - @start_frame).to_f / (@end_frame - @start_frame)
75
+
76
+ # S[s_] = {s^3, s^2, s^1, s^0}
77
+ multipliers_vec = Vector[on_t_interval ** 3, on_t_interval ** 2, on_t_interval ** 1, on_t_interval ** 0]
78
+
79
+ # P[s_] = S[s].h.CC --> Kaboom!
80
+ interpolated_scalar = dot_product(HERMATRIX * @hermite, multipliers_vec)
81
+ end
82
+
83
+ private
84
+
85
+ def dot_product(one, two)
86
+ sum = 0.0
87
+ (0...one.size).each { |i| sum += one[i] * two[i] }
88
+ sum
89
+ end
90
+
91
+ end
92
+
93
+ Point = Struct.new(:x, :y, :tanx, :tany)
94
+
95
+ class BezierSegment < LinearSegment
96
+ def initialize(x1, x2, y1, y2, t1x, t1y, t2x, t2y)
97
+ @start_frame, @end_frame = x1, x2
98
+
99
+ @a = Point.new(x1, y1, t1x, t1y)
100
+ @b = Point.new(x2, y2, t2x, t2y)
101
+ end
102
+
103
+ def value_at(frame)
104
+ # Solve T from X. This determines the correlation between X and T.
105
+ t = approximate_t(frame, @a.x, @a.tanx, @b.tanx, @b.x)
106
+ vy = bezier(t, @a.y, @a.tany, @b.tany, @b.y)
107
+ end
108
+
109
+ private
110
+
111
+ # t is the T interpolant (0 < T < 1)
112
+ # a is the coordinate of the starting vertex
113
+ # b is the coordinate of the left tangent handle
114
+ # c is the coordinate of the right tangent handle
115
+ # d is the coordinate of right vertex
116
+ def bezier(t, a, b, c, d)
117
+ a + (a*(-3) + b*3)*(t) + (a*3 - b*6 + c*3)*(t**2) + (-a + b*3 - c*3 + d)*(t**3)
118
+ end
119
+
120
+ def clamp(value)
121
+ return 0.0 if value < 0
122
+ return 1.0 if value > 1
123
+ return value
124
+ end
125
+
126
+ # /**
127
+ # * Returns the approximated parameter of a parametric curve for the value X
128
+ # * @param atX At which value should the parameter be evaluated
129
+ # * @param p0x The first interpolation point of a curve segment
130
+ # * @param c0x The first control point of a curve segment
131
+ # * @param c1x The second control point of a curve segment
132
+ # * @param P1_x The second interpolation point of a curve segment
133
+ # * @return The parametric argument that is used to retrieve atX using the parametric function representation of this curve
134
+ # */
135
+
136
+ APPROXIMATION_EPSILON = 1.0e-09
137
+ VERYSMALL = 1.0e-20
138
+ MAXIMUM_ITERATIONS = 1000
139
+
140
+ # This is how OPENCOLLADA suggests approximating Bezier animation curves
141
+ # http://www.collada.org/public_forum/viewtopic.php?f=12&t=1132
142
+ def approximate_t (atX, p0x, c0x, c1x, p1x )
143
+
144
+ return 0.0 if (atX - p0x < VERYSMALL)
145
+ return 1.0 if (p1x - atX < VERYSMALL)
146
+
147
+ u, v = 0.0, 1.0
148
+
149
+ # iteratively apply subdivision to approach value atX
150
+ MAXIMUM_ITERATIONS.times do
151
+
152
+ # de Casteljau Subdivision.
153
+ a = (p0x + c0x) / 2.0
154
+ b = (c0x + c1x) / 2.0
155
+ c = (c1x + p1x) / 2.0
156
+ d = (a + b) / 2.0
157
+ e = (b + c) / 2.0
158
+ f = (d + e) / 2.0 # this one is on the curve!
159
+
160
+ # The curve point is close enough to our wanted atX
161
+ if ((f - atX).abs < APPROXIMATION_EPSILON)
162
+ return clamp((u + v)*0.5)
163
+ end
164
+
165
+ # dichotomy
166
+ if (f < atX)
167
+ p0x = f
168
+ c0x = e
169
+ c1x = c
170
+ u = (u + v) / 2.0
171
+ else
172
+ c0x = a
173
+ c1x = d
174
+ p1x = f
175
+ v = (u + v) / 2.0
176
+ end
177
+ end
178
+
179
+ return ClampToZeroOne((u + v) / 2)
180
+
181
+ end
182
+ end
183
+
184
+ # This segment does prepolation of a constant value
185
+ class ConstantPrepolate < LinearSegment
186
+ def initialize(upto_frame, base_value)
187
+ @value = base_value
188
+ @end_frame = upto_frame
189
+ @start_frame = NEG_INF
190
+ end
191
+
192
+ def value_at(frame)
193
+ @value
194
+ end
195
+
196
+ private
197
+ def frame_increment
198
+ 23.0
199
+ end
200
+ end
201
+
202
+ # This segment does extrapolation using a constant value
203
+ class ConstantExtrapolate < LinearSegment
204
+ def initialize(from_frame, base_value)
205
+ @start_frame = from_frame
206
+ @base_value = base_value
207
+ @end_frame = POS_INF
208
+ end
209
+
210
+ def value_at(frame)
211
+ @base_value
212
+ end
213
+ end
214
+
215
+ # This can be used for an anim curve that stays constant all along
216
+ class ConstantFunction < ConstantSegment
217
+
218
+ def defines?(frame)
219
+ true
220
+ end
221
+
222
+ def initialize(value)
223
+ @value = value
224
+ end
225
+
226
+ def value_at(frame)
227
+ @value
228
+ end
229
+ end
230
+ end
231
+