magic_cloud 0.0.2

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.
@@ -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