bezier_curve 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7bdfef0208f447bf444392f207505bcf076d56db
4
+ data.tar.gz: fffe1353eca9734c7b1e5a8076a0da179bfd0435
5
+ SHA512:
6
+ metadata.gz: 7b57e98d8ffcd9ac90b1d969e3728890bc654493d56006d8ec5a5d39f4386e6b6eb5252a6272bfc8c10511130b5bf7f0cbaedcfb8c62ae2779dedf9092119818
7
+ data.tar.gz: e8f69f9b96cb70bed85e8f70167247e33ecd26dd9536061c822b0773f4391b8f274303fc95958d1afc881b549d6c42c6f39d52a787fc15e615b8842bf16b6e8a
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # bezier_curve
2
+ A bézier curve library for Ruby, supporting n-dimensional, nth-degree curves
3
+
4
+ A rather simple and small library that should do pretty much anything you need to do with a bezier curve. It supports arbitrary numbers of dimensions, and arbitrary numbers of control points. Even still, it is pretty easy to use. Here's a simple quadratic (1st degree) bezier curve, in 2-dimensional space:
5
+
6
+ # create the curve
7
+ curve = BezierCurve.new([0,0],[0,1],[1,1])
8
+ # determine its points for drawing purposes
9
+ curve.points
10
+ [[0, 0], [0.00390625, 0.12109375], ... [0.87890625, 0.99609375], 1, 1]
11
+
12
+ When outputting points, it automatically chooses a suitable angle tolerance which would make the resulting poly-line appear relatively smooth (no angle changes more than about 3 degrees). But you can choose your own tolerance:
13
+
14
+ # Extreme precision, less than 1 degree tolerance
15
+ curve.points(tolerance: Math::PI/180)
16
+ # just give me 5 points, quick:
17
+ curve.points(count: 5)
18
+
19
+ You can easily split a curve into two, at a given value of `t`
20
+
21
+ # split the curve at its midpoint
22
+ curve.split_at(0.5)
23
+ #=> [BezierCurve([0, 0], [0.0, 0.5], [0.25, 0.75]), BezierCurve([0.25, 0.75], [0.5, 1.0], [1, 1])]
24
+
25
+ You can also specify n-dimensional curves:
26
+
27
+ # a 4-dimensional curve of 2nd order
28
+ curve = BezierCurve.new([0,0,0,0], [1,1,2,2], [4,5,9,3])
29
+ # get its points
30
+ curve.points(count:3)
31
+ #=> [[0.0, 0.0, 0.0, 0.0], [1.5, 1.75, 3.25, 1.75], [4.0, 5.0, 9.0, 3.0]]
32
+
33
+ You can also specify any degree/order of curve you like:
34
+
35
+ # 2d curve, 6th degree
36
+ curve = BezierCurve.new([0,0],[1,1],[2,-1],[3,1],[4,-1],[5,1],[6,-1])
37
+ curve.degree
38
+ #=> 6
39
+ curve.points.size
40
+ #=> 35
41
+
42
+ Note that the complexity goes up exponentially for higher degree curves, so performance may suffer if used extensively; but the capability is there if you need it. In fact, if you want, you can really slow things down and do a 18-dimensional curve of the 300th order. The sky is the limit. Well, your processor and memory are the limit, but you get the idea.
43
+
44
+ I am interested in making this the most useful ruby library for working with bezier curves. If you need help, or have a suggestion for improvement, feel free to file an issue on Github (https://github.com/marcuserronius/bezier_curve/), or contact me directly.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
@@ -0,0 +1,46 @@
1
+ # bezier_curve/n_point.rb
2
+
3
+ # Extends an array to treat it as an n-dimensional point. Should not
4
+ # occlude any original Array methods
5
+ module NPoint
6
+ # calculates how far this point is from `other`
7
+ def dist_from(other)
8
+ raise "Dimension counts don't match! (%i and %i)" %[size,other.size] unless size == other.size
9
+ zip(other).map{|a,b|(a-b)**2}.inject{|a,b|a+b}**0.5
10
+ end
11
+ # calculates the angle formed by this point and two others
12
+ # in radians
13
+ def angle_to(a,b)
14
+ a,b = *[a,b].map(&:to_np)
15
+ d0,d1 = dist_from(a), a.dist_from(b)
16
+ z = self.to(a,1+d1/d0)
17
+ dz = z.dist_from(b)
18
+ Math.asin(dz/2/d1)*2
19
+ end
20
+
21
+ # calculates the new point on the vector of self->other, scaled by `t`.
22
+ # t=0 returns `self`, t=.5 is the midpoint, t=1 is `other`.
23
+ def to(other, t)
24
+ self.zip(other).map{|a,b|t*(b-a)+a}.to_np
25
+ end
26
+
27
+ # convert to NPoint; returns self
28
+ def to_np
29
+ self
30
+ end
31
+
32
+ # get the x value
33
+ def x() self[0]; end
34
+ # get the y value
35
+ def y() self[1]; end
36
+ # get the z value
37
+ def z() self[2]; end
38
+ end
39
+
40
+
41
+ class Array
42
+ # add n-point functions to a copy of this array
43
+ def to_np
44
+ dup.extend NPoint
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ # bezier_curve/version.rb
2
+ # Just the version information for this library
3
+
4
+ class BezierCurve
5
+ VERSION = "0.8.0"
6
+ RELEASE_DATE = "2015-06-18"
7
+ end
@@ -0,0 +1,151 @@
1
+ # bezier_curve.rb
2
+ # A bézier curve library for Ruby, supporting n-dimensional,
3
+ # nth-degree curves.
4
+
5
+ require 'bezier_curve/version'
6
+ require 'bezier_curve/n_point'
7
+
8
+ # A bezier curve. Usage:
9
+ # c = BezierCurve.new([0,0], [0,1], [1,1])
10
+ # c.first #=> [0,0]
11
+ # c.last #=> [1,1]
12
+ # c[0.375] #=> [t=.375]
13
+ # c.points #=> an array of points that make a fairly smooth curve
14
+ # c.points(tolerance:Math::PI/20)
15
+ # #=> returns points with given tolerance for smoothness
16
+ # c.points(count: 10) #=> returns 10 points, for evenly spaced values of `t`
17
+ class BezierCurve
18
+ # create a new curve, from a list of points.
19
+ def initialize(*controls)
20
+ # check for argument errors
21
+ ZeroDimensionError.check! controls
22
+ DifferingDimensionError.check! controls
23
+ InsufficientPointsError.check! controls
24
+
25
+ @controls = controls.map(&:to_np)
26
+ end
27
+
28
+ attr_reader :controls
29
+
30
+ # the first control point
31
+ def first() controls.first; end
32
+ alias_method :start, :first
33
+ # the last control point
34
+ def last() controls.last; end
35
+ alias_method :end, :last
36
+
37
+ # the degree of the curve
38
+ def degree
39
+ controls.size - 1
40
+ end
41
+ alias_method :order, :degree
42
+ # the number of dimensions given
43
+ def dimensions
44
+ controls[0].size
45
+ end
46
+
47
+ # find the point for a given value of `t`.
48
+ def index(t)
49
+ pts = controls
50
+ while pts.size > 1
51
+ pts = (0..pts.size-2).map do |i|
52
+ pts[i].zip(pts[i+1]).map{|a,b| t*(b-a)+a}
53
+ end
54
+ end
55
+ pts[0].to_np
56
+ end
57
+ alias_method :[], :index
58
+
59
+ # divide this bezier curve into two curves, at the given `t`
60
+ def split_at(t)
61
+ pts = controls
62
+ a,b = [pts.first],[pts.last]
63
+ while pts.size > 1
64
+ pts = (0..pts.size-2).map do |i|
65
+ pts[i].zip(pts[i+1]).map{|a,b| t*(b-a)+a}
66
+ end
67
+ a<<pts.first
68
+ b<<pts.last
69
+ end
70
+ [BezierCurve.new(*a), BezierCurve.new(*b.reverse)]
71
+ end
72
+
73
+
74
+ # Returns a list of points on this curve. If you specify `count`,
75
+ # returns that many points, evenly spread over values of `t`.
76
+ # If you specify `tolerance`, no adjoining line segments will
77
+ # deviate from 180 by an angle of more than the value given (in
78
+ # radians). If unspecified, defaults to `tolerance: 1/64pi` (~3 deg)
79
+ def points(count:nil, tolerance:Math::PI/64)
80
+ if count
81
+ (0...count).map{|i| index i/(count-1.0)}
82
+ else
83
+ lines = subdivide(tolerance)
84
+ lines.map{|seg|seg.first} + [lines.last.last]
85
+ end
86
+ end
87
+
88
+ # recursively subdivides the curve until each is straight within the
89
+ # given tolerance value, in radians
90
+ def subdivide(tolerance)
91
+ if is_straight?(tolerance)
92
+ [self]
93
+ else
94
+ a,b = split_at(0.5)
95
+ a.subdivide(tolerance) + b.subdivide(tolerance)
96
+ end
97
+ end
98
+
99
+
100
+ # test this curve to see of it can be considered straight, optionally
101
+ # within the given angular tolerance, in radians
102
+ def is_straight?(tolerance)
103
+ # sanity check for tolerance in radians
104
+ if first.angle_to(index(0.5), last) <= tolerance
105
+ # maximum wavyness is `degree` - 1; split at `degree` points
106
+ pts = points(count:degree)
107
+ # size-3, because we ignore the last 2 points as starting points;
108
+ # check all angles against `tolerance`
109
+ (0..pts.size-3).all? do |i|
110
+ pts[i].angle_to(pts[i+1], pts[i+2]) < tolerance
111
+ end
112
+ end
113
+ end
114
+
115
+ # Indicates an error where the control points are in zero dimensions.
116
+ # Sounds silly, but you never know when software is generating the
117
+ # points.
118
+ class ZeroDimensionError < ArgumentError
119
+ def initialize
120
+ super "Points given must have at least one dimension"
121
+ end
122
+ def self.check! pointset
123
+ raise new.tap{|e|e.backtrace.shift} if
124
+ pointset[0].size == 0
125
+ end
126
+ end
127
+
128
+ # Indicates that the points do not all have the same number of
129
+ # dimensions, which makes them impossible to use.
130
+ class DifferingDimensionError < ArgumentError
131
+ def initialize
132
+ super "All points must have the same number of dimensions"
133
+ end
134
+ def self.check! pointset
135
+ raise new.tap{|e|e.backtrace.shift} if
136
+ pointset[1..-1].any?{|pt| pointset[0].size != pt.size}
137
+ end
138
+ end
139
+
140
+ # Indicates that there aren't enough control points; minimum of two
141
+ # for a first-degree bezier.
142
+ class InsufficientPointsError < ArgumentError
143
+ def initialize
144
+ super "All points must have the same number of dimensions"
145
+ end
146
+ def self.check! pointset
147
+ raise new.tap{|e|e.backtrace.shift} if
148
+ pointset.size <= 1
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,104 @@
1
+ # test functionality of BezierCurve class
2
+ require 'test/unit'
3
+ require 'bezier_curve'
4
+
5
+ class TestBezierCurve < Test::Unit::TestCase
6
+ # shorthand to create a bezier curve
7
+ def bc(*args)
8
+ BezierCurve.new(*args)
9
+ end
10
+
11
+ def test_degree
12
+ assert_equal 2, bc([0,0],[0,1],[1,1]).degree
13
+ assert_equal 3, bc([0,0],[0,1],[1,0],[1,1]).degree
14
+ end
15
+
16
+ def test_dimensions
17
+ assert_equal 2, bc([0,0],[0,1],[1,1]).dimensions
18
+ assert_equal 3, bc([0,0,0],[0,1,1],[1,1,2]).dimensions
19
+ end
20
+
21
+ def test_index
22
+ # simple curves that cross the origin
23
+ # degree 1
24
+ assert_equal [0,0], bc([1,1],[-1,-1])[0.5]
25
+ assert_equal [0,0,0], bc([1,1,1],[-1,-1,-1])[0.5]
26
+ assert_equal [0,0,0,0], bc([1,1,1,1],[-1,-1,-1,-1])[0.5]
27
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[-1,-1,-1,-1,-1])[0.5]
28
+ # degree 2 (quadratic)
29
+ assert_equal [0,0], bc([1,1],[0,-1],[-1,1])[0.5]
30
+ assert_equal [0,0,0], bc([1,1,1],[0,0,-1],[-1,-1,1])[0.5]
31
+ assert_equal [0,0,0,0], bc([1,1,1,1],[0,0,0,-1],[-1,-1,-1,1])[0.5]
32
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[0,0,0,0,-1],[-1,-1,-1,-1,1])[0.5]
33
+ # degree 3 (cubic)
34
+ assert_equal [0,0], bc([1,1],[-1,1],[1,-1],[-1,-1])[0.5]
35
+ assert_equal [0,0,0], bc([1,1,1],[-1,1,1],[1,-1,-1],[-1,-1,-1])[0.5]
36
+ assert_equal [0,0,0,0], bc([1,1,1,1],[-1,1,1,1],[1,-1,-1,-1],[-1,-1,-1,-1])[0.5]
37
+ assert_equal [0,0,0,0,0], bc([1,1,1,1,1],[-1,1,1,1,1],[1,-1,-1,-1,-1],[-1,-1,-1,-1,-1])[0.5]
38
+ end
39
+ def test_split_at
40
+ # not going to test specific points, just that the resulting curves are the same
41
+ # degree 1
42
+ c = bc([23,42],[13,7])
43
+ _c1, _c2 = *c.split_at(0.25)
44
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
45
+ # degree 2
46
+ c = bc([23,42],[13,7])
47
+ _c1, _c2 = *c.split_at(0.25)
48
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
49
+ # degree 3
50
+ c = bc([23,42,],[13,7],[12,54])
51
+ _c1, _c2 = *c.split_at(0.25)
52
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
53
+ # degree 4
54
+ c = bc([23,42],[13,7],[12,54],[103,144])
55
+ _c1, _c2 = *c.split_at(0.25)
56
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
57
+ # degree 5
58
+ c = bc([23,42],[13,7],[12,54],[103,144],[512,360])
59
+ _c1, _c2 = *c.split_at(0.25)
60
+ assert_equal c.points(count:9), _c1.points(count:3)[0,2] + _c2.points(count:7)
61
+ end
62
+ # does this really need testing? I really hope not. Oh well.
63
+ def test_points_count
64
+ # 1d,o1
65
+ [2,3,5,8,13,23].each do |n|
66
+ assert_equal n, bc([0,0],[1,1]).points(count:n).count
67
+ end
68
+ # 2d,o2
69
+ [2,3,5,8,13,23].each do |n|
70
+ assert_equal n, bc([0,0,0],[1,1,1],[2,2,2]).points(count:n).count
71
+ end
72
+ # 3d,o3
73
+ [2,3,5,8,13,23].each do |n|
74
+ assert_equal n, bc(*4.times.map{|n|Array.new(4).fill(n)}).points(count:n).count
75
+ end
76
+ # 4d,o4
77
+ [2,3,5,8,13,23].each do |n|
78
+ assert_equal n, bc(*5.times.map{|n|Array.new(5).fill(n)}).points(count:n).count
79
+ end
80
+ # 5d,o5
81
+ [2,3,5,8,13,23].each do |n|
82
+ assert_equal n, bc(*6.times.map{|n|Array.new(6).fill(n)}).points(count:n).count
83
+ end
84
+ end
85
+
86
+ def test_points_tolerance
87
+ pi = Math::PI
88
+ # just make sure all points are under tolerance
89
+ # 2d/o
90
+ p = bc([1,0],[3,2],[7,9]).points(tolerance:pi/53)
91
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
92
+ # 3d/o
93
+ p = bc([1,0,2],[3,2,5],[7,9,8],[13,17,12]).points(tolerance:pi/53)
94
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
95
+ # 4d/o
96
+ p = bc([1,0,2,-1],[3,2,5,4],[7,9,8,6],[13,17,12,15],[20,21,32,25]).points(tolerance:pi/53)
97
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
98
+ # 5d/o
99
+ p = bc([1,0,2,-1,3],[3,2,5,4,6],[7,9,8,6,5],[13,17,12,15,14],[20,21,32,25,23],[34,33,47,39,52]).points(tolerance:pi/53)
100
+ assert_true (0..p.count-3).all?{|i|p[i].angle_to(p[i+1],p[i+2]).abs < pi/53}
101
+ end
102
+
103
+ end
104
+
@@ -0,0 +1,51 @@
1
+ # test functionality of NPoint module
2
+ require 'test/unit'
3
+ require 'bezier_curve/n_point'
4
+
5
+ class TestNPoint < Test::Unit::TestCase
6
+ def test_to_np
7
+ np = [9,8,7].to_np
8
+
9
+ assert_kind_of NPoint, np, "Array#to_np should return an NPoint"
10
+ assert_same np.to_np, np, "NPoint#to_np should return self"
11
+ end
12
+ def test_x
13
+ np = [9,8,7].to_np
14
+ assert_equal 9, np.x
15
+ end
16
+ def test_y
17
+ np = [9,8,7].to_np
18
+ assert_equal 8, np.y
19
+ end
20
+ def test_z
21
+ np = [9,8,7].to_np
22
+ assert_equal 7, np.z
23
+ end
24
+ def test_to
25
+ a, b = [1,1,1].to_np, [5,5,5].to_np
26
+
27
+ assert_equal [3,3,3], a.to(b,0.5)
28
+ assert_equal [-1,-1,-1], a.to(b,-0.5)
29
+ assert_equal [7,7,7], a.to(b,1.5)
30
+ end
31
+ def test_dist_from
32
+ # various numbers of dimensions
33
+ assert_equal 3, [3].to_np.dist_from([6])
34
+ assert_equal (3**2*2)**0.5, [3,3].to_np.dist_from([6,6])
35
+ assert_equal (3**2*3)**0.5, [3,3,3].to_np.dist_from([6,6,6])
36
+ assert_equal (3**2*4)**0.5, [3,3,3,3].to_np.dist_from([6,6,6,6])
37
+ assert_equal (3**2*5)**0.5, [3,3,3,3,3].to_np.dist_from([6,6,6,6,6])
38
+
39
+ # across the origin
40
+ assert_in_delta 8**0.5, [1,1].to_np.dist_from([-1,-1]), 0.00001
41
+ end
42
+ def test_angle_to
43
+ pi = Math::PI
44
+ pi_eps = (pi.next_float-pi)*16
45
+ assert_in_epsilon pi/2, [1,0].to_np.angle_to([0,0],[0,1]), pi_eps
46
+ assert_in_epsilon pi, [1,1].to_np.angle_to([0,0],[1,1]), pi_eps
47
+ assert_in_epsilon 0, [1,1].to_np.angle_to([0,0],[-1,-1]), pi_eps
48
+ assert_in_epsilon pi/4, [-1,0].to_np.angle_to([0,0],[1,1]), pi_eps
49
+ end
50
+ end
51
+
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bezier_curve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Mark Hubbart
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A bézier curve library for Ruby, supporting n-dimensional, nth-degree
14
+ curves
15
+ email: mark.hubbart@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - lib/bezier_curve.rb
23
+ - lib/bezier_curve/n_point.rb
24
+ - lib/bezier_curve/version.rb
25
+ - test/test_bezier_curve.rb
26
+ - test/test_n_point.rb
27
+ homepage: https://github.com/marcuserronius/bezier_curve
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.4.5
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: N-dimensional, nth-degree bézier curves
51
+ test_files: []