eight_corner 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|