git @ Cat's Eye Technologies Backtracking-Wang-Tiler / 76b1474
Initial import of docs and one implementation of Wang tiling. Chris Pressey 8 years ago
6 changed file(s) with 1105 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 };