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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1e88b701005804c07000bdc9400d2ef5a3fb600a
4
+ data.tar.gz: 2d7c32b8ff1ac088239d7a77e88dce0fb43cb0be
5
+ SHA512:
6
+ metadata.gz: f2c979c1e10911131df2324b1bff85d8ce07c682dddb4dde0e0d9bf91015f57ce45f9c9953e8a0e7f0bf7daced26480af9e4e90a2364612bcd581de7b62a5e68
7
+ data.tar.gz: 140809eb7a083d01906e36f6d9d64033ce53a8181cde9dd16768b63aeefd26ccdf79f19815c55276f44ccaa33f98d5c06a1a462963cdff91f0b36a379ecad6f2
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in eight_corner.gemspec
4
+ gemspec
@@ -0,0 +1,18 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ $CELLULOID_DEBUG = false
5
+
6
+ # Note: The cmd option is now required due to the increasing number of ways
7
+ # rspec may be run, below are examples of the most common uses.
8
+ # * bundler: 'bundle exec rspec'
9
+ # * bundler binstubs: 'bin/rspec'
10
+ # * spring: 'bin/rsspec' (This will use spring if running and you have
11
+ # installed the spring binstubs per the docs)
12
+ # * zeus: 'zeus rspec' (requires the server to be started separetly)
13
+ # * 'just' rspec: 'rspec'
14
+ guard :rspec, cmd: 'bundle exec rspec' do
15
+ watch(%r{^spec/.+_spec\.rb$})
16
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
17
+ watch('spec/spec_helper.rb') { "spec" }
18
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Alex Dean
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # Eight Corner
2
+
3
+ Draw graphs inspired by [Georg Nees'](https://en.wikipedia.org/wiki/Georg_Nees) [8-corner graphics](http://betonbabe.tumblr.com/post/43724936282/georg-nees-8-corner-generative-computer).
4
+
5
+ ## Example
6
+
7
+ This code was used to create a poster representing the staff of TED Conferences
8
+ in 2014. There is a [blog post](https://www.deanspot.org/alex/2014/08/21/ted-eightcorner.html)
9
+ about this.
10
+
11
+ ![TED Staff Poster](https://www.deanspot.org/assets/eightcorner/ted_staff_poster.png)
12
+
13
+ ## Algorithm
14
+
15
+ 1. input is a string
16
+ 1. convert string into 8 2-element arrays. each element is a float
17
+ in the range 0..1.
18
+ 1. compute a starting point from the string
19
+ 1. for each point element, plot the next point using the 2 floats
20
+ 1. 1st is a direction from the current point
21
+ 1. 2nd is a distance from the current point
22
+ 1. multiple figures in a single body of text may influence each other,
23
+ meaning that ordering is significant.
24
+
25
+ ## Components
26
+
27
+ 1. Map a string into 8 elements and a starting point.
28
+ 1. Compute (x,y) points from 8 elements and a starting point.
29
+ 1. Plot points to generate final graphic.
30
+
31
+ ## Installation
32
+
33
+ Add this line to your application's Gemfile:
34
+
35
+ ```ruby
36
+ gem 'eight_corner'
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ $ bundle
42
+
43
+ Or install it yourself as:
44
+
45
+ $ gem install eight_corner
46
+
47
+ ## Usage
48
+
49
+ TODO: Write usage instructions here
50
+
51
+ ## Contributing
52
+
53
+ 1. Fork it ( https://github.com/alexdean/eight_corner/fork )
54
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
55
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
56
+ 4. Push to the branch (`git push origin my-new-feature`)
57
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eight_corner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "eight_corner"
8
+ spec.version = EightCorner::VERSION
9
+ spec.authors = ["Alex Dean"]
10
+ spec.email = ["alex@crackpot.org"]
11
+ spec.summary = %q{Library for generating abstract figures from text strings.}
12
+ spec.description = %q{Map text to graphic figures inspired by Georg Nees 'eight corner' project.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'interpolate'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0.0"
26
+ spec.add_development_dependency "guard", "~> 2.6.1"
27
+ spec.add_development_dependency "guard-rspec", "~> 4.3.1"
28
+ spec.add_development_dependency "ruby_gntp", "~> 0.3.4"
29
+ end
@@ -0,0 +1,16 @@
1
+ require 'logger'
2
+
3
+ require 'interpolate'
4
+
5
+ require "eight_corner/version"
6
+ require 'eight_corner/base'
7
+ require 'eight_corner/bounds'
8
+ require 'eight_corner/figure'
9
+ require 'eight_corner/quadrant'
10
+ require 'eight_corner/point'
11
+
12
+ require 'eight_corner/string_mapper'
13
+ require 'eight_corner/svg_printer'
14
+
15
+ module EightCorner
16
+ end
@@ -0,0 +1,277 @@
1
+ module EightCorner
2
+
3
+ # This class is a catch-all. Will be cleaned up, you know, sometime.
4
+ class Base
5
+ def self.validate_options!(options, defaults)
6
+ unknown_options = options.keys - defaults.keys
7
+ if unknown_options.size > 0
8
+ raise ArgumentError, "Unrecognized options: #{unknown_options.inspect}"
9
+ end
10
+ end
11
+
12
+ def initialize(x_extent, y_extent, options={})
13
+ defaults = {
14
+ logger: Logger.new('/dev/null')
15
+ }
16
+ self.class.validate_options!(options, defaults)
17
+
18
+ options = defaults.merge(options)
19
+
20
+ @bounds = Bounds.new(x_extent, y_extent)
21
+ @point_count = 8
22
+
23
+ @log = options[:logger]
24
+ # @figure_interdepencence = options[:figure_interdepencence]
25
+ end
26
+
27
+ def plot(str, options={})
28
+ defaults = {
29
+ group_method: :group2,
30
+ angle_method: :percentize_modulus_exp,
31
+ distance_method: :percentize_modulus,
32
+ start_method: :starting_point,
33
+ # will the initial_potential, and potentials generated from previous
34
+ # points in the same figure, be used to alter the angle to the next
35
+ # point?
36
+ point_interdependence: true,
37
+ # 0.5 is 'no change' see angle_potential_interp
38
+ initial_potential: 0.5
39
+ }
40
+ self.class.validate_options!(options, defaults)
41
+ options = defaults.merge(options)
42
+
43
+ mapper = StringMapper.new(group_count: @point_count-1)
44
+
45
+ # 7 2-element arrays. each value is a float 0..1.
46
+ # 1st: % applied to calculate an angle
47
+ # 2nd: % applied to calculate a distance
48
+ potentials = mapper.potentials(
49
+ mapper.groups(str, options[:group_method]),
50
+ options[:angle_method],
51
+ options[:distance_method]
52
+ )
53
+
54
+ # the figure we are drawing.
55
+ figure = Figure.new
56
+ # set starting point.
57
+ figure.points << send(options[:start_method], str)
58
+
59
+ # a potential is a value derived from the previous point in a figure
60
+ # these are used to modify the angle used to locate the next point in
61
+ # the figure. in this way, previous figures add influence
62
+ # which wouldn't be present if the figure were drawn on its own.
63
+ # - median potential (0.5) changes nothing.
64
+ # - extremely low potential (0.0) moves the angle 15% counter-clockwise
65
+ # - extremely high potential (1.0) moves the angle 15% clockwise
66
+ angle_potential_interp = Interpolate::Points.new(0.0 => -0.15, 0.5 => 0.0, 1.0 => 0.15)
67
+
68
+ # increase low distance potentials to encourage longer lines
69
+ # this is added to the raw distance potential determined by the string mapper.
70
+ # - a distance_pct of 0 will have 0.3 added to it.
71
+ # - a distance_pct of 0.5 or greater will have nothing added to it.
72
+ additional_distance_interp = Interpolate::Points.new(0.0 => 0.3, 0.5 => 0.0)
73
+
74
+ previous_potential = options[:initial_potential]
75
+
76
+ (@point_count - 1).times do |i|
77
+ current_point = figure.points[i]
78
+
79
+ # TODO encourage more open angles?
80
+ angle_pct = potentials[i][0]
81
+ distance_pct = potentials[i][1]
82
+
83
+ @log.debug(['angle_pct', angle_pct])
84
+
85
+ # if points can influence each other, apply potential from previous
86
+ # point to the angle-selection process.
87
+ if options[:point_interdependence]
88
+ angle_pct_adjustment = angle_potential_interp.at(previous_potential)
89
+ @log.debug(['angle_pct_adjustment', angle_pct_adjustment])
90
+
91
+ @log.debug(['pre-ajustment', angle_pct, angle(current_point, angle_pct)])
92
+ angle_pct += angle_pct_adjustment
93
+ @log.debug(['post-ajustment', angle_pct, angle(current_point, angle_pct)])
94
+ end
95
+
96
+ angle_to_next = angle(current_point, angle_pct)
97
+ dist_to_boundary = distance_to_boundary(current_point, angle_to_next)
98
+
99
+ @log.debug(['angle_to_next', angle_to_next])
100
+ @log.debug(['distance_to_boundary', dist_to_boundary])
101
+
102
+ # if we're too close to the edge, go the opposite direction.
103
+ # so we don't get trapped in a corner.
104
+ if dist_to_boundary <= 1
105
+ @log.debug('dist_to_boundary is close to border. adjust angle.')
106
+
107
+ angle_to_next += 180
108
+ angle_to_next %= 360
109
+ dist_to_boundary = distance_to_boundary(current_point, angle_to_next)
110
+
111
+ @log.debug(['after 180: angle_to_next', angle_to_next])
112
+ @log.debug(['after 180: distance_to_boundary', dist_to_boundary])
113
+ end
114
+
115
+ # how to encourage more space-filling?
116
+ # track how many points are in each quadrant.
117
+ # if current point is in the most-populated one, move to least-populated.
118
+ # if current point and previous point are too close together...
119
+ # if current point and last point are in different quadrants...
120
+
121
+
122
+ distance_pct += additional_distance_interp.at(distance_pct)
123
+
124
+ # longer lines fill space better
125
+ distance_pct = 0.3 if distance_pct < 0.3
126
+ # keep away from bounds.
127
+ distance_pct = 0.9 if distance_pct > 0.9
128
+
129
+ distance = dist_to_boundary * distance_pct
130
+
131
+ next_point = next_point(
132
+ current_point,
133
+ angle_to_next,
134
+ distance
135
+ )
136
+ next_point.angle_pct = angle_pct
137
+ next_point.distance_pct = distance_pct
138
+ next_point.created_by_potential = previous_potential
139
+
140
+ # TODO: how do we create invalid points?
141
+ # some bug in distance_to_boundary, most likely.
142
+ if ! next_point.valid?
143
+ if next_point.x < 0
144
+ next_point.x = 0
145
+ end
146
+ if next_point.y < 0
147
+ next_point.y = 0
148
+ end
149
+
150
+ @log.error "point produced invalid next. '#{str}' #{i}"
151
+ @log.error(['angle_to_next', angle_to_next])
152
+ @log.error(['distance_to_boundary', dist_to_boundary])
153
+ @log.error(['next_point', next_point])
154
+ end
155
+
156
+ figure.points << next_point
157
+ previous_potential = figure.points.last.potential
158
+ end
159
+
160
+ figure
161
+ end
162
+
163
+ # return a starting point for string
164
+ def starting_point(str)
165
+ mapper = StringMapper.new
166
+ raw_x_pct = mapper.percentize_modulus(str)
167
+ raw_y_pct = mapper.percentize_modulus_exp(str)
168
+
169
+ # mapper produces raw %'s 0..1.
170
+ # figures that start out very close to a border often get trapped and
171
+ # look strange, so we won't allow a starting point <30% or >70%.
172
+ interp = Interpolate::Points.new(0 => 0.2, 1 => 0.8)
173
+
174
+ x_pct = interp.at( raw_x_pct )
175
+ y_pct = interp.at( raw_y_pct )
176
+
177
+ Point.new(
178
+ (x_pct * @bounds.x).to_i,
179
+ (y_pct * @bounds.y).to_i
180
+ )
181
+ end
182
+
183
+ # pick an angle for the next point
184
+ # steer away from the corners by avoiding angles which tend toward the
185
+ # corner we are currently closest to.
186
+ #
187
+ # current Point
188
+ # x & y extents
189
+ # percent : how far along the arc should we go?
190
+ # as a float 0..1
191
+ # always counter-clockwise.
192
+ #
193
+ # return: an angle from current point.
194
+ def angle(current, percent)
195
+
196
+ range = Quadrant.angle_range_for(@bounds.quadrant(current))
197
+ interp = Interpolate::Points.new({
198
+ 0 => range.begin,
199
+ 1 => range.end
200
+ })
201
+
202
+ interp.at(percent).to_i % 360
203
+ end
204
+
205
+ # what is the distance from point to extent, along a line of degrees angle
206
+ def distance_to_boundary(point, degrees)
207
+ degrees %= 360
208
+
209
+ case degrees
210
+ when 0 then
211
+ point.x
212
+
213
+ when 1..89 then
214
+ to_top = aas(90-degrees, 90, point.y)
215
+ to_right = aas(degrees, 90, @bounds.x - point.x)
216
+ [to_top, to_right].min
217
+
218
+ when 90 then
219
+ @bounds.x - point.x
220
+
221
+ when 91..179 then
222
+ to_right = aas(180-degrees, 90, @bounds.x - point.x)
223
+ to_bottom = aas(90-180-degrees, 90, @bounds.y - point.y)
224
+ [to_right, to_bottom].min
225
+
226
+ when 180 then
227
+ @bounds.y - point.y
228
+
229
+ when 181..269 then
230
+ to_bottom = aas(90-degrees-180, 90, @bounds.y - point.y)
231
+ to_left = aas(degrees - 180, 90, point.x)
232
+ [to_bottom, to_left].min
233
+
234
+ when 270 then
235
+ point.x
236
+
237
+ when 271..359 then
238
+ to_left = aas(360-degrees, 90, point.x)
239
+ to_top = aas(90-360-degrees, 90, point.y)
240
+ [to_left, to_top].min
241
+
242
+ end
243
+ end
244
+
245
+ def next_point(last_point, angle, distance)
246
+ # geometry black magic here. still not positive exactly why this works.
247
+ # unit circle begins at 90 and goes counterclockwise.
248
+ # we want to start at 0 and go clockwise
249
+ # orientation of 0 degrees to coordinate space probably matters also.
250
+ theta = (180 - angle) % 360
251
+
252
+ point = Point.new
253
+ point.x = (Math.sin(deg2rad(theta)) * distance + last_point.x).round
254
+ point.y = (Math.cos(deg2rad(theta)) * distance + last_point.y).round
255
+ point.distance_from_last = distance
256
+ point.angle_from_last = angle
257
+ point.bounds = @bounds
258
+ point
259
+ end
260
+
261
+ def deg2rad(degrees)
262
+ degrees * Math::PI / 180
263
+ end
264
+
265
+ def rad2deg(radians)
266
+ radians * 180 / Math::PI
267
+ end
268
+
269
+ # angle, angle, side
270
+ # A / sin(a) == B / sin(b)
271
+ # return length of side_B
272
+ def aas(angle_a, angle_b, side_A)
273
+ side_A / Math.sin(deg2rad(angle_a)) * Math.sin(deg2rad(angle_b))
274
+ end
275
+
276
+ end
277
+ end