eight_corner 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.
@@ -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