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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +2 -0
- data/eight_corner.gemspec +29 -0
- data/lib/eight_corner.rb +16 -0
- data/lib/eight_corner/base.rb +277 -0
- data/lib/eight_corner/bounds.rb +38 -0
- data/lib/eight_corner/figure.rb +18 -0
- data/lib/eight_corner/point.rb +64 -0
- data/lib/eight_corner/quadrant.rb +23 -0
- data/lib/eight_corner/string_mapper.rb +133 -0
- data/lib/eight_corner/svg_printer.rb +89 -0
- data/lib/eight_corner/version.rb +3 -0
- data/spec/lib/eight_corner/base_spec.rb +131 -0
- data/spec/lib/eight_corner/string_mapper_spec.rb +58 -0
- data/spec/spec_helper.rb +77 -0
- data/ted_staff_poster.rb +111 -0
- metadata +166 -0
@@ -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,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
|