dem-curves 0.0.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.
- checksums.yaml +7 -0
- data/lib/core/constraint.rb +79 -0
- data/lib/core/control-point.rb +106 -0
- data/lib/core/path-element.rb +88 -0
- data/lib/core/path.rb +87 -0
- data/lib/core/util.rb +26 -0
- data/lib/dem-curves.rb +19 -0
- data/lib/rubygame-util/control-handles.rb +76 -0
- data/lib/rubygame-util/gfx.rb +15 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 95c1fab002f919736b30a60e299e82c05a10f1fb
|
4
|
+
data.tar.gz: 9a9a364baf601ea4c82d302b38cb2fb7b62fa543
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ce2ca54589e1a097467d565a315383b3269a3cee37f7dbfc8c6e12514ae9ebf38a909721ce7de4c797896f8e16f2b0ea49034bcec57efd2eb7d3def32ffb6757
|
7
|
+
data.tar.gz: f78075a71096ba4ffc79600ed2ddc28a2667a5d234b86923a85b69295ade8c878823960109b19f07f3846347fabf402e7d07a47dfe15bda70901b31039eba1ba
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module DemCurves
|
2
|
+
module BaseConstraint
|
3
|
+
master_point = nil
|
4
|
+
slave_points = []
|
5
|
+
|
6
|
+
def notify(src, orig_src, params)
|
7
|
+
if src == @master_point
|
8
|
+
handle_master src, orig_src, params
|
9
|
+
elsif @slave_points.include? src
|
10
|
+
handle_slave src, orig_src, params
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_master(master, orig_src, params)
|
15
|
+
# This has to be implemented by the class
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_slave(slave, orig_src, params)
|
19
|
+
# This has to be implemented by the class
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class LineUpConstraint
|
25
|
+
include BaseConstraint
|
26
|
+
def initialize(pivot, p0, p1, mirror_distance=false, follow=:pivot)
|
27
|
+
pivot.add_constraint self
|
28
|
+
p0.add_constraint self
|
29
|
+
p1.add_constraint self
|
30
|
+
|
31
|
+
@master_point = pivot
|
32
|
+
@slave_points = [p0, p1]
|
33
|
+
@mirror_distance = mirror_distance
|
34
|
+
@follow = follow
|
35
|
+
p1.move_to p1.loc #hacky way to trigger readjustment
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_master(master, orig_src, params)
|
39
|
+
if (params.include? :new_pos and params.include? :old_pos) or params.include? :rel
|
40
|
+
case @follow
|
41
|
+
when :pivot
|
42
|
+
unless params.include? :rel
|
43
|
+
params[:rel] = params[:new_pos], params[:old_pos]
|
44
|
+
end
|
45
|
+
|
46
|
+
params.delete(:new_pos)
|
47
|
+
params.delete(:old_pos)
|
48
|
+
|
49
|
+
@slave_points.each do |slave|
|
50
|
+
slave.notify_to_move orig_src, self, params
|
51
|
+
end
|
52
|
+
when :p0
|
53
|
+
direction = (params[:new_pos] - @slave_points[0].loc).unit
|
54
|
+
distance = (params[:new_pos] - @slave_points[1].loc).r
|
55
|
+
@slave_points[1].notify_to_move orig_src, self, {:new_pos => params[:new_pos] + (distance * direction)}
|
56
|
+
when :p1
|
57
|
+
direction = (params[:new_pos] - @slave_points[1].loc).unit
|
58
|
+
distance = (params[:new_pos] - @slave_points[0].loc).r
|
59
|
+
@slave_points[0].notify_to_move orig_src, self, {:new_pos => params[:new_pos] + (distance * direction)}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_slave(slave, orig_src, params)
|
65
|
+
if params.include? :new_pos
|
66
|
+
other_slave = (@slave_points.select {|s| s!=slave})[0]
|
67
|
+
direction = (params[:new_pos] - @master_point.loc).unit
|
68
|
+
if @mirror_distance
|
69
|
+
distance = (slave.loc - @master_point.loc).r
|
70
|
+
else
|
71
|
+
distance = (other_slave.loc - @master_point.loc).r
|
72
|
+
end
|
73
|
+
|
74
|
+
new_loc = @master_point.loc + (-1 * direction * distance)
|
75
|
+
other_slave.notify_to_move orig_src, self, {:new_pos => new_loc}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
require 'core/util.rb'
|
3
|
+
|
4
|
+
module DemCurves
|
5
|
+
class ControlPoint
|
6
|
+
# this class is necessary to let other curves and path elements modify
|
7
|
+
# points, it works much better than calling "notify movement" functions
|
8
|
+
# every time a control point moves, this comes in handy when you want a curve
|
9
|
+
# to use another curve's end point as a starting point.
|
10
|
+
attr_reader :loc
|
11
|
+
|
12
|
+
def initialize(loc)
|
13
|
+
@loc = Vector.elements(loc)
|
14
|
+
@path_elements = []
|
15
|
+
@constraints = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_path_element(path_element)
|
19
|
+
@path_elements << path_element
|
20
|
+
end
|
21
|
+
|
22
|
+
def replace(other)
|
23
|
+
old_pos = @loc
|
24
|
+
case other
|
25
|
+
when ControlPoint
|
26
|
+
@loc = Vector.elements(other.loc)
|
27
|
+
when Vector, Array
|
28
|
+
unless other.size == 2
|
29
|
+
raise "Wrong number of dimensions, must be [x, y]"
|
30
|
+
end
|
31
|
+
@loc = Vector.elements(other)
|
32
|
+
else
|
33
|
+
raise "Argument is instance of #{other.class}! Replacement argument must be an instance of Vector, Array or ControlPoint."
|
34
|
+
end
|
35
|
+
|
36
|
+
@path_elements.each do |path_element|
|
37
|
+
path_element.generate
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def shift(rel)
|
42
|
+
unless rel.size == 2
|
43
|
+
raise "Wrong number of dimensions, must be [x, y]"
|
44
|
+
end
|
45
|
+
|
46
|
+
old_pos = @loc
|
47
|
+
move_to @loc + Vector.elements(rel)
|
48
|
+
end
|
49
|
+
|
50
|
+
def move_to(destination)
|
51
|
+
old_pos = @loc
|
52
|
+
replace destination
|
53
|
+
|
54
|
+
new_pos = @loc
|
55
|
+
rel = new_pos - old_pos
|
56
|
+
|
57
|
+
@constraints.each do |constraint|
|
58
|
+
constraint.notify self, self, {:new_pos => new_pos, :old_pos => old_pos, :rel => rel}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def rotate_around(pivot_ctl, angle)
|
63
|
+
offset = @loc - pivot_ctl.loc
|
64
|
+
new_offset = Matrix[[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]] * offset
|
65
|
+
replace new_offset + pivot_ctl.loc
|
66
|
+
end
|
67
|
+
|
68
|
+
def notify_to_move(orig_src, src_constraint, params)
|
69
|
+
unless orig_src == self
|
70
|
+
# Safety measure, it avoids infinite recursion, but produces weird results
|
71
|
+
# with cyclic constraint structures.
|
72
|
+
old_pos = @loc
|
73
|
+
if params.include? :new_pos
|
74
|
+
replace params[:new_pos]
|
75
|
+
elsif params.include? :rel
|
76
|
+
replace params[:rel] + @loc
|
77
|
+
else
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
new_pos = @loc
|
82
|
+
rel = new_pos - old_pos
|
83
|
+
|
84
|
+
@constraints.each do |constraint|
|
85
|
+
unless constraint == src_constraint
|
86
|
+
constraint.notify self, orig_src, {:new_pos => new_pos, :old_pos => old_pos, :rel => rel}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_constraint(constraint)
|
93
|
+
unless @constraints.include? constraint
|
94
|
+
@constraints << constraint
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def clear_constraints
|
99
|
+
@constraints = []
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def ControlPoint.[](*loc)
|
104
|
+
return ControlPoint.new loc
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'core/util.rb'
|
2
|
+
|
3
|
+
module DemCurves
|
4
|
+
class PathElement
|
5
|
+
# Do not directly instantiate this class
|
6
|
+
attr_reader :path_points, :control_points
|
7
|
+
def initialize(control_points)
|
8
|
+
@control_points = control_points
|
9
|
+
@control_points.each_value do |control_point|
|
10
|
+
control_point.add_path_element self
|
11
|
+
end
|
12
|
+
|
13
|
+
@path_points = []
|
14
|
+
generate
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate
|
18
|
+
@path_points = @control_points.values.collect {|point| point.loc}
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_control(control_id, loc)
|
22
|
+
unless control_id.class == Symbol
|
23
|
+
raise 'control_id must be a symbol'
|
24
|
+
end
|
25
|
+
|
26
|
+
@control_points[control_id].move_to location
|
27
|
+
generate
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(control_id, loc)
|
31
|
+
set_control control_id, loc
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_control(control_id)
|
35
|
+
unless control_id.class == Symbol
|
36
|
+
raise 'control_id must be a symbol'
|
37
|
+
end
|
38
|
+
|
39
|
+
return @control_points[control_id]
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](control_id)
|
43
|
+
get_control control_id
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class CubicBezier < PathElement
|
49
|
+
def initialize(start_point, start_handle, end_handle, end_point)
|
50
|
+
super({
|
51
|
+
:start => start_point,
|
52
|
+
:start_handle => start_handle,
|
53
|
+
:end_handle => end_handle,
|
54
|
+
:end => end_point})
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate(t_freq=32)
|
58
|
+
step = 1.0 / t_freq
|
59
|
+
@path_points = (0..t_freq).collect {|i| interpolate(step * i)}
|
60
|
+
end
|
61
|
+
|
62
|
+
def interpolate(t)
|
63
|
+
# http://mathworld.wolfram.com/BezierCurve.html
|
64
|
+
(0..3).inject(Vector[0, 0]) do |mem, i|
|
65
|
+
mem += @control_points.values[i].loc.clone * bernstein_basis(i, 3, t)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_guides
|
70
|
+
# this will be removed soon.
|
71
|
+
return [[self[:start].loc, self[:start_handle].loc], [self[:end_handle].loc, self[:end].loc]]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
class Line < PathElement
|
77
|
+
def initialize(start_point, center_point, end_point)
|
78
|
+
super({
|
79
|
+
:start => start_point,
|
80
|
+
:center => center_point,
|
81
|
+
:end => end_point})
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate
|
85
|
+
@path_points = [self[:start].loc, self[:end].loc]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/core/path.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'core/path-element.rb'
|
2
|
+
|
3
|
+
module DemCurves
|
4
|
+
class Path
|
5
|
+
include Enumerable
|
6
|
+
attr_reader :path_points, :path_elements, :control_points
|
7
|
+
|
8
|
+
def initialize(point)
|
9
|
+
@path_elements = []
|
10
|
+
|
11
|
+
@start_point = ControlPoint[*point]
|
12
|
+
@end_point = @start_point
|
13
|
+
|
14
|
+
@path_points = to_a
|
15
|
+
@control_points = [@start_point]
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_bezier(start_handle, end_handle, end_point, tangent_lock=true)
|
19
|
+
new_bezier = CubicBezier.new(
|
20
|
+
@end_point,
|
21
|
+
ControlPoint[*start_handle],
|
22
|
+
ControlPoint[*end_handle],
|
23
|
+
ControlPoint[*end_point])
|
24
|
+
|
25
|
+
if tangent_lock and @path_elements.last
|
26
|
+
start_length = (new_bezier[:start_handle].loc - @end_point.loc).r
|
27
|
+
last_element = @path_elements.last
|
28
|
+
|
29
|
+
case last_element
|
30
|
+
when CubicBezier
|
31
|
+
LineUpConstraint.new @end_point, last_element[:end_handle], new_bezier[:start_handle]
|
32
|
+
when Line
|
33
|
+
LineUpConstraint.new @end_point, last_element[:center], new_bezier[:start_handle], morror_distance=false, follow=:p0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@end_point = new_bezier[:end]
|
38
|
+
@path_elements << new_bezier
|
39
|
+
|
40
|
+
@path_points = to_a
|
41
|
+
@control_points += new_bezier.control_points.values[1..-1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_line(end_point, tangent_lock=true)
|
45
|
+
center_point = ControlPoint[*(@end_point.loc + (Vector[*end_point] - @end_point.loc) * 0.5)]
|
46
|
+
new_line = Line.new @end_point, center_point, ControlPoint[*end_point]
|
47
|
+
LineUpConstraint.new center_point, new_line[:end], @end_point
|
48
|
+
|
49
|
+
if tangent_lock and @path_elements.last
|
50
|
+
last_element = @path_elements.last
|
51
|
+
|
52
|
+
case last_element
|
53
|
+
when CubicBezier
|
54
|
+
LineUpConstraint.new @end_point, last_element[:end_handle], new_line[:end], morror_distance=false, follow=:p1
|
55
|
+
when Line
|
56
|
+
@end_point.move_to end_point
|
57
|
+
return
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
@end_point = new_line[:end]
|
62
|
+
@path_elements << new_line
|
63
|
+
|
64
|
+
@path_points = to_a
|
65
|
+
@control_points += new_line.control_points.values[1..-1]
|
66
|
+
end
|
67
|
+
|
68
|
+
def each
|
69
|
+
yield @start_point.loc
|
70
|
+
@path_elements.each do |path_element|
|
71
|
+
(1..path_element.path_points.size-1).each do |index|
|
72
|
+
yield path_element.path_points[index]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def size
|
78
|
+
return @path_points.size
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_guides
|
82
|
+
@path_elements.inject([]) do |mem, element|
|
83
|
+
mem += element.get_guides
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/core/util.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Integer
|
2
|
+
def choose(k)
|
3
|
+
# binominal coefficient
|
4
|
+
return self.factorial / ((self -k).factorial * k.factorial)
|
5
|
+
end
|
6
|
+
|
7
|
+
def factorial()
|
8
|
+
# n!
|
9
|
+
return (1..self).inject(1, &:*)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def bernstein_basis(i, n, t)
|
14
|
+
# http://mathworld.wolfram.com/BernsteinPolynomial.html
|
15
|
+
return n.choose(i) * t ** i * (1 - t) ** (n - i)
|
16
|
+
end
|
17
|
+
|
18
|
+
class Vector
|
19
|
+
def unit
|
20
|
+
return self / self.r
|
21
|
+
end
|
22
|
+
|
23
|
+
def angle(other)
|
24
|
+
return Math.acos(self.inner_product other / (self.r * other.r))
|
25
|
+
end
|
26
|
+
end
|
data/lib/dem-curves.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# The backend files
|
2
|
+
require 'core/control-point'
|
3
|
+
require 'core/constraint'
|
4
|
+
require 'core/path-element'
|
5
|
+
require 'core/path'
|
6
|
+
require 'core/util'
|
7
|
+
|
8
|
+
# These are the utils for integration with rubygame
|
9
|
+
begin
|
10
|
+
require 'rubygame'
|
11
|
+
unless Rubygame::Surface.public_method_defined? :draw_line
|
12
|
+
raise LoadError, 'Loading the Rubygame utils for DemCurves requires SDL_GFX to be present on the system'
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'rubygame-util/control-handles'
|
16
|
+
require 'rubygame-util/gfx'
|
17
|
+
rescue LoadError => e
|
18
|
+
puts 'The Rubygame utils for DemCurves require SDL_GFX and Rubygame.'
|
19
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rubygame'
|
2
|
+
require 'matrix'
|
3
|
+
|
4
|
+
module DemCurves
|
5
|
+
module RubygameUtils
|
6
|
+
def self.populate_handles(ctl_points, drag_group)
|
7
|
+
ctl_points.each do |control_point|
|
8
|
+
new_handle = EditorHandle.new
|
9
|
+
new_handle.attach_to control_point
|
10
|
+
drag_group << new_handle
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DragGroup < Rubygame::Sprites::Group
|
15
|
+
dragged_object = nil
|
16
|
+
|
17
|
+
def on_press(evt)
|
18
|
+
self.each do |sprite|
|
19
|
+
if sprite.rect.collide_point? *evt.pos
|
20
|
+
@dragged_object = sprite
|
21
|
+
break
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_release(evt)
|
27
|
+
@dragged_object = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_move(evt)
|
31
|
+
if @dragged_object
|
32
|
+
@dragged_object.move evt.rel
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class EditorHandle
|
39
|
+
include Rubygame::Sprites::Sprite
|
40
|
+
|
41
|
+
def initialize(loc=[50, 50], size=10)
|
42
|
+
@groups =[]
|
43
|
+
@depth = 0
|
44
|
+
|
45
|
+
@rect = Rubygame::Rect.new 0, 0, size, size
|
46
|
+
@rect.c = loc
|
47
|
+
|
48
|
+
@image = Rubygame::Surface.new [size, size], 0, [Rubygame::HWSURFACE, Rubygame::SRCALPHA]
|
49
|
+
@image.fill([180, 180, 180])
|
50
|
+
@attached = false
|
51
|
+
|
52
|
+
@constraints = []
|
53
|
+
end
|
54
|
+
|
55
|
+
def attach_to(control_point)
|
56
|
+
unless @attached
|
57
|
+
@attached = true
|
58
|
+
@control_point = control_point
|
59
|
+
@rect.c = control_point.loc.to_a
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def move(rel)
|
64
|
+
if @attached
|
65
|
+
@control_point.shift rel
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def update
|
70
|
+
if @attached
|
71
|
+
@rect.c = @control_point.loc.to_a
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygame'
|
2
|
+
|
3
|
+
class Rubygame::Surface
|
4
|
+
def draw_path(path, color)
|
5
|
+
(0..path.size - 2).each do |index|
|
6
|
+
_draw_line path.to_a[index], path.to_a[index + 1], color, false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw_path_a(path, color)
|
11
|
+
(0..path.size - 2).each do |index|
|
12
|
+
_draw_line path.to_a[index], path.to_a[index + 1], color, true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dem-curves
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Huba Nagy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-14 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: 12huba@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/core/constraint.rb
|
20
|
+
- lib/core/control-point.rb
|
21
|
+
- lib/core/path-element.rb
|
22
|
+
- lib/core/path.rb
|
23
|
+
- lib/core/util.rb
|
24
|
+
- lib/dem-curves.rb
|
25
|
+
- lib/rubygame-util/control-handles.rb
|
26
|
+
- lib/rubygame-util/gfx.rb
|
27
|
+
homepage: https://github.com/huba/DemCurves
|
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: A library for generating bezier curve based paths from control_points. It
|
51
|
+
can be used with Rubygame
|
52
|
+
test_files: []
|