Initial import of docs and one implementation of Wang tiling.
Chris Pressey
8 years ago
0 | Wang Tilers | |
1 | =========== | |
2 | ||
3 | This repository contains descriptions and implementations of algorithms | |
4 | that generate [Wang titlings][]. | |
5 | ||
6 | (In fact, it currently contains only one implementation of only one such | |
7 | algorithm, but it may, in the future, contain more.) | |
8 | ||
9 | You can read the linked article for more information, but in brief, a | |
10 | Wang tiling is a tiling of the plane by a set of tiles which, by their | |
11 | nature, tile the plane _aperiodically_ — the tiling pattern never quite | |
12 | repeats itself. | |
13 | ||
14 | Even more interestingly, each such aperiodic tiling corresponds to a Turing | |
15 | machine that does not halt. | |
16 | ||
17 | Backtracking Wang Tiler | |
18 | ----------------------- | |
19 | ||
20 | This is a naïve algorithm which works as follows: | |
21 | ||
22 | * Place a tile at the origin. | |
23 | * Place successive tiles in a spiral: from the origin, move right, then | |
24 | down, then left, then up, then right again etc., keeping always | |
25 | adjacent to tiles that have previously been placed. | |
26 | * Before placing a tile, compute all the allowable possibilities for | |
27 | tiles at that position. | |
28 | * If there are no allowable possibilities, backtrack: change the | |
29 | previously-placed tile to the next allowable possibility for it | |
30 | ("retry" it) and continue; and if there are no more allowable | |
31 | possibilities for that previous tile, delete it and retry the tile | |
32 | previous to it, and so forth. | |
33 | ||
34 | Given that Wang tiles do tile the plane (albeit aperiodically,) this will | |
35 | eventually generate a tiling. | |
36 | ||
37 | However, it is incredibly efficient, for the following reason: it is entirely | |
38 | possible that it will lay down a "wall" against which it is impossible to | |
39 | place any series of Wang tiles, because it contains an "impossible spot." | |
40 | While trying to place tiles against this wall, it will keep hitting the | |
41 | "impossible spot" and backtracking, trying possibility after possibility | |
42 | until it backtracks all the way around the spiral and finally replaces the | |
43 | "impossible spot" with something not-impossible-to-tile-against. | |
44 | ||
45 | It is, however, interesting to watch. | |
46 | ||
47 | This algorithm has been implemented in Javascript, using the [yoob.js][] | |
48 | framework, depicting the tiles on an HTML5 canvas. | |
49 | ||
50 | The implementation uses the 13 tiles given by Culik in 1996 (see article | |
51 | linked to above), but it would be quite easy to modify it to use any given | |
52 | set of tiles (even ones which tile periodically, for whatever it's worth.) | |
53 | ||
54 | Other possible algorithms | |
55 | ------------------------- | |
56 | ||
57 | ### Lookahead ### | |
58 | ||
59 | It should be possible to refine the backtracking algorithm by adding some | |
60 | lookahead, and ensuring early that no "impossible spots" are created. | |
61 | ||
62 | In a sense, if you could look ahead infinitely far, you wouldn't need to | |
63 | backtrack at all. Of course, that wouldn't be a practical algorithm; in | |
64 | fact, that strongly implies that you will always need backtracking (or | |
65 | something equivalent to it.) | |
66 | ||
67 | ### Bogo-tile ### | |
68 | ||
69 | It might be possible to use "reconciliation" instead of backtracking, in | |
70 | an algorithm something like: | |
71 | ||
72 | * Place the initial tile randomly. | |
73 | * Choose a random position adjacent to a placed tile. | |
74 | * Place a tile there. | |
75 | * If there are any tiles adjacent to it that don't match, delete them. | |
76 | * And if it is not possible to place a tile in that now-empty position, | |
77 | delete the tile you just placed. | |
78 | * Repeat from step 2. | |
79 | ||
80 | Once a cluster of tiles do fit together, they will only be deleted starting | |
81 | from the edges. But assuming fits are more likely than clashes (are they??), | |
82 | such clusters would tend to grow, rather than shrink, and eventually tile | |
83 | the plane. (Shrinking is the equivalent to backtracking in this scheme.) | |
84 | ||
85 | I expect this would be even less efficient than explicit backtracking. But | |
86 | it, too, might be interesting to watch. And it has the nice properties of | |
87 | being simpler and more "statistical". | |
88 | ||
89 | ### Turing machine translation ### | |
90 | ||
91 | This one should be obvious, since it's how Berger showed the domino problem | |
92 | is undecidable: since every Turing machine which does not halt corresponds | |
93 | to a possible Wang tiling, just pick a Turing machine which you know does | |
94 | not halt, and translate it into a tiling. | |
95 | ||
96 | Thing is, I haven't looked at Berger's paper, and don't know if his translation | |
97 | from TM's to tilings (and back) is constructive. If it isn't, well, there's | |
98 | no easy algorithm to be had. | |
99 | ||
100 | There is also the small complication that you'd have to prove that the Turing | |
101 | machine never halts, first. This isn't insurmountable; the Halting Problem | |
102 | is only undecidable in general, and given a particular TM, we can sometimes | |
103 | prove that it does not halt. But it does kind of place a practical limit on | |
104 | the number of tilings you could generate using this method. | |
105 | ||
106 | [Wang titlings]: http://en.wikipedia.org/wiki/Wang_tile | |
107 | [yoob.js]: http://catseye.tc/node/yoob.js |
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>Backtracking Wang Tiler</title> | |
4 | <style> | |
5 | canvas { | |
6 | border: 1px solid black; | |
7 | } | |
8 | </style> | |
9 | </head> | |
10 | <body> | |
11 | ||
12 | <h1>Backtracking Wang Tiler</h1> | |
13 | ||
14 | <div id="container"></div> | |
15 | ||
16 | </body> | |
17 | <script src="../src/backtracking-wang-tiler.js"></script> | |
18 | <script> | |
19 | launch('../src/yoob/', 'container'); | |
20 | </script> |
0 | "use strict"; | |
1 | ||
2 | function launch(prefix, containerId, config) { | |
3 | var config = config || {}; | |
4 | var deps = [ | |
5 | "element-factory.js", | |
6 | "playfield.js", | |
7 | "animation.js" | |
8 | ]; | |
9 | var loaded = 0; | |
10 | for (var i = 0; i < deps.length; i++) { | |
11 | var elem = document.createElement('script'); | |
12 | elem.src = prefix + deps[i]; | |
13 | elem.onload = function() { | |
14 | if (++loaded < deps.length) return; | |
15 | var container = document.getElementById(containerId); | |
16 | ||
17 | var tileSize = 40; | |
18 | var gridWidth = 11; // odd is nicer b/c first tile is in centre | |
19 | var gridHeight = 11; | |
20 | var canvas = yoob.makeCanvas( | |
21 | container, tileSize * gridWidth, tileSize * gridHeight | |
22 | ); | |
23 | ||
24 | (new WangTiler()).init({ | |
25 | 'canvas': canvas, | |
26 | 'tileSize': tileSize, | |
27 | 'gridWidth': gridWidth, | |
28 | 'gridHeight': gridHeight | |
29 | }); | |
30 | }; | |
31 | document.body.appendChild(elem); | |
32 | } | |
33 | } | |
34 | ||
35 | // Clockwise starting from top. | |
36 | ||
37 | var NORTH = 0; | |
38 | var EAST = 1; | |
39 | var SOUTH = 2; | |
40 | var WEST = 3; | |
41 | ||
42 | var DELTA = [ | |
43 | [0, -1], | |
44 | [1, 0], | |
45 | [0, 1], | |
46 | [-1, 0] | |
47 | ]; | |
48 | ||
49 | // R = Red, G = Green, B = Blue, Y = Yellow, K = Grey | |
50 | ||
51 | var TILES = [ | |
52 | "GGBR", // 0 | |
53 | "GBGR", | |
54 | "GBBG", | |
55 | "RRGG", | |
56 | "RRBB", | |
57 | "RGGB", | |
58 | "YYRY", | |
59 | "BYGY", | |
60 | "GKRY", | |
61 | "GKYY", | |
62 | "YKRK", | |
63 | "BKGK", | |
64 | "GYGK", // 12 | |
65 | ]; | |
66 | ||
67 | var COLOURS = { | |
68 | 'R': '#ff0000', | |
69 | 'G': '#00ff00', | |
70 | 'Y': '#ffff00', | |
71 | 'B': '#0000ff', | |
72 | 'K': '#808080' | |
73 | }; | |
74 | ||
75 | var YES = function(t) { | |
76 | return true; | |
77 | }; | |
78 | ||
79 | var Tile = function() { | |
80 | this.init = function(cfg) { | |
81 | this.x = cfg.x; // grid position in playfield | |
82 | this.y = cfg.y; // (mostly for backtracking) | |
83 | this.possibilities = cfg.possibilities; // list of indices into TILES | |
84 | this.prev = cfg.prev; // Tile | null | |
85 | this.choice = 0; // index into possibilities | |
86 | this.type = this.possibilities[this.choice]; | |
87 | return this; | |
88 | }; | |
89 | ||
90 | /* | |
91 | * Try the next possibility for this Tile (used during | |
92 | * backtracking.) Returns false if the possibilities | |
93 | * for this Tile have been exhausted. | |
94 | */ | |
95 | this.retry = function() { | |
96 | this.choice++; | |
97 | if (this.choice >= this.possibilities.length) { | |
98 | return false; | |
99 | } | |
100 | this.type = this.possibilities[this.choice]; | |
101 | return true; | |
102 | }; | |
103 | ||
104 | /* | |
105 | * Draw this Tile in a drawing context. | |
106 | * (x, y) is the top-left corner, given in context-units. | |
107 | */ | |
108 | this.draw = function(ctx, x, y, w, h) { | |
109 | var td = TILES[this.type]; | |
110 | ||
111 | /* | |
112 | * If there're going to be hairline cracks between the | |
113 | * triangles anyway, they look better in black. | |
114 | */ | |
115 | ctx.fillStyle = 'black'; | |
116 | ctx.fillRect(x, y, w, h); | |
117 | ||
118 | ctx.beginPath(); | |
119 | ctx.fillStyle = COLOURS[td.charAt(NORTH)]; | |
120 | ctx.moveTo(x, y); | |
121 | ctx.lineTo(x + w/2, y + h/2); | |
122 | ctx.lineTo(x + w, y); | |
123 | ctx.closePath(); | |
124 | ctx.fill(); | |
125 | ||
126 | ctx.beginPath(); | |
127 | ctx.fillStyle = COLOURS[td.charAt(EAST)]; | |
128 | ctx.moveTo(x + w, y); | |
129 | ctx.lineTo(x + w/2, y + h/2); | |
130 | ctx.lineTo(x + w, y + h); | |
131 | ctx.closePath(); | |
132 | ctx.fill(); | |
133 | ||
134 | ctx.beginPath(); | |
135 | ctx.fillStyle = COLOURS[td.charAt(SOUTH)]; | |
136 | ctx.moveTo(x + w, y + h); | |
137 | ctx.lineTo(x + w/2, y + h/2); | |
138 | ctx.lineTo(x, y + h); | |
139 | ctx.closePath(); | |
140 | ctx.fill(); | |
141 | ||
142 | ctx.beginPath(); | |
143 | ctx.fillStyle = COLOURS[td.charAt(WEST)]; | |
144 | ctx.moveTo(x, y + h); | |
145 | ctx.lineTo(x + w/2, y + h/2); | |
146 | ctx.lineTo(x, y); | |
147 | ctx.closePath(); | |
148 | ctx.fill(); | |
149 | ||
150 | ctx.strokeStyle = 'black'; | |
151 | ctx.lineWidth = 0.5; | |
152 | ctx.strokeRect(x, y, w, h); | |
153 | }; | |
154 | }; | |
155 | ||
156 | var WangTiler = function() { | |
157 | var canvas; | |
158 | var ctx; | |
159 | ||
160 | var tileSize; | |
161 | var pf; | |
162 | var t; | |
163 | var gridW, gridH; | |
164 | var cx, cy; | |
165 | var wait; | |
166 | ||
167 | /* | |
168 | * Find the set of possibilities that fit into cell (x, y) | |
169 | * in the playfield, and returns it. | |
170 | */ | |
171 | this.possibilitiesForTile = function(x, y) { | |
172 | var n = pf.get(x, y - 1); | |
173 | var e = pf.get(x + 1, y); | |
174 | var s = pf.get(x, y + 1); | |
175 | var w = pf.get(x - 1, y); | |
176 | ||
177 | var predN = n ? function(t) { | |
178 | return t.charAt(NORTH) === TILES[n.type].charAt(SOUTH); | |
179 | } : YES; | |
180 | var predE = e ? function(t) { | |
181 | return t.charAt(EAST) === TILES[e.type].charAt(WEST); | |
182 | } : YES; | |
183 | var predS = s ? function(t) { | |
184 | return t.charAt(SOUTH) === TILES[s.type].charAt(NORTH); | |
185 | } : YES; | |
186 | var predW = w ? function(t) { | |
187 | return t.charAt(WEST) === TILES[w.type].charAt(EAST); | |
188 | } : YES; | |
189 | ||
190 | var possibilities = []; | |
191 | for (var i = 0; i < TILES.length; i++) { | |
192 | var t = TILES[i]; | |
193 | if (predN(t) && predE(t) && predS(t) && predW(t)) { | |
194 | possibilities.push(i); | |
195 | } | |
196 | } | |
197 | ||
198 | return possibilities; | |
199 | }; | |
200 | ||
201 | /* | |
202 | * Assuming the last-placed tile is at (x, y), determine where the | |
203 | * next-placed tile shall be. | |
204 | */ | |
205 | this.moveAlong = function(x, y) { | |
206 | // find tile's neighbours | |
207 | var n = pf.get(x, y - 1); | |
208 | var ne = pf.get(x + 1, y - 1); | |
209 | var e = pf.get(x + 1, y); | |
210 | var se = pf.get(x + 1, y + 1); | |
211 | var s = pf.get(x, y + 1); | |
212 | var sw = pf.get(x - 1, y + 1); | |
213 | var w = pf.get(x - 1, y); | |
214 | var nw = pf.get(x - 1, y - 1); | |
215 | ||
216 | // pick a direction | |
217 | var dx, dy; | |
218 | ||
219 | // if there is a tile to the E, or if there is a tile to the NE but not the N, go N. | |
220 | // if there is a tile to the S, or if there is a tile to the SE but not the E, go E. | |
221 | // if there is a tile to the W, or if there is a tile to the SW but not the S, go S. | |
222 | // if there is a tile to the N, or if there is a tile to the NW but not the W, go W. | |
223 | // else just go east and we'll figure out the rest later. | |
224 | if ((e && !n) || (ne && !n)) { | |
225 | dx = 0; | |
226 | dy = -1; | |
227 | } else if ((s && !e) || (se && !e)) { | |
228 | dx = 1; | |
229 | dy = 0; | |
230 | } else if ((w && !s) || (se && !s)) { | |
231 | dx = 0; | |
232 | dy = 1; | |
233 | } else if ((n && !w) || (nw && !w)) { | |
234 | dx = -1; | |
235 | dy = 0; | |
236 | } else { | |
237 | dx = 1; | |
238 | dy = 0; | |
239 | } | |
240 | ||
241 | if (pf.get(x + dx, y + dy)) { | |
242 | alert("We messed up navigation somehow!") | |
243 | } | |
244 | ||
245 | return [dx, dy]; | |
246 | }; | |
247 | ||
248 | /* | |
249 | * Update the state of the tiling -- move to the next position and | |
250 | * place a new Tile. If that turns out to not be possible, backtrack. | |
251 | */ | |
252 | this.update = function() { | |
253 | var tile = pf.get(cx, cy); | |
254 | ||
255 | var delta = this.moveAlong(cx, cy); | |
256 | var nx = cx + delta[0]; | |
257 | var ny = cy + delta[1]; | |
258 | ||
259 | var possibilities = this.possibilitiesForTile(nx, ny); | |
260 | if (possibilities.length > 0) { | |
261 | ||
262 | if (true) { | |
263 | var newPossibilities = []; | |
264 | while (possibilities.length > 0) { | |
265 | newPossibilities.push( | |
266 | possibilities.splice( | |
267 | Math.random() * possibilities.length, 1 | |
268 | )[0] | |
269 | ); | |
270 | } | |
271 | possibilities = newPossibilities; | |
272 | } | |
273 | ||
274 | var newTile = (new Tile()).init({ | |
275 | 'x': nx, | |
276 | 'y': ny, | |
277 | 'possibilities': possibilities, | |
278 | 'prev': tile | |
279 | }); | |
280 | cx = nx; | |
281 | cy = ny; | |
282 | pf.put(cx, cy, newTile); | |
283 | } else { | |
284 | // backtrack | |
285 | var done = false; | |
286 | while (!done) { | |
287 | if (tile.retry()) { | |
288 | done = true; | |
289 | } else { | |
290 | // remove tile from playfield, and backtrack further | |
291 | pf.put(tile.x, tile.y, null); | |
292 | tile = tile.prev; | |
293 | cx = tile.x; | |
294 | cy = tile.y; | |
295 | } | |
296 | } | |
297 | } | |
298 | }; | |
299 | ||
300 | /* | |
301 | * Draw the current state of the tiling on the canvas. | |
302 | */ | |
303 | this.draw = function() { | |
304 | ctx.clearRect(0, 0, canvas.width, canvas.height); | |
305 | pf.foreach(function(x, y, tile) { | |
306 | tile.draw(ctx, x * tileSize, y * tileSize, tileSize, tileSize); | |
307 | }); | |
308 | }; | |
309 | ||
310 | this.init = function(cfg) { | |
311 | canvas = cfg.canvas; | |
312 | ctx = canvas.getContext('2d'); | |
313 | ||
314 | tileSize = cfg.tileSize || 20; | |
315 | pf = (new yoob.Playfield()).setDefault(null); | |
316 | wait = 0; | |
317 | ||
318 | gridW = cfg.gridWidth || 20; | |
319 | gridH = cfg.gridHeight || 20; | |
320 | cx = Math.trunc(gridW / 2); | |
321 | cy = Math.trunc(gridH / 2); | |
322 | ||
323 | var possibilities = this.possibilitiesForTile(cx, cy); | |
324 | if (possibilities.length === 0) { | |
325 | alert('wat'); | |
326 | } | |
327 | var initTile = (new Tile()).init({ | |
328 | 'x': cx, | |
329 | 'y': cy, | |
330 | 'possibilities': possibilities | |
331 | }); | |
332 | pf.put(cx, cy, initTile); | |
333 | ||
334 | this.animation = (new yoob.Animation()).init({ | |
335 | 'object': this | |
336 | }); | |
337 | this.animation.start(); | |
338 | }; | |
339 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.7 | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * Pretty standard shim to get window.{request,cancelRequest}AnimationFrame | |
9 | * functions, synthesized from the theory and the many examples I've seen. | |
10 | */ | |
11 | ||
12 | window.requestAnimationFrame = | |
13 | window.requestAnimationFrame || | |
14 | window.webkitRequestAnimationFrame || | |
15 | window.mozRequestAnimationFrame || | |
16 | window.oRequestAnimationFrame || | |
17 | window.msRequestAnimationFrame || | |
18 | function(f, elem) { | |
19 | return setTimeout(function() { | |
20 | f(Date.now()); | |
21 | }, 1000 / 60); | |
22 | }; | |
23 | ||
24 | // it was called "cancelRequestAnimationFrame" in the editor's draft: | |
25 | // http://webstuff.nfshost.com/anim-timing/Overview.html | |
26 | // but "cancelAnimationFrame" in the Candidate Recommendation: | |
27 | // http://www.w3.org/TR/animation-timing/ | |
28 | window.cancelAnimationFrame = | |
29 | window.cancelAnimationFrame || | |
30 | window.webkitCancelAnimationFrame || | |
31 | window.mozCancelAnimationFrame || | |
32 | window.oCancelAnimationFrame || | |
33 | window.msCancelAnimationFrame || | |
34 | window.cancelRequestAnimationFrame || | |
35 | window.webkitCancelRequestAnimationFrame || | |
36 | window.mozCancelRequestAnimationFrame || | |
37 | window.oCancelRequestAnimationFrame || | |
38 | window.msCancelRequestAnimationFrame || | |
39 | clearTimeout; | |
40 | window.cancelRequestAnimationFrame = window.cancelAnimationFrame; | |
41 | ||
42 | /* | |
43 | * A yoob.Animation object manages an animation. | |
44 | * | |
45 | * How many things get animated by one yoob.Animation object is up to | |
46 | * you. For animated demos, it may be sufficient to have only one | |
47 | * Animation object which updates many independent graphical objects. | |
48 | * However, it may be useful to have multiple Animation objects, if | |
49 | * the program can be in different states (for example, one title screen | |
50 | * Animation, and a different Animation to use during gameplay.) | |
51 | */ | |
52 | yoob.Animation = function() { | |
53 | /* | |
54 | * Initialize a yoob.Animation. Takes a configuration dictionary: | |
55 | * | |
56 | * mode 'quantum' (default) or 'proportional' | |
57 | * object the object to call methods on | |
58 | * tickTime in msec. for quantum only. default = 1/60th sec | |
59 | * lastTime internal (but see below) | |
60 | * accumDelta internal, only used in quantum mode | |
61 | * | |
62 | * There are two modes that a yoob.Animation can be in, | |
63 | * 'quantum' (the default) and 'proportional'. | |
64 | * | |
65 | * Once the Animation has been started (by calling animation.start()): | |
66 | * | |
67 | * In the 'quantum' mode, the object's draw() method is called on | |
68 | * each animation frame, and the object's update() method is called as | |
69 | * many times as necessary to ensure it is called once for every tickTime | |
70 | * milliseconds that have passed. Neither method is passed any | |
71 | * parameters. | |
72 | * | |
73 | * update() (or draw(), in 'proportional' mode only) may return the | |
74 | * exact object 'false' to force the animation to stop immediately. | |
75 | * | |
76 | * In the 'proportional' mode, the object's draw() method is called on | |
77 | * each animation frame, and the amount of time (in milliseconds) that has | |
78 | * elapsed since the last time it was called (or 0 if it was never | |
79 | * previously called) is passed as the first and only parameter. | |
80 | */ | |
81 | this.init = function(cfg) { | |
82 | this.object = cfg.object; | |
83 | this.lastTime = cfg.lastTime || null; | |
84 | this.accumDelta = cfg.accumDelta || 0; | |
85 | this.tickTime = cfg.tickTime || (1000.0 / 60.0); | |
86 | this.mode = cfg.mode || 'quantum'; | |
87 | this.request = null; | |
88 | return this; | |
89 | }; | |
90 | ||
91 | this.start = function() { | |
92 | if (this.request) { | |
93 | return false; | |
94 | } | |
95 | var $this = this; | |
96 | if (this.mode === 'quantum') { | |
97 | var animFrame = function(time) { | |
98 | $this.object.draw(); | |
99 | if ($this.lastTime === null) { | |
100 | $this.lastTime = time; | |
101 | } | |
102 | $this.accumDelta += (time - $this.lastTime); | |
103 | while ($this.accumDelta > $this.tickTime) { | |
104 | $this.accumDelta -= $this.tickTime; | |
105 | var result = $this.object.update(); | |
106 | if (result === false) { | |
107 | $this.accumDelta = $this.tickTime; | |
108 | $this.request = null; | |
109 | } | |
110 | } | |
111 | $this.lastTime = time; | |
112 | if ($this.request) { | |
113 | $this.request = requestAnimationFrame(animFrame); | |
114 | } | |
115 | }; | |
116 | } else if (this.mode === 'proportional') { | |
117 | var animFrame = function(time) { | |
118 | var timeElapsed = ( | |
119 | $this.lastTime == null ? 0 : time - $this.lastTime | |
120 | ); | |
121 | $this.lastTime = time; | |
122 | var result = $this.object.draw(timeElapsed); | |
123 | if (result === false) { | |
124 | $this.request = null; | |
125 | } | |
126 | if ($this.request) { | |
127 | $this.request = requestAnimationFrame(animFrame); | |
128 | } | |
129 | }; | |
130 | } | |
131 | this.request = requestAnimationFrame(animFrame); | |
132 | return true; | |
133 | }; | |
134 | ||
135 | this.stop = function() { | |
136 | if (this.request) { | |
137 | cancelRequestAnimationFrame(this.request); | |
138 | } | |
139 | this.request = null; | |
140 | }; | |
141 | ||
142 | this.reset = function() { | |
143 | this.stop(); | |
144 | this.lastTime = null; | |
145 | }; | |
146 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.8-PRE | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * Functions for creating elements. | |
9 | */ | |
10 | ||
11 | yoob.makeCanvas = function(container, width, height) { | |
12 | var canvas = document.createElement('canvas'); | |
13 | if (width) { | |
14 | canvas.width = width; | |
15 | } | |
16 | if (height) { | |
17 | canvas.height = height; | |
18 | } | |
19 | container.appendChild(canvas); | |
20 | return canvas; | |
21 | }; | |
22 | ||
23 | yoob.makeButton = function(container, labelText, fun) { | |
24 | var button = document.createElement('button'); | |
25 | button.innerHTML = labelText; | |
26 | container.appendChild(button); | |
27 | if (fun) { | |
28 | button.onclick = fun; | |
29 | } | |
30 | return button; | |
31 | }; | |
32 | ||
33 | yoob.checkBoxNumber = 0; | |
34 | yoob.makeCheckbox = function(container, checked, labelText, fun) { | |
35 | var checkbox = document.createElement('input'); | |
36 | checkbox.type = "checkbox"; | |
37 | checkbox.id = 'cfzzzb_' + yoob.checkBoxNumber; | |
38 | checkbox.checked = checked; | |
39 | var label = document.createElement('label'); | |
40 | label.htmlFor = 'cfzzzb_' + yoob.checkBoxNumber; | |
41 | yoob.checkBoxNumber += 1; | |
42 | label.appendChild(document.createTextNode(labelText)); | |
43 | ||
44 | container.appendChild(checkbox); | |
45 | container.appendChild(label); | |
46 | ||
47 | if (fun) { | |
48 | checkbox.onchange = function(e) { | |
49 | fun(checkbox.checked); | |
50 | }; | |
51 | } | |
52 | return checkbox; | |
53 | }; | |
54 | ||
55 | yoob.makeTextInput = function(container, size, value) { | |
56 | var input = document.createElement('input'); | |
57 | input.size = "" + (size || 12); | |
58 | input.value = value || ""; | |
59 | container.appendChild(input); | |
60 | return input; | |
61 | }; | |
62 | ||
63 | yoob.makeSlider = function(container, min, max, value, fun) { | |
64 | var slider = document.createElement('input'); | |
65 | slider.type = "range"; | |
66 | slider.min = min; | |
67 | slider.max = max; | |
68 | slider.value = value || 0; | |
69 | if (fun) { | |
70 | slider.onchange = function(e) { | |
71 | fun(parseInt(slider.value, 10)); | |
72 | }; | |
73 | } | |
74 | container.appendChild(slider); | |
75 | return slider; | |
76 | }; | |
77 | ||
78 | yoob.makeParagraph = function(container, innerHTML) { | |
79 | var p = document.createElement('p'); | |
80 | p.innerHTML = innerHTML || ''; | |
81 | container.appendChild(p); | |
82 | return p; | |
83 | }; | |
84 | ||
85 | yoob.makeSpan = function(container, innerHTML) { | |
86 | var span = document.createElement('span'); | |
87 | span.innerHTML = innerHTML || ''; | |
88 | container.appendChild(span); | |
89 | return span; | |
90 | }; | |
91 | ||
92 | yoob.makeDiv = function(container, innerHTML) { | |
93 | var div = document.createElement('div'); | |
94 | div.innerHTML = innerHTML || ''; | |
95 | container.appendChild(div); | |
96 | return div; | |
97 | }; | |
98 | ||
99 | yoob.makePre = function(container, innerHTML) { | |
100 | var elem = document.createElement('pre'); | |
101 | elem.innerHTML = innerHTML || ''; | |
102 | container.appendChild(elem); | |
103 | return elem; | |
104 | }; | |
105 | ||
106 | yoob.makePanel = function(container, title, isOpen) { | |
107 | isOpen = !!isOpen; | |
108 | var panelContainer = document.createElement('div'); | |
109 | var button = document.createElement('button'); | |
110 | var innerContainer = document.createElement('div'); | |
111 | innerContainer.style.display = isOpen ? "block" : "none"; | |
112 | ||
113 | button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title; | |
114 | button.onclick = function(e) { | |
115 | isOpen = !isOpen; | |
116 | button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title; | |
117 | innerContainer.style.display = isOpen ? "block" : "none"; | |
118 | }; | |
119 | ||
120 | panelContainer.appendChild(button); | |
121 | panelContainer.appendChild(innerContainer); | |
122 | container.appendChild(panelContainer); | |
123 | return innerContainer; | |
124 | }; | |
125 | ||
126 | yoob.makeTextArea = function(container, cols, rows, initial) { | |
127 | var textarea = document.createElement('textarea'); | |
128 | textarea.rows = "" + rows; | |
129 | textarea.cols = "" + cols; | |
130 | if (initial) { | |
131 | container.value = initial; | |
132 | } | |
133 | container.appendChild(textarea); | |
134 | return textarea; | |
135 | }; | |
136 | ||
137 | yoob.makeLineBreak = function(container) { | |
138 | var br = document.createElement('br'); | |
139 | container.appendChild(br); | |
140 | return br; | |
141 | }; | |
142 | ||
143 | yoob.makeSelect = function(container, labelText, optionsArray) { | |
144 | var label = document.createElement('label'); | |
145 | label.innerHTML = labelText; | |
146 | container.appendChild(label); | |
147 | ||
148 | var select = document.createElement("select"); | |
149 | ||
150 | for (var i = 0; i < optionsArray.length; i++) { | |
151 | var op = document.createElement("option"); | |
152 | op.value = optionsArray[i][0]; | |
153 | op.text = optionsArray[i][1]; | |
154 | if (optionsArray[i].length > 2) { | |
155 | op.selected = optionsArray[i][2]; | |
156 | } else { | |
157 | op.selected = false; | |
158 | } | |
159 | select.options.add(op); | |
160 | } | |
161 | ||
162 | container.appendChild(select); | |
163 | return select; | |
164 | }; | |
165 | ||
166 | SliderPlusTextInput = function() { | |
167 | this.init = function(cfg) { | |
168 | this.slider = cfg.slider; | |
169 | this.textInput = cfg.textInput; | |
170 | this.callback = cfg.callback; | |
171 | return this; | |
172 | }; | |
173 | ||
174 | this.set = function(value) { | |
175 | this.slider.value = "" + value; | |
176 | this.textInput.value = "" + value; | |
177 | this.callback(value); | |
178 | }; | |
179 | }; | |
180 | ||
181 | yoob.makeSliderPlusTextInput = function(container, label, min_, max_, size, value, fun) { | |
182 | yoob.makeSpan(container, label); | |
183 | var slider = yoob.makeSlider(container, min_, max_, value); | |
184 | var s = "" + value; | |
185 | var textInput = yoob.makeTextInput(container, size, s); | |
186 | slider.onchange = function(e) { | |
187 | textInput.value = slider.value; | |
188 | fun(parseInt(slider.value, 10)); | |
189 | }; | |
190 | textInput.onchange = function(e) { | |
191 | var v = parseInt(textInput.value, 10); | |
192 | if (v !== NaN) { | |
193 | slider.value = "" + v; | |
194 | fun(v); | |
195 | } | |
196 | }; | |
197 | return new SliderPlusTextInput().init({ | |
198 | 'slider': slider, | |
199 | 'textInput': textInput, | |
200 | 'callback': fun | |
201 | }); | |
202 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.6 | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * A two-dimensional Cartesian grid of values. | |
9 | */ | |
10 | yoob.Playfield = function() { | |
11 | this._store = {}; | |
12 | this.minX = undefined; | |
13 | this.minY = undefined; | |
14 | this.maxX = undefined; | |
15 | this.maxY = undefined; | |
16 | this._default = undefined; | |
17 | ||
18 | /* | |
19 | * Set the default value for this Playfield. This | |
20 | * value is returned by get() for any cell that was | |
21 | * never written to, or had `undefined` put() into it. | |
22 | */ | |
23 | this.setDefault = function(v) { | |
24 | this._default = v; | |
25 | return this; | |
26 | }; | |
27 | ||
28 | /* | |
29 | * Obtain the value at (x, y). The default value will | |
30 | * be returned if the cell was never written to. | |
31 | */ | |
32 | this.get = function(x, y) { | |
33 | var v = this._store[x+','+y]; | |
34 | if (v === undefined) return this._default; | |
35 | return v; | |
36 | }; | |
37 | ||
38 | /* | |
39 | * Write a new value into (x, y). Note that writing | |
40 | * `undefined` into a cell has the semantics of deleting | |
41 | * the value at that cell; a subsequent get() for that | |
42 | * location will return this Playfield's default value. | |
43 | */ | |
44 | this.put = function(x, y, value) { | |
45 | var key = x+','+y; | |
46 | if (value === undefined || value === this._default) { | |
47 | delete this._store[key]; | |
48 | return; | |
49 | } | |
50 | if (this.minX === undefined || x < this.minX) this.minX = x; | |
51 | if (this.maxX === undefined || x > this.maxX) this.maxX = x; | |
52 | if (this.minY === undefined || y < this.minY) this.minY = y; | |
53 | if (this.maxY === undefined || y > this.maxY) this.maxY = y; | |
54 | this._store[key] = value; | |
55 | }; | |
56 | ||
57 | /* | |
58 | * Like put(), but does not update the playfield bounds. Do | |
59 | * this if you must do a batch of put()s in a more efficient | |
60 | * manner; after doing so, call recalculateBounds(). | |
61 | */ | |
62 | this.putDirty = function(x, y, value) { | |
63 | var key = x+','+y; | |
64 | if (value === undefined || value === this._default) { | |
65 | delete this._store[key]; | |
66 | return; | |
67 | } | |
68 | this._store[key] = value; | |
69 | }; | |
70 | ||
71 | /* | |
72 | * Recalculate the bounds (min/max X/Y) which are tracked | |
73 | * internally to support methods like foreach(). This is | |
74 | * not needed *unless* you've used putDirty() at some point. | |
75 | * (In which case, call this immediately after your batch | |
76 | * of putDirty()s.) | |
77 | */ | |
78 | this.recalculateBounds = function() { | |
79 | this.minX = undefined; | |
80 | this.minY = undefined; | |
81 | this.maxX = undefined; | |
82 | this.maxY = undefined; | |
83 | ||
84 | for (var cell in this._store) { | |
85 | var pos = cell.split(','); | |
86 | var x = parseInt(pos[0], 10); | |
87 | var y = parseInt(pos[1], 10); | |
88 | if (this.minX === undefined || x < this.minX) this.minX = x; | |
89 | if (this.maxX === undefined || x > this.maxX) this.maxX = x; | |
90 | if (this.minY === undefined || y < this.minY) this.minY = y; | |
91 | if (this.maxY === undefined || y > this.maxY) this.maxY = y; | |
92 | } | |
93 | }; | |
94 | ||
95 | /* | |
96 | * Clear the contents of this Playfield. | |
97 | */ | |
98 | this.clear = function() { | |
99 | this._store = {}; | |
100 | this.minX = undefined; | |
101 | this.minY = undefined; | |
102 | this.maxX = undefined; | |
103 | this.maxY = undefined; | |
104 | }; | |
105 | ||
106 | /* | |
107 | * Scroll a rectangular subrectangle of this Playfield, up. | |
108 | * TODO: support other directions. | |
109 | */ | |
110 | this.scrollRectangleY = function(dy, minX, minY, maxX, maxY) { | |
111 | if (dy < 1) { | |
112 | for (var y = minY; y <= (maxY + dy); y++) { | |
113 | for (var x = minX; x <= maxX; x++) { | |
114 | this.put(x, y, this.get(x, y - dy)); | |
115 | } | |
116 | } | |
117 | } else { alert("scrollRectangleY(" + dy + ") notImplemented"); } | |
118 | }; | |
119 | ||
120 | this.clearRectangle = function(minX, minY, maxX, maxY) { | |
121 | // Could also do this with a foreach that checks | |
122 | // each position. Would be faster on sparser playfields. | |
123 | for (var y = minY; y <= maxY; y++) { | |
124 | for (var x = minX; x <= maxX; x++) { | |
125 | this.put(x, y, undefined); | |
126 | } | |
127 | } | |
128 | }; | |
129 | ||
130 | /* | |
131 | * Load a string into this Playfield. | |
132 | * The string may be multiline, with newline (ASCII 10) | |
133 | * characters delimiting lines. ASCII 13 is ignored. | |
134 | * | |
135 | * If transformer is given, it should be a one-argument | |
136 | * function which accepts a character and returns the | |
137 | * object you wish to write into the playfield upon reading | |
138 | * that character. | |
139 | */ | |
140 | this.load = function(x, y, string, transformer) { | |
141 | var lx = x; | |
142 | var ly = y; | |
143 | if (transformer === undefined) { | |
144 | transformer = function(c) { | |
145 | if (c === ' ') { | |
146 | return undefined; | |
147 | } else { | |
148 | return c; | |
149 | } | |
150 | } | |
151 | } | |
152 | for (var i = 0; i < string.length; i++) { | |
153 | var c = string.charAt(i); | |
154 | if (c === '\n') { | |
155 | lx = x; | |
156 | ly++; | |
157 | } else if (c === '\r') { | |
158 | } else { | |
159 | this.putDirty(lx, ly, transformer(c)); | |
160 | lx++; | |
161 | } | |
162 | } | |
163 | this.recalculateBounds(); | |
164 | }; | |
165 | ||
166 | /* | |
167 | * Convert this Playfield to a multi-line string. Each row | |
168 | * is a line, delimited with a newline (ASCII 10). | |
169 | * | |
170 | * If transformer is given, it should be a one-argument | |
171 | * function which accepts a playfield element and returns a | |
172 | * character (or string) you wish to place in the resulting | |
173 | * string for that element. | |
174 | */ | |
175 | this.dump = function(transformer) { | |
176 | var text = ""; | |
177 | if (transformer === undefined) { | |
178 | transformer = function(c) { return c; } | |
179 | } | |
180 | for (var y = this.minY; y <= this.maxY; y++) { | |
181 | var row = ""; | |
182 | for (var x = this.minX; x <= this.maxX; x++) { | |
183 | row += transformer(this.get(x, y)); | |
184 | } | |
185 | text += row + "\n"; | |
186 | } | |
187 | return text; | |
188 | }; | |
189 | ||
190 | /* | |
191 | * Iterate over every defined cell in the Playfield. | |
192 | * fun is a callback which takes three parameters: | |
193 | * x, y, and value. If this callback returns a value, | |
194 | * it is written into the Playfield at that position. | |
195 | * This function ensures a particular order. | |
196 | */ | |
197 | this.foreach = function(fun) { | |
198 | for (var y = this.minY; y <= this.maxY; y++) { | |
199 | for (var x = this.minX; x <= this.maxX; x++) { | |
200 | var key = x+','+y; | |
201 | var value = this._store[key]; | |
202 | if (value === undefined) | |
203 | continue; | |
204 | var result = fun(x, y, value); | |
205 | if (result !== undefined) { | |
206 | if (result === ' ') { | |
207 | result = undefined; | |
208 | } | |
209 | this.put(x, y, result); | |
210 | } | |
211 | } | |
212 | } | |
213 | }; | |
214 | ||
215 | /* | |
216 | * Analogous to (monoid) map in functional languages, | |
217 | * iterate over this Playfield, transform each value using | |
218 | * a supplied function, and write the transformed value into | |
219 | * a destination Playfield. | |
220 | * | |
221 | * Supplied function should take a Playfield (this Playfield), | |
222 | * x, and y, and return a value. | |
223 | * | |
224 | * The map source may extend beyond the internal bounds of | |
225 | * the Playfield, by giving the min/max Dx/Dy arguments | |
226 | * (which work like margin offsets.) | |
227 | * | |
228 | * Useful for evolving a cellular automaton playfield. In this | |
229 | * case, min/max Dx/Dy should be computed from the neighbourhood. | |
230 | */ | |
231 | this.map = function(destPf, fun, minDx, minDy, maxDx, maxDy) { | |
232 | if (minDx === undefined) minDx = 0; | |
233 | if (minDy === undefined) minDy = 0; | |
234 | if (maxDx === undefined) maxDx = 0; | |
235 | if (maxDy === undefined) maxDy = 0; | |
236 | for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) { | |
237 | for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) { | |
238 | destPf.putDirty(x, y, fun(pf, x, y)); | |
239 | } | |
240 | } | |
241 | destPf.recalculateBounds(); | |
242 | }; | |
243 | ||
244 | /* | |
245 | * Accessors for the minimum (resp. maximum) x (resp. y) values of | |
246 | * occupied (non-default-valued) cells in this Playfield. If there are | |
247 | * no cells in this Playfield, these will refturn undefined. Note that | |
248 | * these are not guaranteed to be tight bounds; if values in cells | |
249 | * are deleted, these bounds may still be considered to be outside them. | |
250 | */ | |
251 | this.getMinX = function() { | |
252 | return this.minX; | |
253 | }; | |
254 | this.getMaxX = function() { | |
255 | return this.maxX; | |
256 | }; | |
257 | this.getMinY = function() { | |
258 | return this.minY; | |
259 | }; | |
260 | this.getMaxY = function() { | |
261 | return this.maxY; | |
262 | }; | |
263 | ||
264 | /* | |
265 | * Returns the number of occupied cells in the x direction. | |
266 | */ | |
267 | this.getExtentX = function() { | |
268 | if (this.maxX === undefined || this.minX === undefined) { | |
269 | return 0; | |
270 | } else { | |
271 | return this.maxX - this.minX + 1; | |
272 | } | |
273 | }; | |
274 | ||
275 | /* | |
276 | * Returns the number of occupied cells in the y direction. | |
277 | */ | |
278 | this.getExtentY = function() { | |
279 | if (this.maxY === undefined || this.minY === undefined) { | |
280 | return 0; | |
281 | } else { | |
282 | return this.maxY - this.minY + 1; | |
283 | } | |
284 | }; | |
285 | }; |