flame_channel_parser 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.txt +10 -0
- data/Manifest.txt +27 -0
- data/README.rdoc +65 -0
- data/Rakefile +13 -0
- data/lib/flame_channel_parser.rb +24 -0
- data/lib/interpolator.rb +92 -0
- data/lib/parser_2011.rb +121 -0
- data/lib/parser_2012.rb +57 -0
- data/lib/segments.rb +231 -0
- data/plots.numbers +0 -0
- data/plots_2012.numbers +0 -0
- data/test/baked.csv +201 -0
- data/test/channel_with_constants.dat +31 -0
- data/test/curve.csv +7 -0
- data/test/sample_channel.dat +31 -0
- data/test/snaps/FLEM_BrokenTangents.action +1322 -0
- data/test/snaps/FLEM_advanced_curve.png +0 -0
- data/test/snaps/FLEM_advanced_curve_example_FL2012.action +4046 -0
- data/test/snaps/FLEM_baked_curve.png +0 -0
- data/test/snaps/FLEM_curves_example.action +1988 -0
- data/test/snaps/FLEM_curves_example_migrated_to_2012.action +2708 -0
- data/test/snaps/FLEM_std_curve.png +0 -0
- data/test/snaps/TW.timewarp +12288 -0
- data/test/test_flame_channel_parser.rb +73 -0
- data/test/test_interpolator.rb +89 -0
- data/test/test_segments.rb +261 -0
- metadata +115 -0
data/.autotest
ADDED
@@ -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
|
data/.gemtest
ADDED
File without changes
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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"
|
data/lib/interpolator.rb
ADDED
@@ -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
|
+
|
data/lib/parser_2011.rb
ADDED
@@ -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
|
data/lib/parser_2012.rb
ADDED
@@ -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
|
data/lib/segments.rb
ADDED
@@ -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
|
+
|