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
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
|
+
![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
|
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
|