theseus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,183 @@
1
+ require 'chunky_png'
2
+
3
+ module Theseus
4
+ module Formatters
5
+ # This is an abstract superclass for PNG formatters. It simply provides some common
6
+ # utility and drawing methods that subclasses can take advantage of, to render
7
+ # mazes to a PNG canvas.
8
+ #
9
+ # Colors are given as 32-bit integers, with each RGBA component occupying 1 byte.
10
+ # R is the highest byte, A is the lowest byte. In other words, 0xFF0000FF is an
11
+ # opaque red, and 0x7f7f7f7f is a semi-transparent gray. 0x0 is fully transparent.
12
+ #
13
+ # You may also provide the colors as hexadecimal string values, and they will be
14
+ # converted to the corresponding integers.
15
+ class PNG
16
+ # The default options. Note that not all PNG formatters honor all of these options;
17
+ # specifically, +:wall_width+ is not consistently supported across all formatters.
18
+ DEFAULTS = {
19
+ :cell_size => 10,
20
+ :wall_width => 1,
21
+ :wall_color => 0x000000FF,
22
+ :cell_color => 0xFFFFFFFF,
23
+ :solution_color => 0xFFAFAFFF,
24
+ :background => 0x00000000,
25
+ :outer_padding => 2,
26
+ :cell_padding => 1,
27
+ :solution => false
28
+ }
29
+
30
+ # North, whether in the under or primary plane
31
+ ANY_N = Maze::N | (Maze::N << Maze::UNDER_SHIFT)
32
+
33
+ # South, whether in the under or primary plane
34
+ ANY_S = Maze::S | (Maze::S << Maze::UNDER_SHIFT)
35
+
36
+ # West, whether in the under or primary plane
37
+ ANY_W = Maze::W | (Maze::W << Maze::UNDER_SHIFT)
38
+
39
+ # East, whether in the under or primary plane
40
+ ANY_E = Maze::E | (Maze::E << Maze::UNDER_SHIFT)
41
+
42
+ # The options to use for the formatter. These are the ones passed
43
+ # to the constructor, plus the ones from the DEFAULTS hash.
44
+ attr_reader :options
45
+
46
+ # The +options+ must be a hash of any of the following options:
47
+ #
48
+ # [:cell_size] The number of pixels on a side that each cell
49
+ # should occupy. Different maze types will use that
50
+ # space differently. Also, the cell padding is applied
51
+ # inside the cell, and so consumes some of the area.
52
+ # The default is 10.
53
+ # [:wall_width] How thick the walls should be drawn. The default is 1.
54
+ # Note that not all PNG formatters will honor this value
55
+ # (yet).
56
+ # [:wall_color] The color to use when drawing the wall. Defaults to black.
57
+ # [:cell_color] The color to use when drawing the cell. Defaults to white.
58
+ # [:solution_color] The color to use when drawing the solution path. This is
59
+ # only used when the :solution option is given.
60
+ # [:background] The color to use for the background of the maze. Defaults
61
+ # to transparent.
62
+ # [:outer_padding] The extra padding (in pixels) to add around the outside
63
+ # edge of the maze. Defaults to 2.
64
+ # [:cell_padding] The padding (in pixels) to add around the inside of each
65
+ # cell. This has the effect of separating the cells. The
66
+ # default cell padding is 1.
67
+ # [:solution] A boolean value indicating whether or not to draw the
68
+ # solution path as well. The default is false.
69
+ def initialize(maze, options)
70
+ @options = DEFAULTS.merge(options)
71
+
72
+ [:background, :wall_color, :cell_color, :solution_color].each do |c|
73
+ @options[c] = ChunkyPNG::Color.from_hex(@options[c]) unless Fixnum === @options[c]
74
+ end
75
+
76
+ @paths = @options[:paths] || []
77
+
78
+ if @options[:solution]
79
+ path = maze.new_solver(type: @options[:solution]).solve.to_path(color: @options[:solution_color])
80
+ @paths = [path, *@paths]
81
+ end
82
+ end
83
+
84
+ # Returns the raw PNG data for the formatter.
85
+ def to_blob
86
+ @blob
87
+ end
88
+
89
+ # Returns the color at the given point by considering all provided paths. The
90
+ # +:color: metadata from the first path that is set at the given point is
91
+ # returned. If no path describes the given point, then the value of the
92
+ # +:cell_color+ option is returned.
93
+ def color_at(pt, direction=nil)
94
+ @paths.each do |path|
95
+ return path[:color] if direction ? path.path?(pt, direction) : path.set?(pt)
96
+ end
97
+
98
+ return @options[:cell_color]
99
+ end
100
+
101
+ # Returns a new 2-tuple (x2,y2), where x2 is point[0] + dx, and y2 is point[1] + dy.
102
+ def move(point, dx, dy)
103
+ [point[0] + dx, point[1] + dy]
104
+ end
105
+
106
+ # Clamps the value +x+ so that it lies between +low+ and +hi+. In other words,
107
+ # returns +low+ if +x+ is less than +low+, and +high+ if +x+ is greater than
108
+ # +high+, and returns +x+ otherwise.
109
+ def clamp(x, low, hi)
110
+ x = low if x < low
111
+ x = hi if x > hi
112
+ return x
113
+ end
114
+
115
+ # Draws a line from +p1+ to +p2+ on the given canvas object, in the given
116
+ # color. The coordinates of the given points are clamped (naively) to lie
117
+ # within the canvas' bounds.
118
+ def line(canvas, p1, p2, color)
119
+ canvas.line(
120
+ clamp(p1[0].round, 0, canvas.width-1),
121
+ clamp(p1[1].round, 0, canvas.height-1),
122
+ clamp(p2[0].round, 0, canvas.width-1),
123
+ clamp(p2[1].round, 0, canvas.height-1),
124
+ color)
125
+ end
126
+
127
+ # Fills the rectangle defined by the given coordinates with the given color.
128
+ # The coordinates are clamped to lie within the canvas' bounds.
129
+ def fill_rect(canvas, x0, y0, x1, y1, color)
130
+ x0 = clamp(x0, 0, canvas.width-1)
131
+ y0 = clamp(y0, 0, canvas.height-1)
132
+ x1 = clamp(x1, 0, canvas.width-1)
133
+ y1 = clamp(y1, 0, canvas.height-1)
134
+ [x0, x1].min.ceil.upto([x0, x1].max.floor) do |x|
135
+ [y0, y1].min.ceil.upto([y0, y1].max.floor) do |y|
136
+ canvas.point(x, y, color)
137
+ end
138
+ end
139
+ end
140
+
141
+ # Fills the polygon defined by the +points+ array, with the given +color+.
142
+ # Each element of +points+ must be a 2-tuple describing a vertex of the
143
+ # polygon. It is assumed that the polygon is closed. All points are
144
+ # clamped (naively) to lie within the canvas' bounds.
145
+ def fill_poly(canvas, points, color)
146
+ min_y = 1_000_000
147
+ max_y = -1_000_000
148
+ points.each do |x,y|
149
+ min_y = y if y < min_y
150
+ max_y = y if y > max_y
151
+ end
152
+
153
+ min_y = clamp(min_y, 0, canvas.height-1)
154
+ max_y = clamp(max_y, 0, canvas.height-1)
155
+
156
+ min_y.floor.upto(max_y.ceil) do |y|
157
+ nodes = []
158
+
159
+ prev = points.last
160
+ points.each do |point|
161
+ if point[1] < y && prev[1] >= y || prev[1] < y && point[1] >= y
162
+ nodes << (point[0] + (y - point[1]).to_f / (prev[1] - point[1]) * (prev[0] - point[0]))
163
+ end
164
+ prev = point
165
+ end
166
+
167
+ next if nodes.empty?
168
+ nodes.sort!
169
+
170
+ prev = nil
171
+ 0.step(nodes.length-1, 2) do |a|
172
+ x1, x2 = nodes[a], nodes[a+1]
173
+ x1, x2 = x2, x1 if x1 > x2
174
+ next if x1 < 0 || x2 >= canvas.width
175
+ x1.ceil.upto(x2.floor) do |x|
176
+ canvas.point(x, y, color)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,85 @@
1
+ require 'theseus/formatters/png'
2
+
3
+ module Theseus
4
+ module Formatters
5
+ class PNG
6
+ # Renders a DeltaMaze to a PNG canvas. Does not currently support the
7
+ # +:wall_width+ option.
8
+ #
9
+ # You will almost never access this class directly. Instead, use
10
+ # DeltaMaze#to(:png, options) to return the raw PNG data directly.
11
+ class Delta < PNG
12
+ # Create and return a fully initialized PNG::Delta object, with the
13
+ # maze rendered. To get the maze data, call #to_blob.
14
+ #
15
+ # See Theseus::Formatters::PNG for a list of all supported options.
16
+ def initialize(maze, options={})
17
+ super
18
+
19
+ height = @options[:outer_padding] * 2 + maze.height * @options[:cell_size]
20
+ width = @options[:outer_padding] * 2 + (maze.width + 1) * @options[:cell_size] / 2
21
+
22
+ canvas = ChunkyPNG::Image.new(width, height, @options[:background])
23
+
24
+ maze.height.times do |y|
25
+ py = @options[:outer_padding] + y * @options[:cell_size]
26
+ maze.row_length(y).times do |x|
27
+ px = @options[:outer_padding] + x * @options[:cell_size] / 2.0
28
+ draw_cell(canvas, [x, y], maze.points_up?(x,y), px, py, maze[x, y])
29
+ end
30
+ end
31
+
32
+ @blob = canvas.to_blob
33
+ end
34
+
35
+ private
36
+
37
+ def draw_cell(canvas, point, up, x, y, cell) #:nodoc:
38
+ return if cell == 0
39
+
40
+ p1 = [x + options[:cell_size] / 2.0, up ? (y + options[:cell_padding]) : (y + options[:cell_size] - options[:cell_padding])]
41
+ p2 = [x + options[:cell_padding], up ? (y + options[:cell_size] - options[:cell_padding]) : (y + options[:cell_padding])]
42
+ p3 = [x + options[:cell_size] - options[:cell_padding], p2[1]]
43
+
44
+ fill_poly(canvas, [p1, p2, p3], color_at(point))
45
+
46
+ if cell & (Maze::N | Maze::S) != 0
47
+ clr = color_at(point, (Maze::N | Maze::S))
48
+ dy = options[:cell_padding]
49
+ sign = (cell & Maze::N != 0) ? -1 : 1
50
+ r1, r2 = p2, move(p3, 0, sign*dy)
51
+ fill_rect(canvas, r1[0].round, r1[1].round, r2[0].round, r2[1].round, clr)
52
+ line(canvas, r1, [r1[0], r2[1]], options[:wall_color])
53
+ line(canvas, r2, [r2[0], r1[1]], options[:wall_color])
54
+ else
55
+ line(canvas, p2, p3, options[:wall_color])
56
+ end
57
+
58
+ dx = options[:cell_padding]
59
+ if cell & ANY_W != 0
60
+ r1, r2, r3, r4 = p1, move(p1,-dx,0), move(p2,-dx,0), p2
61
+ fill_poly(canvas, [r1, r2, r3, r4], color_at(point, ANY_W))
62
+ line(canvas, r1, r2, options[:wall_color])
63
+ line(canvas, r3, r4, options[:wall_color])
64
+ end
65
+
66
+ if cell & Maze::W == 0
67
+ line(canvas, p1, p2, options[:wall_color])
68
+ end
69
+
70
+ if cell & ANY_E != 0
71
+ r1, r2, r3, r4 = p1, move(p1,dx,0), move(p3,dx,0), p3
72
+ fill_poly(canvas, [r1, r2, r3, r4], color_at(point, ANY_E))
73
+ line(canvas, r1, r2, options[:wall_color])
74
+ line(canvas, r3, r4, options[:wall_color])
75
+ end
76
+
77
+ if cell & Maze::E == 0
78
+ line(canvas, p3, p1, options[:wall_color])
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -0,0 +1,87 @@
1
+ require 'theseus/formatters/png'
2
+
3
+ module Theseus
4
+ module Formatters
5
+ class PNG
6
+ # Renders an OrthogonalMaze to a PNG canvas.
7
+ #
8
+ # You will almost never access this class directly. Instead, use
9
+ # OrthogonalMaze#to(:png, options) to return the raw PNG data directly.
10
+ class Orthogonal < PNG
11
+ # Create and return a fully initialized PNG::Orthogonal object, with the
12
+ # maze rendered. To get the maze data, call #to_blob.
13
+ #
14
+ # See Theseus::Formatters::PNG for a list of all supported options.
15
+ def initialize(maze, options={})
16
+ super
17
+
18
+ width = @options[:outer_padding] * 2 + maze.width * @options[:cell_size]
19
+ height = @options[:outer_padding] * 2 + maze.height * @options[:cell_size]
20
+
21
+ canvas = ChunkyPNG::Image.new(width, height, @options[:background])
22
+
23
+ @d1 = @options[:cell_padding]
24
+ @d2 = @options[:cell_size] - @options[:cell_padding]
25
+ @w1 = (@options[:wall_width] / 2.0).floor
26
+ @w2 = ((@options[:wall_width] - 1) / 2.0).floor
27
+
28
+ maze.height.times do |y|
29
+ py = @options[:outer_padding] + y * @options[:cell_size]
30
+ maze.width.times do |x|
31
+ px = @options[:outer_padding] + x * @options[:cell_size]
32
+ draw_cell(canvas, [x, y], px, py, maze[x, y])
33
+ end
34
+ end
35
+
36
+ @blob = canvas.to_blob
37
+ end
38
+
39
+ private
40
+
41
+ def draw_cell(canvas, point, x, y, cell) #:nodoc:
42
+ return if cell == 0
43
+
44
+ fill_rect(canvas, x + @d1, y + @d1, x + @d2, y + @d2, color_at(point))
45
+
46
+ north = cell & Maze::N == Maze::N
47
+ north_under = (cell >> Maze::UNDER_SHIFT) & Maze::N == Maze::N
48
+ south = cell & Maze::S == Maze::S
49
+ south_under = (cell >> Maze::UNDER_SHIFT) & Maze::S == Maze::S
50
+ west = cell & Maze::W == Maze::W
51
+ west_under = (cell >> Maze::UNDER_SHIFT) & Maze::W == Maze::W
52
+ east = cell & Maze::E == Maze::E
53
+ east_under = (cell >> Maze::UNDER_SHIFT) & Maze::E == Maze::E
54
+
55
+ draw_vertical(canvas, x, y, 1, north || north_under, !north || north_under, color_at(point, ANY_N))
56
+ draw_vertical(canvas, x, y + options[:cell_size], -1, south || south_under, !south || south_under, color_at(point, ANY_S))
57
+ draw_horizontal(canvas, x, y, 1, west || west_under, !west || west_under, color_at(point, ANY_W))
58
+ draw_horizontal(canvas, x + options[:cell_size], y, -1, east || east_under, !east || east_under, color_at(point, ANY_E))
59
+ end
60
+
61
+ def draw_vertical(canvas, x, y, direction, corridor, wall, color) #:nodoc:
62
+ if corridor
63
+ fill_rect(canvas, x + @d1, y, x + @d2, y + @d1 * direction, color)
64
+ fill_rect(canvas, x + @d1 - @w1, y - (@w1 * direction), x + @d1 + @w2, y + (@d1 + @w2) * direction, options[:wall_color])
65
+ fill_rect(canvas, x + @d2 - @w2, y - (@w1 * direction), x + @d2 + @w1, y + (@d1 + @w2) * direction, options[:wall_color])
66
+ end
67
+
68
+ if wall
69
+ fill_rect(canvas, x + @d1 - @w1, y + (@d1 - @w1) * direction, x + @d2 + @w2, y + (@d1 + @w2) * direction, options[:wall_color])
70
+ end
71
+ end
72
+
73
+ def draw_horizontal(canvas, x, y, direction, corridor, wall, color) #:nodoc:
74
+ if corridor
75
+ fill_rect(canvas, x, y + @d1, x + @d1 * direction, y + @d2, color)
76
+ fill_rect(canvas, x - (@w1 * direction), y + @d1 - @w1, x + (@d1 + @w2) * direction, y + @d1 + @w2, options[:wall_color])
77
+ fill_rect(canvas, x - (@w1 * direction), y + @d2 - @w2, x + (@d1 + @w2) * direction, y + @d2 + @w1, options[:wall_color])
78
+ end
79
+
80
+ if wall
81
+ fill_rect(canvas, x + (@d1 - @w1) * direction, y + @d1 - @w1, x + (@d1 + @w2) * direction, y + @d2 + @w2, options[:wall_color])
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,105 @@
1
+ require 'theseus/formatters/png'
2
+
3
+ module Theseus
4
+ module Formatters
5
+ class PNG
6
+ # Renders a SigmaMaze to a PNG canvas. Does not currently support the
7
+ # +:wall_width+ option.
8
+ #
9
+ # You will almost never access this class directly. Instead, use
10
+ # SigmaMaze#to(:png, options) to return the raw PNG data directly.
11
+ class Sigma < PNG
12
+ # Create and return a fully initialized PNG::Sigma object, with the
13
+ # maze rendered. To get the maze data, call #to_blob.
14
+ #
15
+ # See Theseus::Formatters::PNG for a list of all supported options.
16
+ def initialize(maze, options={})
17
+ super
18
+
19
+ width = @options[:outer_padding] * 2 + (3 * maze.width + 1) * @options[:cell_size] / 4
20
+ height = @options[:outer_padding] * 2 + maze.height * @options[:cell_size] + @options[:cell_size] / 2
21
+
22
+ canvas = ChunkyPNG::Image.new(width, height, @options[:background])
23
+
24
+ maze.height.times do |y|
25
+ py = @options[:outer_padding] + y * @options[:cell_size]
26
+ maze.row_length(y).times do |x|
27
+ px = @options[:outer_padding] + x * 3 * @options[:cell_size] / 4.0
28
+ shifted = (x % 2 != 0)
29
+ dy = shifted ? (@options[:cell_size] / 2.0) : 0
30
+ draw_cell(canvas, [x, y], shifted, px, py+dy, maze[x, y])
31
+ end
32
+ end
33
+
34
+ @blob = canvas.to_blob
35
+ end
36
+
37
+ private
38
+
39
+ def draw_cell(canvas, point, shifted, x, y, cell) #:nodoc:
40
+ return if cell == 0
41
+
42
+ size = options[:cell_size] - options[:cell_padding] * 2
43
+ s4 = size / 4.0
44
+
45
+ fs4 = options[:cell_size] / 4.0 # fs == full-size, without padding
46
+
47
+ p1 = [x + options[:cell_padding] + s4, y + options[:cell_padding]]
48
+ p2 = [x + options[:cell_size] - options[:cell_padding] - s4, p1[1]]
49
+ p3 = [x + options[:cell_padding] + size, y + options[:cell_size] / 2.0]
50
+ p4 = [p2[0], y + options[:cell_size] - options[:cell_padding]]
51
+ p5 = [p1[0], p4[1]]
52
+ p6 = [x + options[:cell_padding], p3[1]]
53
+
54
+ fill_poly(canvas, [p1, p2, p3, p4, p5, p6], color_at(point))
55
+
56
+ n = Maze::N
57
+ s = Maze::S
58
+ nw = shifted ? Maze::W : Maze::NW
59
+ ne = shifted ? Maze::E : Maze::NE
60
+ sw = shifted ? Maze::SW : Maze::W
61
+ se = shifted ? Maze::SE : Maze::E
62
+
63
+ any = proc { |x| x | (x << Maze::UNDER_SHIFT) }
64
+
65
+ if cell & any[s] != 0
66
+ r1, r2 = p5, move(p4, 0, options[:cell_padding]*2)
67
+ fill_rect(canvas, r1[0], r1[1], r2[0], r2[1], color_at(point, any[s]))
68
+ line(canvas, p5, move(p5, 0, options[:cell_padding]*2), options[:wall_color])
69
+ line(canvas, p4, move(p4, 0, options[:cell_padding]*2), options[:wall_color])
70
+ end
71
+
72
+ if cell & any[ne] != 0
73
+ ne_x = x + 3 * options[:cell_size] / 4.0
74
+ ne_y = y - options[:cell_size] * 0.5
75
+ ne_p5 = [ne_x + options[:cell_padding] + s4, ne_y + options[:cell_size] - options[:cell_padding]]
76
+ ne_p6 = [ne_x + options[:cell_padding], ne_y + options[:cell_size] * 0.5]
77
+ r1, r2, r3, r4 = p2, p3, ne_p5, ne_p6
78
+ fill_poly(canvas, [r1, r2, r3, r4], color_at(point, any[ne]))
79
+ line(canvas, r1, r4, options[:wall_color])
80
+ line(canvas, r2, r3, options[:wall_color])
81
+ end
82
+
83
+ if cell & any[se] != 0
84
+ se_x = x + 3 * options[:cell_size] / 4.0
85
+ se_y = y + options[:cell_size] * 0.5
86
+ se_p1 = [se_x + s4 + options[:cell_padding], se_y + options[:cell_padding]]
87
+ se_p6 = [se_x + options[:cell_padding], se_y + options[:cell_size] * 0.5]
88
+ r1, r2, r3, r4 = p3, p4, se_p6, se_p1
89
+ fill_poly(canvas, [r1, r2, r3, r4], color_at(point, any[se]))
90
+ line(canvas, r1, r4, options[:wall_color])
91
+ line(canvas, r2, r3, options[:wall_color])
92
+ end
93
+
94
+ line(canvas, p1, p2, options[:wall_color]) if cell & n == 0
95
+ line(canvas, p2, p3, options[:wall_color]) if cell & ne == 0
96
+ line(canvas, p3, p4, options[:wall_color]) if cell & se == 0
97
+ line(canvas, p4, p5, options[:wall_color]) if cell & s == 0
98
+ line(canvas, p5, p6, options[:wall_color]) if cell & sw == 0
99
+ line(canvas, p6, p1, options[:wall_color]) if cell & nw == 0
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+