eight_corner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ # A bounding box.
2
+ module EightCorner
3
+ class Bounds
4
+
5
+ # width
6
+ attr_accessor :x
7
+
8
+ # height
9
+ attr_accessor :y
10
+
11
+ def initialize(x=nil, y=nil)
12
+ @x = x
13
+ @y = y
14
+ end
15
+
16
+ def x_from_pct(percent)
17
+ @x * percent
18
+ end
19
+ def y_from_pct(percent)
20
+ @y * percent
21
+ end
22
+
23
+ def quadrant(point)
24
+ current = [
25
+ point.x < x/2 ? 0 : 1,
26
+ point.y < y/2 ? 0 : 1
27
+ ]
28
+
29
+ {
30
+ [0,0] => Quadrant::UPPER_LEFT,
31
+ [1,0] => Quadrant::UPPER_RIGHT,
32
+ [0,1] => Quadrant::LOWER_LEFT,
33
+ [1,1] => Quadrant::LOWER_RIGHT
34
+ }[current]
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module EightCorner
2
+
3
+ # a Figure is 8 connected points
4
+ class Figure
5
+
6
+ attr_accessor :points
7
+ def initialize
8
+ @points = []
9
+ end
10
+
11
+ # an overall potential based on the points in this figure
12
+ # for use as an input for another Base#plot
13
+ def potential
14
+ points.last.potential
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,64 @@
1
+ module EightCorner
2
+
3
+ # A point on a 2D plane.
4
+ class Point
5
+
6
+ attr_accessor :x, :y,
7
+ # distance from previous point to this one
8
+ :distance_from_last,
9
+ # the distance % used to build this distance
10
+ :distance_pct,
11
+ # angle from previous point to this one
12
+ :angle_from_last,
13
+ # the angle % used to build this angle
14
+ :angle_pct,
15
+ # the bounds object that the point exists in
16
+ :bounds,
17
+ # the potential value used to create this point
18
+ :created_by_potential
19
+
20
+ def initialize(x=nil, y=nil)
21
+ @x = x
22
+ @y = y
23
+ end
24
+
25
+ # which quadrant of the Bounds is this point in?
26
+ def quadrant
27
+ raise "cannot calculate quadrant. bounds is nil" if bounds.nil?
28
+ @quadrant ||= bounds.quadrant(self)
29
+ end
30
+
31
+ def angle_range
32
+ Quadrant.angle_range_for(quadrant)
33
+ end
34
+
35
+ # a single % value based on the data in this point
36
+ # used as an input for another Base#plot call
37
+ # TODO: what is distribution of values of this function?
38
+ # we want something reasonably gaussian, i think.
39
+ #
40
+ # w/o created_by_potential, rearranging the initial string in a series of figures
41
+ # alters the first 10 figures or so, and then they eventually converge back to
42
+ # being identical. (The ~11th figure shows no dependence on this initial change in ordering.)
43
+ def potential
44
+ (x/bounds.x.to_f + y/bounds.y.to_f + distance_pct.to_f + angle_pct.to_f + created_by_potential.to_f) % 1
45
+ end
46
+
47
+ def max
48
+ [@x,@y].max
49
+ end
50
+
51
+ def max_is
52
+ @x > @y ? :x : :y
53
+ end
54
+
55
+ def ==(other)
56
+ x == other.x && y == other.y
57
+ end
58
+
59
+ def valid?
60
+ x >= 0 && y >= 0 && x <= bounds.x && y <= bounds.y
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,23 @@
1
+ module EightCorner
2
+
3
+ # a Bounds has 4 quadrants.
4
+ # TODO: singleton instance for each quadrant?
5
+ # would allow each to return their own angle_range_for.
6
+ module Quadrant
7
+ UPPER_LEFT = 0
8
+ UPPER_RIGHT = 1
9
+ LOWER_RIGHT = 2
10
+ LOWER_LEFT = 3
11
+
12
+ def self.angle_range_for(quad)
13
+ # the valid range of angles (to the next point)
14
+ # based on the quadrant the current point is in.
15
+ {
16
+ UPPER_LEFT => 30..240,
17
+ UPPER_RIGHT => 120..330,
18
+ LOWER_LEFT => 300..(330+180),
19
+ LOWER_RIGHT => 210..(330+90)
20
+ }[quad]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,133 @@
1
+ module EightCorner
2
+
3
+ # StringMapper provides various methods for converting strings to
4
+ # percentage/potential values, which can then be mapped to (x,y) points.
5
+ class StringMapper
6
+
7
+ def initialize(options={})
8
+ defaults = {
9
+ group_count: 7,
10
+ min_group_size: 3
11
+ }
12
+ Base.validate_options!(options, defaults)
13
+ options = defaults.merge(options)
14
+
15
+ @group_count = options[:group_count]
16
+ @group_max_idx = @group_count - 1
17
+ @min_group_size = options[:min_group_size]
18
+
19
+ @frequencies = {"E"=>0.103202, "A"=>0.095238, "R"=>0.092638, "N"=>0.087925, "O"=>0.075898, "S"=>0.065659, "L"=>0.064196, "I"=>0.046481, "T"=>0.040793, "H"=>0.03868, "C"=>0.038193, "D"=>0.03413, "M"=>0.033317, "B"=>0.023891, "G"=>0.023566, "Y"=>0.022103, "U"=>0.021615, "W"=>0.019178, "K"=>0.016252, "P"=>0.015602, "F"=>0.011539, "V"=>0.010889, "Z"=>0.010076, "J"=>0.003738, " "=>0.00195, "X"=>0.00195, "Q"=>0.000975}
20
+ end
21
+
22
+ # return an array of 2-float arrays.
23
+ # provide method symbols for and percentizing method for constructing the 2 array elements.
24
+ def potentials(groups, percentizeA, percentizeB)
25
+ [percentizeA, percentizeB].each do |arg|
26
+ raise ArgumentError, "Invalid method #{arg}" if ! respond_to?(arg)
27
+ end
28
+
29
+ out = []
30
+ groups.each do |i|
31
+ # puts send(groupA, str, i)
32
+ out << [
33
+ send(percentizeA, i),
34
+ send(percentizeB, i),
35
+ ]
36
+ end
37
+
38
+ out
39
+ end
40
+
41
+ # split a string into groups, via :method
42
+ def groups(str, method)
43
+ raise ArgumentError, "Invalid method #{arg}" if ! respond_to?(method)
44
+
45
+ out = []
46
+ @group_count.times do |i|
47
+ out << send(method, str, i)
48
+ end
49
+ out
50
+ end
51
+
52
+ # sequential series of characters extracted from string.
53
+ # loops back to beginning for short strings
54
+ def group1(str, idx)
55
+ range = 0..@group_max_idx
56
+ return ArgumentError, "argument must be in #{range}" if ! range.include?(idx)
57
+
58
+ str_size = str.size
59
+ g_size = group_size str
60
+
61
+ out = ""
62
+
63
+ start_idx = (idx * g_size)
64
+ end_idx = start_idx + g_size
65
+
66
+ (start_idx...end_idx).each do |x|
67
+ out += str[x % str_size]
68
+ end
69
+
70
+ out
71
+ end
72
+
73
+ # builds a group from every nth character in the string.
74
+ def group2(str, idx)
75
+ str_size = str.size
76
+
77
+ out = ''
78
+ group_size(str).times do |i|
79
+ out += str[(i * @group_count + idx) % str_size]
80
+ end
81
+ out
82
+ end
83
+
84
+ # how many characters should be in each group?
85
+ def group_size(str)
86
+ [
87
+ str.size / @group_count,
88
+ @min_group_size
89
+ ].max
90
+ end
91
+
92
+ def percentize_modulus(str)
93
+ # i+memo just to add some order-dependency. "alex" != "xela"
94
+ (str.each_byte.inject(0){|memo,i| memo += i + memo; memo} % 100)/100.to_f
95
+ end
96
+
97
+ def percentize_modulus_exp(str)
98
+ (str.each_byte.inject(0){|memo,i| memo += i^2 + memo; memo} % 100)/100.to_f
99
+ end
100
+
101
+ # turn a string into a float 0..1
102
+ # a string with common letters should be near 0.5.
103
+ # a string with uncommon letters should be near 0 or 1.
104
+ def percentize_frequency(str)
105
+ # letters aren't evenly distributed.
106
+ # a string that has every letter once would add up to 1.
107
+
108
+ # totally common: sum would be E*3
109
+
110
+ common = @frequencies.first[1] * str.size
111
+
112
+ sum = 0
113
+ str.upcase.each_char {|c| sum += @frequencies[c].to_f }
114
+
115
+ distance = common - sum # distance from common.
116
+
117
+ # add or subtract from 0.5?
118
+ # all letters are positive/negative based on order in frequency distribution.
119
+ m = 1
120
+ @frequencies.keys.each do |i|
121
+ m *= -1
122
+ break if str[0].upcase == i
123
+ end
124
+
125
+ interp = Interpolate::Points.new({0=>0, common=>0.5})
126
+ pct_distance = interp.at(distance)
127
+ # interpolate (0 .. common) => (0 .. 0.5)
128
+ # multiply by m and add to 0.5
129
+
130
+ pct_distance * m + 0.5
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,89 @@
1
+ module EightCorner
2
+
3
+ # print a figure or collection of figures as an svg document
4
+ class SvgPrinter
5
+
6
+ def initialize(options={})
7
+ options[:incremental_colors] ||= false
8
+
9
+ @options = options
10
+ end
11
+
12
+ def svg(width, height)
13
+ out = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='#{width}' height='#{height}'>\n"
14
+ out += yield(self)
15
+ out += '</svg>'
16
+
17
+ out
18
+ end
19
+
20
+ def print(points)
21
+ svg do
22
+ @options[:incremental_colors] ? incremental_colors(points) : solid(points)
23
+ end
24
+ end
25
+
26
+ def draw(figure, options={})
27
+ defaults = {
28
+ x_offset: 0,
29
+ y_offset: 0,
30
+ width: 200,
31
+ height: 200,
32
+ show_border: false,
33
+ mark_initial_point: false,
34
+ label: nil,
35
+ method: :solid
36
+ }
37
+ Base.validate_options!(options, defaults)
38
+ options = defaults.merge(options)
39
+ raise ArgumentError, "invalid :method" if ! respond_to?(options[:method])
40
+
41
+ points = figure.points
42
+
43
+ out = "<g transform='translate(#{options[:x_offset]}, #{options[:y_offset]})'>"
44
+ if options[:show_border]
45
+ out += "<rect width='#{options[:width]}' height='#{options[:height]}' style='stroke:black; stroke-width:1; fill:none'></rect>"
46
+ end
47
+ out += send(options[:method], points)
48
+ if options[:mark_initial_point]
49
+ out += point(points[0].x, points[0].y, 5, '#ff0000')
50
+ end
51
+ if options[:label]
52
+ out += "<text x='5' y='#{options[:height]-5}' style='font-family: sans-serif'>#{options[:label]}</text>"
53
+ end
54
+
55
+ out += "</g>\n"
56
+ out
57
+ end
58
+
59
+ def solid(points)
60
+ out = '<polygon points="'
61
+ out += points.map{|p| "#{p.x},#{p.y}"}.join(' ')
62
+ out += '" style="fill:none; stroke:black; stroke-width:4"/>'
63
+ out
64
+ end
65
+
66
+ def incremental_colors(points, options={})
67
+ out = ''
68
+ interp = Interpolate::Points.new(1 => 0, (points.size-1) => 12)
69
+ 1.upto(points.size-1) do |i|
70
+ prev = points[i-1]
71
+ curr = points[i]
72
+
73
+ hex_str = interp.at(i).to_i.to_s(16) * 6
74
+ out += line(prev, curr, hex_str)
75
+ end
76
+ out += line(points.last, points.first, interp.at(points.size-1).to_i.to_s(16) * 6)
77
+ out
78
+ end
79
+
80
+ def line(from, to, color)
81
+ "<line x1='#{from.x}' y1='#{from.y}' x2='#{to.x}' y2='#{to.y}' style='stroke:##{color}; stroke-width:4'/>\n"
82
+ end
83
+
84
+ def point(x, y, r, color)
85
+ "<circle cx='#{x}' cy='#{y}' r='#{r}' fill='#{color}' stroke='none' />"
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module EightCorner
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+ require 'eight_corner'
3
+ include EightCorner
4
+
5
+ describe Base do
6
+ let(:subject) {Base.new(10,10)}
7
+
8
+ describe "next_point" do
9
+ it "should work" do
10
+ expect(
11
+ subject.next_point(Point.new(3,3), 45, 2.8)
12
+ ).to eq (Point.new(5,1))
13
+ end
14
+ end
15
+
16
+ describe "angle" do
17
+ it 'should always return an integer'
18
+ # D, [2014-08-09T16:09:53.857347 #12486] DEBUG -- : ["current", #<EightCorner::Point:0x007fcfd427d7f0 @x=10, @y=78>]
19
+ # D, [2014-08-09T16:09:53.857372 #12486] DEBUG -- : ["angle_to_next", 180.9]
20
+ # D, [2014-08-09T16:09:53.857394 #12486] DEBUG -- : ["distance_to_boundary", nil]
21
+ end
22
+
23
+ describe "distance_to_boundary" do
24
+
25
+ describe "for 0 degrees" do
26
+ it "should return x" do
27
+ expect(
28
+ subject.distance_to_boundary(Point.new(5,6), 0)
29
+ ).to eq(5)
30
+ end
31
+ end
32
+
33
+ describe "between 1 and 89 degrees" do
34
+ it "should return distance to top boundary when that is closest" do
35
+ expect(
36
+ subject.distance_to_boundary(Point.new(2,2), 45).round(4)
37
+ ).to eq(2.8284)
38
+ end
39
+ it "should return distance to right boundary when that is closest" do
40
+ expect(
41
+ subject.distance_to_boundary(Point.new(9,5), 45).round(4)
42
+ ).to eq(1.4142)
43
+ end
44
+ it "should return a value when hitting upper-right corner" do
45
+ expect(
46
+ subject.distance_to_boundary(Point.new(5,5), 45).round(4)
47
+ ).to eq(7.0711)
48
+ end
49
+ end
50
+
51
+ describe "for 90 degrees" do
52
+ it "should return distance to right boundary" do
53
+ expect(
54
+ subject.distance_to_boundary(Point.new(1,1), 90)
55
+ ).to eq(9)
56
+ end
57
+ end
58
+
59
+ describe "for 91 to 179 degrees" do
60
+ it "should return distance to right boundary when that is closest" do
61
+ expect(
62
+ subject.distance_to_boundary(Point.new(9,7), 135).round(4)
63
+ ).to eq(1.4142)
64
+ end
65
+ it "should return distance to bottom boundary when that is closest" do
66
+ expect(
67
+ subject.distance_to_boundary(Point.new(3,8), 135).round(4)
68
+ ).to eq(2.8284)
69
+ end
70
+ it "should return a value when hitting lower-right corner" do
71
+ expect(
72
+ subject.distance_to_boundary(Point.new(5,5), 135).round(4)
73
+ ).to eq(7.0711)
74
+ end
75
+ end
76
+
77
+ describe "for 180 degrees" do
78
+ it "should return distance to bottom boundary" do
79
+ expect(
80
+ subject.distance_to_boundary(Point.new(5,7), 180)
81
+ ).to eq(3)
82
+ end
83
+ end
84
+
85
+ describe "for 181 to 269 degrees" do
86
+ it "should return distance to bottom boundary when that is closest" do
87
+ expect(
88
+ subject.distance_to_boundary(Point.new(5,8), 225).round(4)
89
+ ).to eq(2.8284)
90
+ end
91
+ it "should return distance to left boundary when that is closest" do
92
+ expect(
93
+ subject.distance_to_boundary(Point.new(2,5), 225).round(4)
94
+ ).to eq(2.8284)
95
+ end
96
+ it "should return a value when hitting lower-left corner" do
97
+ expect(
98
+ subject.distance_to_boundary(Point.new(5,5), 225).round(4)
99
+ ).to eq(7.0711)
100
+ end
101
+ end
102
+
103
+ describe "for 270 degrees" do
104
+ it "should return distance to left boundary" do
105
+ expect(
106
+ subject.distance_to_boundary(Point.new(3,7), 270)
107
+ ).to eq(3)
108
+ end
109
+ end
110
+
111
+ describe "for 271 to 359 degrees" do
112
+ it "should return distance to left boundary when that is closest" do
113
+ expect(
114
+ subject.distance_to_boundary(Point.new(2,5), 315).round(4)
115
+ ).to eq(2.8284)
116
+ end
117
+ it "should return distance to top boundary when that is closest" do
118
+ expect(
119
+ subject.distance_to_boundary(Point.new(5,2), 315).round(4)
120
+ ).to eq(2.8284)
121
+ end
122
+ it "should return a value when hitting upper-left corner" do
123
+ expect(
124
+ subject.distance_to_boundary(Point.new(5,5), 315).round(4)
125
+ ).to eq(7.0711)
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ end