magic_cloud 0.0.3 → 0.0.5
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 +5 -5
- data/README.md +29 -24
- data/lib/magic_cloud/bit_matrix.rb +4 -3
- data/lib/magic_cloud/canvas.rb +18 -9
- data/lib/magic_cloud/cloud.rb +48 -36
- data/lib/magic_cloud/collision_board.rb +7 -4
- data/lib/magic_cloud/debug.rb +5 -4
- data/lib/magic_cloud/layouter/place.rb +5 -6
- data/lib/magic_cloud/layouter.rb +6 -5
- data/lib/magic_cloud/palettes.rb +6 -5
- data/lib/magic_cloud/rect.rb +8 -6
- data/lib/magic_cloud/shape.rb +2 -1
- data/lib/magic_cloud/spriter.rb +7 -6
- data/lib/magic_cloud/version.rb +2 -2
- data/lib/magic_cloud/word.rb +3 -2
- data/lib/magic_cloud.rb +1 -1
- data/magic_cloud.gemspec +3 -1
- metadata +33 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3f49646844ea8dff390b90306511424e14e24fb898a1c7ea3a48e1d0f282da62
|
4
|
+
data.tar.gz: b1e962c6fd8bb07cf7be250f24b189441976b881fe9393091be172ec8d670678
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e79e0950a3f0a8c72b5ba8d8cdaa22dedc825f50a269a286bd7245635bf749ca090f843e3bf64f3634e16a1fd9164b8651f1e8dc400a874d3d7b28684f56972
|
7
|
+
data.tar.gz: 24e3cb62f96a6107cd5ff01c23f963db0427449bc68458d839778a98875bb954e53b81c43917aa83ca1a25bc58b6837f2fe31daf3164bdb6ff248c5218220a3d
|
data/README.md
CHANGED
@@ -12,12 +12,16 @@ Usage
|
|
12
12
|
|
13
13
|
```ruby
|
14
14
|
words = [
|
15
|
-
[test, 50],
|
16
|
-
[me, 40],
|
17
|
-
[tenderly, 30],
|
15
|
+
['test', 50],
|
16
|
+
['me', 40],
|
17
|
+
['tenderly', 30],
|
18
18
|
# ....
|
19
19
|
]
|
20
20
|
cloud = MagicCloud::Cloud.new(words, rotate: :free, scale: :log)
|
21
|
+
|
22
|
+
# To save to file, if not redering it for a page
|
23
|
+
img = cloud.draw(960, 600) #default height/width
|
24
|
+
img.write('test.png')
|
21
25
|
```
|
22
26
|
|
23
27
|
Or from command-line:
|
@@ -37,16 +41,16 @@ Installation
|
|
37
41
|
gem install magic_cloud
|
38
42
|
```
|
39
43
|
|
40
|
-
rmagick is requirement, and it
|
44
|
+
rmagick is requirement, and it needs compilation, so you may expect
|
41
45
|
problems in non-compiler-friendly environment (Windows).
|
42
46
|
|
43
47
|
Origins
|
44
48
|
-------
|
45
49
|
|
46
|
-
At first, it was straightforward port of [d3.layout.cloud.js](https://github.com/jasondavies/d3-cloud)
|
50
|
+
At first, it was a straightforward port of [d3.layout.cloud.js](https://github.com/jasondavies/d3-cloud)
|
47
51
|
by Jason Davies, which, I assume, is an implementation of Wordle algorithm.
|
48
52
|
|
49
|
-
Then there was major
|
53
|
+
Then there was major refactoring, to make code correspond to Ruby
|
50
54
|
standards (and be understandable to poor dumb me).
|
51
55
|
|
52
56
|
Then collision algorithm was rewritten from scratch.
|
@@ -61,7 +65,7 @@ References:
|
|
61
65
|
Performance
|
62
66
|
-----------
|
63
67
|
|
64
|
-
It's reasonable for me. On my small Thinkpad E330, some 50-words cloud
|
68
|
+
It's reasonable for me. On my small Thinkpad E330, some 50-words cloud
|
65
69
|
image, size 700×500, are typically generated in <3sec. It's not that cool,
|
66
70
|
yet not too long for you to fell asleep.
|
67
71
|
|
@@ -69,17 +73,17 @@ The time of cloud making depends on words count, size of image
|
|
69
73
|
(it's faster to find place for all words on larger image) and used rotation
|
70
74
|
algorithm (vertical+horizontal words only is significantly faster - and,
|
71
75
|
on my opinion, better looking - than "cool" free-rotated-words cloud). It
|
72
|
-
even depends on font - dense font like Impact takes
|
73
|
-
|
76
|
+
even depends on font - dense font like Impact takes more time to lay
|
77
|
+
out than sparse Tahoma.
|
74
78
|
|
75
79
|
Major performance eater is perfect collision detection, which Wordle-like
|
76
|
-
cloud needs. MagicCloud for now uses really dumb
|
77
|
-
not-so-dumb optimizations. You can look into
|
78
|
-
`lib/magic_cloud/collision_board.rb` - everything can be optimized is
|
80
|
+
cloud needs. MagicCloud for now uses really dumb algorithm with some
|
81
|
+
not-so-dumb optimizations. You can look into
|
82
|
+
`lib/magic_cloud/collision_board.rb` - everything can be optimized is
|
79
83
|
there; especially in `CollisionBoard#collides?` method.
|
80
84
|
|
81
85
|
I assume, for example, that naive rewriting of code in there as a C
|
82
|
-
extension
|
86
|
+
extension would help significantly.
|
83
87
|
|
84
88
|
Another possible way is adding some smart tricks, which eliminate as much
|
85
89
|
of pixel-by-pixel comparisons as possible (some of already made are
|
@@ -88,18 +92,18 @@ criss-cross intersection check, and memoizing of last crossed sprite).
|
|
88
92
|
Memory effectiviness
|
89
93
|
--------------------
|
90
94
|
|
91
|
-
Basically: it's not.
|
95
|
+
Basically: it's not.
|
92
96
|
|
93
|
-
Plain Ruby arrays are used to represent collision bitmasks (each array
|
94
|
-
member stand for 1 bit), so, for example, 700×500 pixel cloud will requre
|
97
|
+
Plain Ruby arrays are used to represent collision bitmasks (each array
|
98
|
+
member stand for 1 bit), so, for example, 700×500 pixel cloud will requre
|
95
99
|
collision board size `700*500` (i.e. 350k array items only for board, and
|
96
100
|
slightly less for all sprites).
|
97
101
|
|
98
102
|
It should be wise to use some packing (considering each Ruby Fixmnum can
|
99
|
-
represent not one, but whole 32 bits). Unfortunately, all bit array
|
100
|
-
libraries I've tried are causing major slowdown of cloud computation.
|
101
|
-
With, say, 50 words we'll have literally millions of operation
|
102
|
-
`bitmask#[]` and `bitmask#[]=`, so, even methods
|
103
|
+
represent not one, but whole 32 bits). Unfortunately, all bit array
|
104
|
+
libraries I've tried are causing major slowdown of cloud computation.
|
105
|
+
With, say, 50 words we'll have literally millions of operation
|
106
|
+
`bitmask#[]` and `bitmask#[]=`, so, even methods
|
103
107
|
like `Fixnum#&` and `Fixnum#|` (typically used for bit array representation)
|
104
108
|
are causing significant overload.
|
105
109
|
|
@@ -111,10 +115,10 @@ cloud = MagicCloud.new(words, palette: palette, rotate: rotate)
|
|
111
115
|
```
|
112
116
|
|
113
117
|
* `:palette` (default is `:color20`):
|
114
|
-
* `:category10`, `:category20`, ... - from (
|
118
|
+
* `:category10`, `:category20`, ... - from [D3.js](https://github.com/d3/d3-scale/blob/master/README.md#category-scales)
|
115
119
|
* `[array, of, colors]` - each color should be hex color, or any other RMagick color string (See "Color names at http://www.imagemagick.org/RMagick/doc/imusage.html)
|
116
120
|
* any lambda, accepting `(word, index)` and returning color string
|
117
|
-
* any object, responding to `color(word, index)` - so, you can make color
|
121
|
+
* any object, responding to `color(word, index)` - so, you can make color
|
118
122
|
depend on tag text, not only on its number in tags list
|
119
123
|
* `:rotate` - rotation algorithm:
|
120
124
|
* `:square` (only horizontal and vertical words) - it's default
|
@@ -129,12 +133,13 @@ cloud = MagicCloud.new(words, palette: palette, rotate: rotate)
|
|
129
133
|
* `:linear` - linear scaling (default);
|
130
134
|
* `:log` - logarithmic scaling;
|
131
135
|
* `:sqrt` - square root scaling;
|
132
|
-
* `:font_family` (Impact is default).
|
136
|
+
* `:font_family` (Impact is default, ex. Arial, Helvetica, Futura).
|
137
|
+
* `:font` - Full path to custom font file. Overwrite `:font_family`.
|
133
138
|
|
134
139
|
Current state
|
135
140
|
-------------
|
136
141
|
|
137
|
-
This library is extracted from real-life project. It should be
|
142
|
+
This library is extracted from a real-life project. It should be
|
138
143
|
pretty stable (apart from bugs introduced during extraction and gemification).
|
139
144
|
|
140
145
|
What it really lacks for now, is thorough (or any) testing, and
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MagicCloud
|
3
4
|
# Dead simple 2-dimensional "bit matrix", storing 1s and 0s.
|
4
5
|
# Not memory effectife at all, but the fastest pure-Ruby solution
|
@@ -25,8 +26,8 @@ module MagicCloud
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def dump
|
28
|
-
(0...height).map{|y|
|
29
|
-
(0...width).map{|x| at(x, y) ? ' ' : 'x'}.join
|
29
|
+
(0...height).map { |y|
|
30
|
+
(0...width).map { |x| at(x, y) ? ' ' : 'x' }.join
|
30
31
|
}.join("\n")
|
31
32
|
end
|
32
33
|
end
|
data/lib/magic_cloud/canvas.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rmagick'
|
3
4
|
|
4
5
|
module MagicCloud
|
5
6
|
# Thin wrapper around RMagick, incapsulating ALL the real drawing.
|
@@ -9,16 +10,18 @@ module MagicCloud
|
|
9
10
|
class Canvas
|
10
11
|
def initialize(w, h, back = 'transparent')
|
11
12
|
@width, @height = w, h
|
12
|
-
@internal = Magick::Image.new(w, h){|i| i.background_color =
|
13
|
+
@internal = Magick::Image.new(w, h) { |i| i.background_color = back }
|
13
14
|
end
|
14
15
|
|
15
16
|
attr_reader :internal, :width, :height
|
16
17
|
|
17
18
|
RADIANS = Math::PI / 180
|
18
19
|
|
19
|
-
def draw_text(text, options = {})
|
20
|
+
def draw_text(text, options = {}) # rubocop:todo Metrics/AbcSize
|
21
|
+
return nil if text.empty?
|
22
|
+
|
20
23
|
draw = Magick::Draw.new # FIXME: is it necessary every time?
|
21
|
-
|
24
|
+
|
22
25
|
x = options.fetch(:x, 0)
|
23
26
|
y = options.fetch(:y, 0)
|
24
27
|
rotate = options.fetch(:rotate, 0)
|
@@ -47,16 +50,16 @@ module MagicCloud
|
|
47
50
|
@internal.export_pixels(x, y, w, h, 'RGBA')
|
48
51
|
end
|
49
52
|
|
50
|
-
# rubocop:disable TrivialAccessors
|
51
53
|
def render
|
52
54
|
@internal
|
53
55
|
end
|
54
|
-
# rubocop:enable TrivialAccessors
|
55
56
|
|
56
57
|
private
|
57
58
|
|
58
59
|
def set_text_options(draw, options)
|
59
60
|
draw.font_family = options[:font_family]
|
61
|
+
draw.font = options[:font] if options[:font]
|
62
|
+
|
60
63
|
draw.font_weight = Magick::NormalWeight
|
61
64
|
draw.font_style = Magick::NormalStyle
|
62
65
|
|
@@ -67,13 +70,13 @@ module MagicCloud
|
|
67
70
|
end
|
68
71
|
|
69
72
|
def _measure_text(draw, text, rotate)
|
70
|
-
metrics = draw.get_type_metrics(
|
73
|
+
metrics = draw.get_type_metrics(%("#{validate_text(text)}m"))
|
71
74
|
w, h = rotated_metrics(metrics.width, metrics.height, rotate)
|
72
75
|
|
73
76
|
Rect.new(0, 0, w, h)
|
74
77
|
end
|
75
78
|
|
76
|
-
def rotated_metrics(w, h, degrees)
|
79
|
+
def rotated_metrics(w, h, degrees) # rubocop:todo Metrics/AbcSize
|
77
80
|
radians = degrees * Math::PI / 180
|
78
81
|
|
79
82
|
# FIXME: not too clear, just straightforward from d3.cloud
|
@@ -89,5 +92,11 @@ module MagicCloud
|
|
89
92
|
|
90
93
|
[w, h]
|
91
94
|
end
|
95
|
+
|
96
|
+
def validate_text(text)
|
97
|
+
return text +=' ' if text[-1] == '%'
|
98
|
+
|
99
|
+
text
|
100
|
+
end
|
92
101
|
end
|
93
102
|
end
|
data/lib/magic_cloud/cloud.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
|
-
#
|
2
|
-
require_relative './rect'
|
3
|
-
require_relative './canvas'
|
4
|
-
require_relative './palettes'
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
require_relative '
|
3
|
+
require_relative 'rect'
|
4
|
+
require_relative 'canvas'
|
5
|
+
require_relative 'palettes'
|
7
6
|
|
8
|
-
require_relative '
|
9
|
-
require_relative './spriter'
|
7
|
+
require_relative 'word'
|
10
8
|
|
11
|
-
require_relative '
|
9
|
+
require_relative 'layouter'
|
10
|
+
require_relative 'spriter'
|
11
|
+
|
12
|
+
require_relative 'debug'
|
12
13
|
|
13
14
|
module MagicCloud
|
14
15
|
# Main word-cloud class. Takes words with sizes, returns image
|
15
|
-
class Cloud
|
16
|
+
class Cloud # rubocop:todo Metrics/ClassLength
|
16
17
|
def initialize(words, options = {})
|
17
18
|
@words = words.sort_by(&:last).reverse
|
18
19
|
@options = options
|
@@ -23,15 +24,21 @@ module MagicCloud
|
|
23
24
|
|
24
25
|
DEFAULT_FAMILY = 'Impact'
|
25
26
|
|
26
|
-
|
27
|
+
# rubocop:todo Metrics/MethodLength
|
28
|
+
def draw(width, height) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
|
27
29
|
# FIXME: do it in init, for specs would be happy
|
28
|
-
shapes = @words.each_with_index.map{|(word, size), i|
|
29
|
-
|
30
|
-
word,
|
30
|
+
shapes = @words.each_with_index.map { |(word, size), i|
|
31
|
+
word_options = {
|
31
32
|
font_family: @options[:font_family] || DEFAULT_FAMILY,
|
32
33
|
font_size: scaler.call(word, size, i),
|
33
34
|
color: palette.call(word, i),
|
34
35
|
rotate: rotator.call(word, i)
|
36
|
+
}
|
37
|
+
word_options[:font] = @options[:font] if @options[:font]
|
38
|
+
|
39
|
+
Word.new(
|
40
|
+
word,
|
41
|
+
word_options
|
35
42
|
)
|
36
43
|
}
|
37
44
|
|
@@ -44,10 +51,11 @@ module MagicCloud
|
|
44
51
|
visible = layouter.layout!(shapes)
|
45
52
|
|
46
53
|
canvas = Canvas.new(width, height, 'white')
|
47
|
-
visible.each{|sh| sh.draw(canvas)}
|
54
|
+
visible.each { |sh| sh.draw(canvas) }
|
48
55
|
|
49
56
|
canvas.render
|
50
57
|
end
|
58
|
+
# rubocop:enable Metrics/MethodLength
|
51
59
|
|
52
60
|
private
|
53
61
|
|
@@ -61,11 +69,11 @@ module MagicCloud
|
|
61
69
|
when Symbol
|
62
70
|
make_const_palette(source)
|
63
71
|
when Array
|
64
|
-
->(_, index){source[index % source.size]}
|
72
|
+
->(_, index) { source[index % source.size] }
|
65
73
|
when Proc
|
66
74
|
source
|
67
|
-
when ->(s){s.respond_to?(:color)}
|
68
|
-
->(word, index){source.color(word, index)}
|
75
|
+
when ->(s) { s.respond_to?(:color) }
|
76
|
+
->(word, index) { source.color(word, index) }
|
69
77
|
else
|
70
78
|
fail ArgumentError, "Unknown palette: #{source.inspect}"
|
71
79
|
end
|
@@ -75,29 +83,29 @@ module MagicCloud
|
|
75
83
|
palette = PALETTES[sym] or
|
76
84
|
fail(ArgumentError, "Unknown palette: #{sym.inspect}")
|
77
85
|
|
78
|
-
->(_, index){palette[index % palette.size]}
|
86
|
+
->(_, index) { palette[index % palette.size] }
|
79
87
|
end
|
80
88
|
|
81
89
|
def make_rotator(source)
|
82
90
|
case source
|
83
91
|
when :none
|
84
|
-
->(*){0}
|
92
|
+
->(*) { 0 }
|
85
93
|
when :square
|
86
|
-
->(*){
|
94
|
+
->(*) {
|
87
95
|
(rand * 2).to_i * 90
|
88
96
|
}
|
89
97
|
when :free
|
90
|
-
->(*){
|
98
|
+
->(*) {
|
91
99
|
(((rand * 6) - 3) * 30).round
|
92
100
|
}
|
93
101
|
when Array
|
94
|
-
->(*){
|
102
|
+
->(*) {
|
95
103
|
source.sample
|
96
104
|
}
|
97
105
|
when Proc
|
98
106
|
source
|
99
|
-
when ->(s){s.respond_to?(:rotate)}
|
100
|
-
->(word, index){source.rotate(word, index)}
|
107
|
+
when ->(s) { s.respond_to?(:rotate) }
|
108
|
+
->(word, index) { source.rotate(word, index) }
|
101
109
|
else
|
102
110
|
fail ArgumentError, "Unknown rotation algo: #{source.inspect}"
|
103
111
|
end
|
@@ -111,27 +119,31 @@ module MagicCloud
|
|
111
119
|
norm =
|
112
120
|
case algo
|
113
121
|
when :no
|
114
|
-
|
115
|
-
return ->(_word, size, _index){size}
|
122
|
+
return ->(_word, size, _index) { size }
|
116
123
|
when :linear
|
117
|
-
->(x){x}
|
124
|
+
->(x) { x }
|
118
125
|
when :log
|
119
|
-
->(x){Math.log(x) / Math.log(10)}
|
126
|
+
->(x) { Math.log(x) / Math.log(10) }
|
120
127
|
when :sqrt
|
121
|
-
->(x){Math.sqrt(x)}
|
128
|
+
->(x) { Math.sqrt(x) }
|
122
129
|
else
|
123
130
|
fail ArgumentError, "Unknown scaling algo: #{algo.inspect}"
|
124
131
|
end
|
125
|
-
|
132
|
+
|
126
133
|
smin = norm.call(words.map(&:last).min)
|
127
134
|
smax = norm.call(words.map(&:last).max)
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
(
|
133
|
-
|
135
|
+
|
136
|
+
if smin == smax
|
137
|
+
->(_word, _size, _index) { FONT_MIN } # Return the minimum font size if no scaling is needed
|
138
|
+
else
|
139
|
+
koeff = (FONT_MAX - FONT_MIN).to_f / (smax - smin)
|
140
|
+
->(_word, size, _index) {
|
141
|
+
ssize = norm.call(size)
|
142
|
+
((ssize - smin).to_f * koeff + FONT_MIN).to_i
|
143
|
+
}
|
144
|
+
end
|
134
145
|
end
|
146
|
+
|
135
147
|
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity,Metrics/AbcSize
|
136
148
|
end
|
137
149
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'bit_matrix'
|
3
4
|
|
4
5
|
module MagicCloud
|
5
6
|
# Pixel-by-pixel collision board
|
@@ -17,7 +18,7 @@ module MagicCloud
|
|
17
18
|
attr_reader :rects, :intersections_cache
|
18
19
|
|
19
20
|
def criss_cross_collision?(rect)
|
20
|
-
if rects.any?{|r| r.criss_cross?(rect)}
|
21
|
+
if rects.any? { |r| r.criss_cross?(rect) }
|
21
22
|
Debug.stats[:criss_cross] += 1
|
22
23
|
true
|
23
24
|
else
|
@@ -25,6 +26,7 @@ module MagicCloud
|
|
25
26
|
end
|
26
27
|
end
|
27
28
|
|
29
|
+
# rubocop:disable Lint/HashCompareByIdentity -- probably should be followed, but don't have time to test it now
|
28
30
|
def collides_previous?(shape, intersections)
|
29
31
|
prev_idx = intersections_cache[shape.object_id]
|
30
32
|
|
@@ -52,6 +54,7 @@ module MagicCloud
|
|
52
54
|
|
53
55
|
false
|
54
56
|
end
|
57
|
+
# rubocop:enable Lint/HashCompareByIdentity
|
55
58
|
|
56
59
|
def collides?(shape)
|
57
60
|
Debug.stats[:collide_total] += 1
|
@@ -64,7 +67,7 @@ module MagicCloud
|
|
64
67
|
return true if criss_cross_collision?(shape.rect)
|
65
68
|
|
66
69
|
# then find which of placed sprites rectangles tag intersects
|
67
|
-
intersections = rects.map{|r| r.intersect(shape.rect)}
|
70
|
+
intersections = rects.map { |r| r.intersect(shape.rect) }
|
68
71
|
|
69
72
|
# no need to further check: this tag is not inside any others' rectangle
|
70
73
|
if intersections.compact.empty?
|
data/lib/magic_cloud/debug.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'forwardable'
|
3
4
|
require 'logger'
|
4
5
|
|
@@ -15,14 +16,14 @@ module MagicCloud
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def initialize
|
18
|
-
@logger = Logger.new(
|
19
|
-
@stats = Hash.new{|h, k| h[k] = 0}
|
19
|
+
@logger = Logger.new($stdout).tap { |l| l.level = Logger::FATAL }
|
20
|
+
@stats = Hash.new { |h, k| h[k] = 0 }
|
20
21
|
end
|
21
22
|
|
22
23
|
attr_reader :logger, :stats
|
23
24
|
|
24
25
|
def reset!
|
25
|
-
@stats = Hash.new{|h, k| h[k] = 0}
|
26
|
+
@stats = Hash.new { |h, k| h[k] = 0 }
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MagicCloud
|
3
4
|
class Layouter
|
4
5
|
class PlaceNotFound < RuntimeError
|
@@ -9,7 +10,7 @@ module MagicCloud
|
|
9
10
|
# 2. at each step, shift in spiral from the previous place
|
10
11
|
# 3. always knows, if the place "ready" for shape (empty and inside board)
|
11
12
|
class Place
|
12
|
-
def initialize(layouter, shape)
|
13
|
+
def initialize(layouter, shape) # rubocop:todo Metrics/AbcSize
|
13
14
|
@layouter, @shape = layouter, shape
|
14
15
|
|
15
16
|
# initial position
|
@@ -60,10 +61,9 @@ module MagicCloud
|
|
60
61
|
rectangular_spiral(step)
|
61
62
|
end
|
62
63
|
|
63
|
-
# rubocop:disable Metrics/AbcSize
|
64
64
|
def archimedean_spiral(size)
|
65
65
|
e = width / height
|
66
|
-
->(t){
|
66
|
+
->(t) {
|
67
67
|
t1 = t * size * 0.01
|
68
68
|
|
69
69
|
[
|
@@ -78,7 +78,7 @@ module MagicCloud
|
|
78
78
|
dx = dy * @layouter.width / @layouter.height
|
79
79
|
x = 0
|
80
80
|
y = 0
|
81
|
-
->(t){
|
81
|
+
->(t) {
|
82
82
|
sign = t < 0 ? -1 : 1
|
83
83
|
|
84
84
|
# zverok: this is original comment & code from d3.layout.cloud.js
|
@@ -95,7 +95,6 @@ module MagicCloud
|
|
95
95
|
[x, y].map(&:round)
|
96
96
|
}
|
97
97
|
end
|
98
|
-
# rubocop:enable Metrics/AbcSize
|
99
98
|
end
|
100
99
|
end
|
101
100
|
end
|
data/lib/magic_cloud/layouter.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'collision_board'
|
3
4
|
|
4
5
|
module MagicCloud
|
5
6
|
# Main magic of magic cloud - layouting shapes without collisions.
|
@@ -49,7 +50,7 @@ module MagicCloud
|
|
49
50
|
|
50
51
|
board.add(shape)
|
51
52
|
Debug.logger.info 'Place for %p found in %i steps (%.2f sec)' %
|
52
|
-
|
53
|
+
[shape, steps, Time.now-start]
|
53
54
|
|
54
55
|
break
|
55
56
|
end
|
@@ -57,11 +58,11 @@ module MagicCloud
|
|
57
58
|
true
|
58
59
|
rescue PlaceNotFound
|
59
60
|
Debug.logger.warn 'No place for %p found in %i steps (%.2f sec)' %
|
60
|
-
|
61
|
+
[shape, steps, Time.now-start]
|
61
62
|
|
62
63
|
false
|
63
64
|
end
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
|
-
require_relative '
|
68
|
+
require_relative 'layouter/place'
|
data/lib/magic_cloud/palettes.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MagicCloud
|
3
4
|
PALETTES = {
|
4
5
|
# Categorical colors from d3
|
5
|
-
# Source: https://github.com/
|
6
|
-
|
6
|
+
# Source: https://github.com/d3/d3-scale/blob/master/README.md#category-scales
|
7
|
+
|
7
8
|
category10: %w[
|
8
9
|
#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd
|
9
10
|
#8c564b #e377c2 #7f7f7f #bcbd22 #17becf
|
@@ -15,7 +16,7 @@ module MagicCloud
|
|
15
16
|
#8c564b #c49c94 #e377c2 #f7b6d2 #7f7f7f
|
16
17
|
#c7c7c7 #bcbd22 #dbdb8d #17becf #9edae5
|
17
18
|
],
|
18
|
-
|
19
|
+
|
19
20
|
category20b: %w[
|
20
21
|
#393b79 #5254a3 #6b6ecf #9c9ede #637939
|
21
22
|
#8ca252 #b5cf6b #cedb9c #8c6d31 #bd9e39
|
@@ -29,5 +30,5 @@ module MagicCloud
|
|
29
30
|
#a1d99b #c7e9c0 #756bb1 #9e9ac8 #bcbddc
|
30
31
|
#dadaeb #636363 #969696 #bdbdbd #d9d9d9
|
31
32
|
]
|
32
|
-
}
|
33
|
+
}.freeze
|
33
34
|
end
|
data/lib/magic_cloud/rect.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module MagicCloud
|
3
4
|
# Utility geometrical rectangle, implementing arithmetic interactions
|
4
5
|
# with other rectangles
|
@@ -9,6 +10,7 @@ module MagicCloud
|
|
9
10
|
end
|
10
11
|
|
11
12
|
attr_accessor :x0, :y0, :x1, :y1
|
13
|
+
|
12
14
|
# NB: we are trying to use instance variables instead of accessors
|
13
15
|
# inside this class methods, because they are called so many
|
14
16
|
# times that accessor overhead IS significant.
|
@@ -36,20 +38,18 @@ module MagicCloud
|
|
36
38
|
@y0 = y
|
37
39
|
end
|
38
40
|
|
39
|
-
# rubocop:disable Metrics/AbcSize
|
40
41
|
def adjust!(other)
|
41
42
|
@x0 = other.x0 if other.x0 < @x0
|
42
43
|
@y0 = other.y0 if other.y0 < @y0
|
43
44
|
@x1 = other.x1 if other.x1 > @x1
|
44
45
|
@y1 = other.y1 if other.y1 > @y1
|
45
46
|
end
|
46
|
-
# rubocop:enable Metrics/AbcSize
|
47
47
|
|
48
48
|
def adjust(other)
|
49
|
-
dup.tap{|d| d.adjust!(other)}
|
49
|
+
dup.tap { |d| d.adjust!(other) }
|
50
50
|
end
|
51
51
|
|
52
|
-
# rubocop:disable Metrics/
|
52
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
53
53
|
def criss_cross?(other)
|
54
54
|
# case 1: this one is horizontal:
|
55
55
|
# overlaps other by x, to right and left, and goes inside it by y
|
@@ -60,15 +60,17 @@ module MagicCloud
|
|
60
60
|
@y0 < other.y0 && @y1 > other.y1 &&
|
61
61
|
@x0 > other.x0 && @x1 < other.x1
|
62
62
|
end
|
63
|
-
# rubocop:enable Metrics/
|
63
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
64
64
|
|
65
65
|
def intersect(other)
|
66
66
|
# direct comparison is dirtier, yet significantly faster than
|
67
67
|
# something like [@x0, other.x0].max
|
68
|
+
# rubocop:disable Style/MinMaxComparison
|
68
69
|
ix0 = @x0 > other.x0 ? @x0 : other.x0
|
69
70
|
ix1 = @x1 < other.x1 ? @x1 : other.x1
|
70
71
|
iy0 = @y0 > other.y0 ? @y0 : other.y0
|
71
72
|
iy1 = @y1 < other.y1 ? @y1 : other.y1
|
73
|
+
# rubocop:enable Style/MinMaxComparison
|
72
74
|
|
73
75
|
if ix0 > ix1 || iy0 > iy1
|
74
76
|
nil # rectangles are not intersected, in fact
|
data/lib/magic_cloud/shape.rb
CHANGED
data/lib/magic_cloud/spriter.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'bit_matrix'
|
3
4
|
|
4
5
|
module MagicCloud
|
5
6
|
# Incapsulates sprite maker for any Shape, able to draw itself.
|
@@ -21,14 +22,14 @@ module MagicCloud
|
|
21
22
|
end
|
22
23
|
|
23
24
|
Debug.logger.info 'Sprites ready: %i sec, %i canvases' %
|
24
|
-
|
25
|
+
[Time.now - start, Debug.stats[:canvases]]
|
25
26
|
end
|
26
27
|
|
27
28
|
private
|
28
29
|
|
29
30
|
attr_reader :canvas, :cur_x, :cur_y, :row_height
|
30
31
|
|
31
|
-
def make_sprite(shape)
|
32
|
+
def make_sprite(shape) # rubocop:todo Metrics/AbcSize
|
32
33
|
rect = shape.measure(canvas)
|
33
34
|
ensure_position(rect)
|
34
35
|
|
@@ -42,10 +43,10 @@ module MagicCloud
|
|
42
43
|
shift_position(rect)
|
43
44
|
|
44
45
|
Debug.logger.debug 'Sprite for %p ready: %i×%i' %
|
45
|
-
|
46
|
+
[shape, shape.sprite.width, shape.sprite.height]
|
46
47
|
end
|
47
48
|
|
48
|
-
CANVAS_SIZE = [1024, 1024]
|
49
|
+
CANVAS_SIZE = [1024, 1024].freeze
|
49
50
|
|
50
51
|
def restart_canvas!
|
51
52
|
Debug.stats[:canvases] += 1
|
data/lib/magic_cloud/version.rb
CHANGED
data/lib/magic_cloud/word.rb
CHANGED
data/lib/magic_cloud.rb
CHANGED
data/magic_cloud.gemspec
CHANGED
@@ -32,9 +32,11 @@ Gem::Specification.new do |s|
|
|
32
32
|
s.rubygems_version = '2.2.2'
|
33
33
|
|
34
34
|
s.add_dependency 'rmagick'
|
35
|
-
s.add_dependency 'slop', '~> 3.0
|
35
|
+
s.add_dependency 'slop', '~> 3.0' # for bin/magic_cloud options parsing
|
36
36
|
|
37
37
|
s.add_development_dependency 'rubocop'
|
38
38
|
s.add_development_dependency 'bundler'
|
39
39
|
s.add_development_dependency 'ruby-prof'
|
40
|
+
s.add_development_dependency 'rake'
|
41
|
+
s.add_development_dependency 'rubygems-tasks'
|
40
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: magic_cloud
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Victor Shepelev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rmagick
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 3.0
|
33
|
+
version: '3.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 3.0
|
40
|
+
version: '3.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rubocop
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubygems-tasks
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description: |2
|
84
112
|
Simple, pure-ruby library for making pretty Wordle-like clouds.
|
85
113
|
It uses RMagick as graphic backend.
|
@@ -128,8 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
156
|
- !ruby/object:Gem::Version
|
129
157
|
version: '0'
|
130
158
|
requirements: []
|
131
|
-
|
132
|
-
rubygems_version: 2.4.6
|
159
|
+
rubygems_version: 3.4.10
|
133
160
|
signing_key:
|
134
161
|
specification_version: 4
|
135
162
|
summary: Pretty word cloud maker for Ruby
|