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
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+

|
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
|
data/Rakefile
ADDED
@@ -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
|
data/lib/eight_corner.rb
ADDED
@@ -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
|