flame_channel_parser 1.1.1
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.
- 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
|
+
|