magic_cloud 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +142 -0
- data/bin/magic_cloud +86 -0
- data/lib/magic_cloud.rb +7 -0
- data/lib/magic_cloud/bit_matrix.rb +33 -0
- data/lib/magic_cloud/canvas.rb +93 -0
- data/lib/magic_cloud/cloud.rb +137 -0
- data/lib/magic_cloud/collision_board.rb +110 -0
- data/lib/magic_cloud/debug.rb +28 -0
- data/lib/magic_cloud/layouter.rb +67 -0
- data/lib/magic_cloud/layouter/place.rb +109 -0
- data/lib/magic_cloud/palettes.rb +33 -0
- data/lib/magic_cloud/rect.rb +88 -0
- data/lib/magic_cloud/shape.rb +57 -0
- data/lib/magic_cloud/spriter.rb +90 -0
- data/lib/magic_cloud/version.rb +6 -0
- data/lib/magic_cloud/word.rb +30 -0
- data/magic_cloud.gemspec +40 -0
- data/samples/cat-in-the-hat.txt +2 -0
- data/samples/cat.png +0 -0
- metadata +136 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative './bit_matrix'
|
3
|
+
|
4
|
+
module MagicCloud
|
5
|
+
# Pixel-by-pixel collision board
|
6
|
+
#
|
7
|
+
# Providen by width and height of the board, allows to check if given
|
8
|
+
# shape (array of zero and non-zero pixels) "collides" with any of
|
9
|
+
# previosly placed shapes
|
10
|
+
class CollisionBoard < BitMatrix
|
11
|
+
def initialize(width, height)
|
12
|
+
super
|
13
|
+
@rects = []
|
14
|
+
@intersections_cache = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :rects, :intersections_cache
|
18
|
+
|
19
|
+
def criss_cross_collision?(rect)
|
20
|
+
if rects.any?{|r| r.criss_cross?(rect)}
|
21
|
+
Debug.stats[:criss_cross] += 1
|
22
|
+
true
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def collides_previous?(shape, intersections)
|
29
|
+
prev_idx = intersections_cache[shape.object_id]
|
30
|
+
|
31
|
+
if prev_idx && (prev = intersections[prev_idx]) &&
|
32
|
+
pixels_collision?(shape, prev)
|
33
|
+
|
34
|
+
Debug.stats[:px_prev_yes] += 1
|
35
|
+
true
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def pixels_collision_multi?(shape, intersections)
|
42
|
+
intersections.each_with_index do |intersection, idx|
|
43
|
+
next unless intersection
|
44
|
+
next if idx == intersections_cache[shape.object_id] # already checked it
|
45
|
+
|
46
|
+
next unless pixels_collision?(shape, intersection)
|
47
|
+
|
48
|
+
Debug.stats[:px_yes] += 1
|
49
|
+
intersections_cache[shape.object_id] = idx
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def collides?(shape)
|
57
|
+
Debug.stats[:collide_total] += 1
|
58
|
+
|
59
|
+
# nothing on board - so, no collisions
|
60
|
+
return false if rects.empty?
|
61
|
+
|
62
|
+
# no point to try drawing criss-crossed words
|
63
|
+
# even if they will not collide pixel-per-pixel
|
64
|
+
return true if criss_cross_collision?(shape.rect)
|
65
|
+
|
66
|
+
# then find which of placed sprites rectangles tag intersects
|
67
|
+
intersections = rects.map{|r| r.intersect(shape.rect)}
|
68
|
+
|
69
|
+
# no need to further check: this tag is not inside any others' rectangle
|
70
|
+
if intersections.compact.empty?
|
71
|
+
Debug.stats[:rect_no] += 1
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
# most probable that we are still collide with this word
|
76
|
+
return true if collides_previous?(shape, intersections)
|
77
|
+
|
78
|
+
# only then check points inside intersected rectangles
|
79
|
+
return true if pixels_collision_multi?(shape, intersections)
|
80
|
+
|
81
|
+
Debug.stats[:px_no] += 1
|
82
|
+
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def pixels_collision?(shape, rect)
|
87
|
+
l, t = shape.left, shape.top
|
88
|
+
(rect.x0...rect.x1).each do |x|
|
89
|
+
(rect.y0...rect.y1).each do |y|
|
90
|
+
dx = x - l
|
91
|
+
dy = y - t
|
92
|
+
return true if shape.sprite.at(dx, dy) && at(x, y)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
def add(shape)
|
100
|
+
l, t = shape.left, shape.top
|
101
|
+
shape.height.times do |dy|
|
102
|
+
shape.width.times do |dx|
|
103
|
+
put(l + dx, t + dy) if shape.sprite.at(dx, dy)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
rects << shape.rect
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'forwardable'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module MagicCloud
|
6
|
+
# Debugging utilities class for cloud development itself
|
7
|
+
class Debug
|
8
|
+
class << self
|
9
|
+
def instance
|
10
|
+
@instance ||= new
|
11
|
+
end
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
def_delegators :instance, :logger, :stats, :reset!
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@logger = Logger.new(STDOUT).tap{|l| l.level = Logger::FATAL}
|
19
|
+
@stats = Hash.new{|h, k| h[k] = 0}
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :logger, :stats
|
23
|
+
|
24
|
+
def reset!
|
25
|
+
@stats = Hash.new{|h, k| h[k] = 0}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative './collision_board'
|
3
|
+
|
4
|
+
module MagicCloud
|
5
|
+
# Main magic of magic cloud - layouting shapes without collisions.
|
6
|
+
# Also, alongside with CollisionBoard - the slowest and
|
7
|
+
# algorithmically trickiest part.
|
8
|
+
class Layouter
|
9
|
+
def initialize(w, h, options = {})
|
10
|
+
@board = CollisionBoard.new(w, h)
|
11
|
+
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :board
|
16
|
+
|
17
|
+
def width
|
18
|
+
board.width
|
19
|
+
end
|
20
|
+
|
21
|
+
def height
|
22
|
+
board.height
|
23
|
+
end
|
24
|
+
|
25
|
+
def layout!(shapes)
|
26
|
+
visible_shapes = []
|
27
|
+
|
28
|
+
shapes.each do |shape|
|
29
|
+
next unless find_place(shape)
|
30
|
+
|
31
|
+
visible_shapes.push(shape)
|
32
|
+
end
|
33
|
+
|
34
|
+
visible_shapes
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_place(shape)
|
40
|
+
place = Place.new(self, shape)
|
41
|
+
start = Time.now
|
42
|
+
steps = 0
|
43
|
+
|
44
|
+
loop do
|
45
|
+
steps += 1
|
46
|
+
place.next!
|
47
|
+
|
48
|
+
next unless place.ready?
|
49
|
+
|
50
|
+
board.add(shape)
|
51
|
+
Debug.logger.info 'Place for %p found in %i steps (%.2f sec)' %
|
52
|
+
[shape, steps, Time.now-start]
|
53
|
+
|
54
|
+
break
|
55
|
+
end
|
56
|
+
|
57
|
+
true
|
58
|
+
rescue PlaceNotFound
|
59
|
+
Debug.logger.warn 'No place for %p found in %i steps (%.2f sec)' %
|
60
|
+
[shape, steps, Time.now-start]
|
61
|
+
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
require_relative './layouter/place'
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MagicCloud
|
3
|
+
class Layouter
|
4
|
+
class PlaceNotFound < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Incapsulating place lookup process
|
8
|
+
# 1. find initial random place
|
9
|
+
# 2. at each step, shift in spiral from the previous place
|
10
|
+
# 3. always knows, if the place "ready" for shape (empty and inside board)
|
11
|
+
class Place
|
12
|
+
def initialize(layouter, shape)
|
13
|
+
@layouter, @shape = layouter, shape
|
14
|
+
|
15
|
+
# initial position
|
16
|
+
@start_x = (
|
17
|
+
@layouter.width/2 + # from center
|
18
|
+
(rand-0.5) * # random shift
|
19
|
+
(@layouter.width - @shape.width) # not more than (cloud width - word width)
|
20
|
+
).to_i
|
21
|
+
@start_y = (
|
22
|
+
@layouter.height/2 + # from center
|
23
|
+
(rand-0.5) * # random shift
|
24
|
+
(@layouter.height - @shape.height) # not more than (cloud height - word height)
|
25
|
+
).to_i
|
26
|
+
|
27
|
+
# when shift of position is more than max delta (diagonal of cloud)
|
28
|
+
# there is no hope it will eventually found its place
|
29
|
+
@max_delta = Math.sqrt(@layouter.width**2 + @layouter.height**2)
|
30
|
+
|
31
|
+
# algo of next position calc
|
32
|
+
@spiral = make_spiral(@shape.size)
|
33
|
+
|
34
|
+
# direction of spiral
|
35
|
+
@dt = rand < 0.5 ? 1 : -1
|
36
|
+
|
37
|
+
# initial point of time before we start to look for place
|
38
|
+
@t = -@dt
|
39
|
+
end
|
40
|
+
|
41
|
+
def next!
|
42
|
+
@t += @dt
|
43
|
+
dx, dy = @spiral.call(@t)
|
44
|
+
|
45
|
+
fail PlaceNotFound if [dx, dy].map(&:abs).min > @max_delta
|
46
|
+
|
47
|
+
@shape.x = @start_x + dx
|
48
|
+
@shape.y = @start_y + dy
|
49
|
+
end
|
50
|
+
|
51
|
+
def ready?
|
52
|
+
!out_of_board? && !@layouter.board.collides?(@shape)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def out_of_board?
|
58
|
+
@shape.left < 0 || @shape.top < 0 ||
|
59
|
+
@shape.right > @layouter.width || @shape.bottom > @layouter.height
|
60
|
+
end
|
61
|
+
|
62
|
+
# FIXME: now we always use "rectangular spiral"
|
63
|
+
# d3.layout.cloud had two of them as an option - rectangular and
|
64
|
+
# archimedean. I assume, it should be checked if usage of two
|
65
|
+
# spirals produce significantly different results, and then
|
66
|
+
# either one spiral should left, or it should became an option.
|
67
|
+
def make_spiral(step)
|
68
|
+
rectangular_spiral(step)
|
69
|
+
end
|
70
|
+
|
71
|
+
# rubocop:disable Metrics/AbcSize
|
72
|
+
def archimedean_spiral(size)
|
73
|
+
e = width / height
|
74
|
+
->(t){
|
75
|
+
t1 = t * size * 0.01
|
76
|
+
|
77
|
+
[
|
78
|
+
e * t1 * Math.cos(t1),
|
79
|
+
t1 * Math.sin(t1)
|
80
|
+
].map(&:round)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def rectangular_spiral(size)
|
85
|
+
dy = 4 * size * 0.1
|
86
|
+
dx = dy * @layouter.width / @layouter.height
|
87
|
+
x = 0
|
88
|
+
y = 0
|
89
|
+
->(t){
|
90
|
+
sign = t < 0 ? -1 : 1
|
91
|
+
|
92
|
+
# zverok: this is original comment & code from d3.layout.cloud.js
|
93
|
+
# Looks too witty for me.
|
94
|
+
#
|
95
|
+
# See triangular numbers: T_n = n * (n + 1) / 2.
|
96
|
+
case (Math.sqrt(1 + 4 * sign * t) - sign).to_i & 3
|
97
|
+
when 0 then x += dx
|
98
|
+
when 1 then y += dy
|
99
|
+
when 2 then x -= dx
|
100
|
+
else y -= dy
|
101
|
+
end
|
102
|
+
|
103
|
+
[x, y].map(&:round)
|
104
|
+
}
|
105
|
+
end
|
106
|
+
# rubocop:enable Metrics/AbcSize
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MagicCloud
|
3
|
+
PALETTES = {
|
4
|
+
# Categorical colors from d3
|
5
|
+
# Source: https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
|
6
|
+
|
7
|
+
category10: %w[
|
8
|
+
#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd
|
9
|
+
#8c564b #e377c2 #7f7f7f #bcbd22 #17becf
|
10
|
+
],
|
11
|
+
|
12
|
+
category20: %w[
|
13
|
+
#1f77b4 #aec7e8 #ff7f0e #ffbb78 #2ca02c
|
14
|
+
#98df8a #d62728 #ff9896 #9467bd #c5b0d5
|
15
|
+
#8c564b #c49c94 #e377c2 #f7b6d2 #7f7f7f
|
16
|
+
#c7c7c7 #bcbd22 #dbdb8d #17becf #9edae5
|
17
|
+
],
|
18
|
+
|
19
|
+
category20b: %w[
|
20
|
+
#393b79 #5254a3 #6b6ecf #9c9ede #637939
|
21
|
+
#8ca252 #b5cf6b #cedb9c #8c6d31 #bd9e39
|
22
|
+
#e7ba52 #e7cb94 #843c39 #ad494a #d6616b
|
23
|
+
#e7969c #7b4173 #a55194 #ce6dbd #de9ed6
|
24
|
+
],
|
25
|
+
|
26
|
+
category20c: %w[
|
27
|
+
#3182bd #6baed6 #9ecae1 #c6dbef #e6550d
|
28
|
+
#fd8d3c #fdae6b #fdd0a2 #31a354 #74c476
|
29
|
+
#a1d99b #c7e9c0 #756bb1 #9e9ac8 #bcbddc
|
30
|
+
#dadaeb #636363 #969696 #bdbdbd #d9d9d9
|
31
|
+
]
|
32
|
+
}
|
33
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MagicCloud
|
3
|
+
# Utility geometrical rectangle, implementing arithmetic interactions
|
4
|
+
# with other rectangles
|
5
|
+
# (not to be confused with drawable shapes)
|
6
|
+
class Rect
|
7
|
+
def initialize(x0, y0, x1, y1)
|
8
|
+
@x0, @y0, @x1, @y1 = x0, y0, x1, y1
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :x0, :y0, :x1, :y1
|
12
|
+
# NB: we are trying to use instance variables instead of accessors
|
13
|
+
# inside this class methods, because they are called so many
|
14
|
+
# times that accessor overhead IS significant.
|
15
|
+
|
16
|
+
def width
|
17
|
+
@x1 - @x0
|
18
|
+
end
|
19
|
+
|
20
|
+
def height
|
21
|
+
@y1 - @y0
|
22
|
+
end
|
23
|
+
|
24
|
+
def collide?(other)
|
25
|
+
@x1 > other.x0 &&
|
26
|
+
@x0 < other.x1 &&
|
27
|
+
@y1 > other.y0 &&
|
28
|
+
@y0 < other.y1
|
29
|
+
end
|
30
|
+
|
31
|
+
# shift to new coords, preserving the size
|
32
|
+
def move_to(x, y)
|
33
|
+
@x1 += x - @x0
|
34
|
+
@y1 += y - @y0
|
35
|
+
@x0 = x
|
36
|
+
@y0 = y
|
37
|
+
end
|
38
|
+
|
39
|
+
# rubocop:disable Metrics/AbcSize
|
40
|
+
def adjust!(other)
|
41
|
+
@x0 = other.x0 if other.x0 < @x0
|
42
|
+
@y0 = other.y0 if other.y0 < @y0
|
43
|
+
@x1 = other.x1 if other.x1 > @x1
|
44
|
+
@y1 = other.y1 if other.y1 > @y1
|
45
|
+
end
|
46
|
+
# rubocop:enable Metrics/AbcSize
|
47
|
+
|
48
|
+
def adjust(other)
|
49
|
+
dup.tap{|d| d.adjust!(other)}
|
50
|
+
end
|
51
|
+
|
52
|
+
# rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize
|
53
|
+
def criss_cross?(other)
|
54
|
+
# case 1: this one is horizontal:
|
55
|
+
# overlaps other by x, to right and left, and goes inside it by y
|
56
|
+
@x0 < other.x0 && @x1 > other.x1 &&
|
57
|
+
@y0 > other.y0 && @y1 < other.y1 ||
|
58
|
+
# case 2: this one is vertical:
|
59
|
+
# overlaps other by y, to top and bottom, and goes inside it by x
|
60
|
+
@y0 < other.y0 && @y1 > other.y1 &&
|
61
|
+
@x0 > other.x0 && @x1 < other.x1
|
62
|
+
end
|
63
|
+
# rubocop:enable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize
|
64
|
+
|
65
|
+
def intersect(other)
|
66
|
+
# direct comparison is dirtier, yet significantly faster than
|
67
|
+
# something like [@x0, other.x0].max
|
68
|
+
ix0 = @x0 > other.x0 ? @x0 : other.x0
|
69
|
+
ix1 = @x1 < other.x1 ? @x1 : other.x1
|
70
|
+
iy0 = @y0 > other.y0 ? @y0 : other.y0
|
71
|
+
iy1 = @y1 < other.y1 ? @y1 : other.y1
|
72
|
+
|
73
|
+
if ix0 > ix1 || iy0 > iy1
|
74
|
+
nil # rectangles are not intersected, in fact
|
75
|
+
else
|
76
|
+
Rect.new(ix0, iy0, ix1, iy1)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<Rect[#{x0},#{y0};#{x1},#{y1}]>"
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"#<Rect[#{x0},#{y0};#{x1},#{y1}]>"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MagicCloud
|
3
|
+
# Basic "abstract shape" class, with all primitive functionality
|
4
|
+
# necessary for use it in Spriter and Layouter.
|
5
|
+
#
|
6
|
+
# Word for wordcloud is inherited from it, and its potentially
|
7
|
+
# possible to inherit other types of shapes and layout them also.
|
8
|
+
class Shape
|
9
|
+
def initialize
|
10
|
+
@x = 0
|
11
|
+
@y = 0
|
12
|
+
@sprite = nil
|
13
|
+
@rect = nil
|
14
|
+
@width = 0
|
15
|
+
@height = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :sprite, :x, :y, :width, :height, :rect
|
19
|
+
|
20
|
+
def sprite=(sprite)
|
21
|
+
@sprite = sprite
|
22
|
+
@width = sprite.width
|
23
|
+
@height = sprite.height
|
24
|
+
@rect = Rect.new(left, top, right, bottom)
|
25
|
+
end
|
26
|
+
|
27
|
+
def x=(newx)
|
28
|
+
@x = newx
|
29
|
+
@rect.move_to(@x, @y)
|
30
|
+
end
|
31
|
+
|
32
|
+
def y=(newy)
|
33
|
+
@y = newy
|
34
|
+
@rect.move_to(@x, @y)
|
35
|
+
end
|
36
|
+
|
37
|
+
def left
|
38
|
+
x
|
39
|
+
end
|
40
|
+
|
41
|
+
def right
|
42
|
+
x + width
|
43
|
+
end
|
44
|
+
|
45
|
+
def top
|
46
|
+
y
|
47
|
+
end
|
48
|
+
|
49
|
+
def bottom
|
50
|
+
y + height
|
51
|
+
end
|
52
|
+
|
53
|
+
def draw(_canvas)
|
54
|
+
fail NotImplementedError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|