magic_cloud 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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