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,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