theseus 1.0.0

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,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
+