about summary refs log tree commit diff stats
path: root/app/assets/javascripts/trace2.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/trace2.js')
-rw-r--r--app/assets/javascripts/trace2.js1055
1 files changed, 1055 insertions, 0 deletions
diff --git a/app/assets/javascripts/trace2.js b/app/assets/javascripts/trace2.js new file mode 100644 index 0000000..000e60b --- /dev/null +++ b/app/assets/javascripts/trace2.js
@@ -0,0 +1,1055 @@
1namespace(function() {
2
3var BBOX_DEBUG = false
4
5function clamp(value, min, max) {
6 return value < min ? min : value > max ? max : value
7}
8
9class BoundingBox {
10 constructor(x1, x2, y1, y2, sym=false) {
11 this.raw = {'x1':x1, 'x2':x2, 'y1':y1, 'y2':y2}
12 this.sym = sym
13 if (BBOX_DEBUG === true) {
14 this.debug = createElement('rect')
15 data.svg.appendChild(this.debug)
16 this.debug.setAttribute('opacity', 0.5)
17 this.debug.setAttribute('style', 'pointer-events: none;')
18 if (data.puzzle.symType == SYM_TYPE_NONE) {
19 this.debug.setAttribute('fill', 'white')
20 } else {
21 if (this.sym !== true) {
22 this.debug.setAttribute('fill', 'blue')
23 } else {
24 this.debug.setAttribute('fill', 'orange')
25 }
26 }
27 }
28 this._update()
29 }
30
31 shift(dir, pixels) {
32 if (dir === 'left') {
33 this.raw.x2 = this.raw.x1
34 this.raw.x1 -= pixels
35 } else if (dir === 'right') {
36 this.raw.x1 = this.raw.x2
37 this.raw.x2 += pixels
38 } else if (dir === 'top') {
39 this.raw.y2 = this.raw.y1
40 this.raw.y1 -= pixels
41 } else if (dir === 'bottom') {
42 this.raw.y1 = this.raw.y2
43 this.raw.y2 += pixels
44 }
45 this._update()
46 }
47
48 inMain(x, y) {
49 var inMainBox =
50 (this.x1 < x && x < this.x2) &&
51 (this.y1 < y && y < this.y2)
52 var inRawBox =
53 (this.raw.x1 < x && x < this.raw.x2) &&
54 (this.raw.y1 < y && y < this.raw.y2)
55
56 return inMainBox && !inRawBox
57 }
58
59 _update() {
60 this.x1 = this.raw.x1
61 this.x2 = this.raw.x2
62 this.y1 = this.raw.y1
63 this.y2 = this.raw.y2
64
65 // Check for endpoint adjustment.
66 // Pretend it's not an endpoint if the sym cell isn't an endpoint.
67 if (data.puzzle.symType != SYM_TYPE_NONE) {
68 var cell1 = data.puzzle.getCell(data.pos.x, data.pos.y)
69 var cell2 = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y)
70
71 if ((cell1.end == null) != (cell2.end == null)) {
72 var cell = {'end': 'none'}
73 } else if (this.sym !== true) {
74 var cell = cell1
75 } else {
76 var cell = cell2
77 }
78 } else {
79 var cell = data.puzzle.getCell(data.pos.x, data.pos.y)
80 }
81 if (cell.end === 'left') {
82 this.x1 -= 24
83 } else if (cell.end === 'right') {
84 this.x2 += 24
85 } else if (cell.end === 'top') {
86 this.y1 -= 24
87 } else if (cell.end === 'bottom') {
88 this.y2 += 24
89 }
90
91 this.middle = { // Note: Middle of the raw object
92 'x':(this.raw.x1 + this.raw.x2)/2,
93 'y':(this.raw.y1 + this.raw.y2)/2
94 }
95
96 if (this.debug != null) {
97 this.debug.setAttribute('x', this.x1)
98 this.debug.setAttribute('y', this.y1)
99 this.debug.setAttribute('width', this.x2 - this.x1)
100 this.debug.setAttribute('height', this.y2 - this.y1)
101 }
102 }
103}
104
105class PathSegment {
106 constructor(dir) {
107 this.poly1 = createElement('polygon')
108 this.circ = createElement('circle')
109 this.poly2 = createElement('polygon')
110 this.pillarCirc = createElement('circle')
111 this.dir = dir
112 data.svg.insertBefore(this.circ, data.cursor)
113 data.svg.insertBefore(this.poly2, data.cursor)
114 data.svg.insertBefore(this.pillarCirc, data.cursor)
115 this.circ.setAttribute('cx', data.bbox.middle.x)
116 this.circ.setAttribute('cy', data.bbox.middle.y)
117
118 if (data.puzzle.pillar === true) {
119 // cx/cy are updated in redraw(), since pillarCirc tracks the cursor
120 this.pillarCirc.setAttribute('cy', data.bbox.middle.y)
121 this.pillarCirc.setAttribute('r', 12)
122 if (data.pos.x === 0 && this.dir === MOVE_RIGHT) {
123 this.pillarCirc.setAttribute('cx', data.bbox.x1)
124 this.pillarCirc.setAttribute('static', true)
125 } else if (data.pos.x === data.puzzle.width - 1 && this.dir === MOVE_LEFT) {
126 this.pillarCirc.setAttribute('cx', data.bbox.x2)
127 this.pillarCirc.setAttribute('static', true)
128 } else {
129 this.pillarCirc.setAttribute('cx', data.bbox.middle.x)
130 }
131 }
132
133 if (data.puzzle.symType == SYM_TYPE_NONE) {
134 this.poly1.setAttribute('class', 'line-1 ' + data.svg.id)
135 this.circ.setAttribute('class', 'line-1 ' + data.svg.id)
136 this.poly2.setAttribute('class', 'line-1 ' + data.svg.id)
137 this.pillarCirc.setAttribute('class', 'line-1 ' + data.svg.id)
138 } else {
139 this.poly1.setAttribute('class', 'line-2 ' + data.svg.id)
140 this.circ.setAttribute('class', 'line-2 ' + data.svg.id)
141 this.poly2.setAttribute('class', 'line-2 ' + data.svg.id)
142 this.pillarCirc.setAttribute('class', 'line-2 ' + data.svg.id)
143
144 this.symPoly1 = createElement('polygon')
145 this.symCirc = createElement('circle')
146 this.symPoly2 = createElement('polygon')
147 this.symPillarCirc = createElement('circle')
148 data.svg.insertBefore(this.symCirc, data.cursor)
149 data.svg.insertBefore(this.symPoly2, data.cursor)
150 data.svg.insertBefore(this.symPillarCirc, data.cursor)
151
152 if (data.puzzle.settings.INVISIBLE_SYMMETRY) {
153 this.symPoly1.setAttribute('class', 'line-4 ' + data.svg.id)
154 this.symCirc.setAttribute('class', 'line-4 ' + data.svg.id)
155 this.symPoly2.setAttribute('class', 'line-4 ' + data.svg.id)
156 this.symPillarCirc.setAttribute('class', 'line-4 ' + data.svg.id)
157 } else {
158 this.symPoly1.setAttribute('class', 'line-3 ' + data.svg.id)
159 this.symCirc.setAttribute('class', 'line-3 ' + data.svg.id)
160 this.symPoly2.setAttribute('class', 'line-3 ' + data.svg.id)
161 this.symPillarCirc.setAttribute('class', 'line-3 ' + data.svg.id)
162 }
163
164 this.symCirc.setAttribute('cx', data.symbbox.middle.x)
165 this.symCirc.setAttribute('cy', data.symbbox.middle.y)
166
167 if (data.puzzle.pillar === true) {
168 // cx/cy are updated in redraw(), since symPillarCirc tracks the cursor
169 this.symPillarCirc.setAttribute('cy', data.symbbox.middle.y)
170 this.symPillarCirc.setAttribute('r', 12)
171 var symmetricalDir = getSymmetricalDir(data.puzzle, this.dir)
172 if (data.sym.x === 0 && symmetricalDir === MOVE_RIGHT) {
173 this.symPillarCirc.setAttribute('cx', data.symbbox.x1)
174 this.symPillarCirc.setAttribute('static', true)
175 } else if (data.sym.x === data.puzzle.width - 1 && symmetricalDir === MOVE_LEFT) {
176 this.symPillarCirc.setAttribute('cx', data.symbbox.x2)
177 this.symPillarCirc.setAttribute('static', true)
178 } else {
179 this.symPillarCirc.setAttribute('cx', data.symbbox.middle.x)
180 }
181 }
182 }
183
184 if (this.dir === MOVE_NONE) { // Start point
185 this.circ.setAttribute('r', 24)
186 this.circ.setAttribute('class', this.circ.getAttribute('class') + ' start')
187 if (data.puzzle.symType != SYM_TYPE_NONE) {
188 this.symCirc.setAttribute('r', 24)
189 this.symCirc.setAttribute('class', this.symCirc.getAttribute('class') + ' start')
190 }
191 } else {
192 // Only insert poly1 in non-startpoints
193 data.svg.insertBefore(this.poly1, data.cursor)
194 this.circ.setAttribute('r', 12)
195 if (data.puzzle.symType != SYM_TYPE_NONE) {
196 data.svg.insertBefore(this.symPoly1, data.cursor)
197 this.symCirc.setAttribute('r', 12)
198 }
199 }
200 }
201
202 destroy() {
203 data.svg.removeChild(this.poly1)
204 data.svg.removeChild(this.circ)
205 data.svg.removeChild(this.poly2)
206 data.svg.removeChild(this.pillarCirc)
207 if (data.puzzle.symType != SYM_TYPE_NONE) {
208 data.svg.removeChild(this.symPoly1)
209 data.svg.removeChild(this.symCirc)
210 data.svg.removeChild(this.symPoly2)
211 data.svg.removeChild(this.symPillarCirc)
212 }
213 }
214
215 redraw() { // Uses raw bbox because of endpoints
216 // Move the cursor and related objects
217 var x = clamp(data.x, data.bbox.x1, data.bbox.x2)
218 var y = clamp(data.y, data.bbox.y1, data.bbox.y2)
219 data.cursor.setAttribute('cx', x)
220 data.cursor.setAttribute('cy', y)
221 if (data.puzzle.symType != SYM_TYPE_NONE) {
222 data.symcursor.setAttribute('cx', this._reflX(x,y))
223 data.symcursor.setAttribute('cy', this._reflY(x,y))
224 }
225 if (data.puzzle.pillar === true) {
226 if (this.pillarCirc.getAttribute('static') == null) {
227 this.pillarCirc.setAttribute('cx', x)
228 this.pillarCirc.setAttribute('cy', y)
229 }
230 if (data.puzzle.symType != SYM_TYPE_NONE) {
231 if (this.symPillarCirc.getAttribute('static') == null) {
232 this.symPillarCirc.setAttribute('cx', this._reflX(x,y))
233 this.symPillarCirc.setAttribute('cy', this._reflY(x,y))
234 }
235 }
236 }
237
238 // Draw the first-half box
239 var points1 = JSON.parse(JSON.stringify(data.bbox.raw))
240 if (this.dir === MOVE_LEFT) {
241 points1.x1 = clamp(data.x, data.bbox.middle.x, data.bbox.x2)
242 } else if (this.dir === MOVE_RIGHT) {
243 points1.x2 = clamp(data.x, data.bbox.x1, data.bbox.middle.x)
244 } else if (this.dir === MOVE_TOP) {
245 points1.y1 = clamp(data.y, data.bbox.middle.y, data.bbox.y2)
246 } else if (this.dir === MOVE_BOTTOM) {
247 points1.y2 = clamp(data.y, data.bbox.y1, data.bbox.middle.y)
248 }
249 this.poly1.setAttribute('points',
250 points1.x1 + ' ' + points1.y1 + ',' +
251 points1.x1 + ' ' + points1.y2 + ',' +
252 points1.x2 + ' ' + points1.y2 + ',' +
253 points1.x2 + ' ' + points1.y1
254 )
255
256 var firstHalf = false
257 var isEnd = (data.puzzle.grid[data.pos.x][data.pos.y].end != null)
258 // The second half of the line uses the raw so that it can enter the endpoint properly.
259 var points2 = JSON.parse(JSON.stringify(data.bbox.raw))
260 if (data.x < data.bbox.middle.x && this.dir !== MOVE_RIGHT) {
261 points2.x1 = clamp(data.x, data.bbox.x1, data.bbox.middle.x)
262 points2.x2 = data.bbox.middle.x
263 if (isEnd && data.pos.x%2 === 0 && data.pos.y%2 === 1) {
264 points2.y1 += 17
265 points2.y2 -= 17
266 }
267 } else if (data.x > data.bbox.middle.x && this.dir !== MOVE_LEFT) {
268 points2.x1 = data.bbox.middle.x
269 points2.x2 = clamp(data.x, data.bbox.middle.x, data.bbox.x2)
270 if (isEnd && data.pos.x%2 === 0 && data.pos.y%2 === 1) {
271 points2.y1 += 17
272 points2.y2 -= 17
273 }
274 } else if (data.y < data.bbox.middle.y && this.dir !== MOVE_BOTTOM) {
275 points2.y1 = clamp(data.y, data.bbox.y1, data.bbox.middle.y)
276 points2.y2 = data.bbox.middle.y
277 if (isEnd && data.pos.x%2 === 1 && data.pos.y%2 === 0) {
278 points2.x1 += 17
279 points2.x2 -= 17
280 }
281 } else if (data.y > data.bbox.middle.y && this.dir !== MOVE_TOP) {
282 points2.y1 = data.bbox.middle.y
283 points2.y2 = clamp(data.y, data.bbox.middle.y, data.bbox.y2)
284 if (isEnd && data.pos.x%2 === 1 && data.pos.y%2 === 0) {
285 points2.x1 += 17
286 points2.x2 -= 17
287 }
288 } else {
289 firstHalf = true
290 }
291
292 this.poly2.setAttribute('points',
293 points2.x1 + ' ' + points2.y1 + ',' +
294 points2.x1 + ' ' + points2.y2 + ',' +
295 points2.x2 + ' ' + points2.y2 + ',' +
296 points2.x2 + ' ' + points2.y1
297 )
298
299 // Show the second poly only in the second half of the cell
300 this.poly2.setAttribute('opacity', (firstHalf ? 0 : 1))
301 // Show the circle in the second half of the cell AND in the start
302 if (firstHalf && this.dir !== MOVE_NONE) {
303 this.circ.setAttribute('opacity', 0)
304 } else {
305 this.circ.setAttribute('opacity', 1)
306 }
307
308 // Draw the symmetrical path based on the original one
309 if (data.puzzle.symType != SYM_TYPE_NONE) {
310 this.symPoly1.setAttribute('points',
311 this._reflX(points1.x2, points1.y2) + ' ' + this._reflY(points1.x2, points1.y2) + ',' +
312 this._reflX(points1.x2, points1.y2) + ' ' + this._reflY(points1.x1, points1.y1) + ',' +
313 this._reflX(points1.x1, points1.y1) + ' ' + this._reflY(points1.x1, points1.y1) + ',' +
314 this._reflX(points1.x1, points1.y1) + ' ' + this._reflY(points1.x2, points1.y2)
315 )
316
317 this.symPoly2.setAttribute('points',
318 this._reflX(points2.x2, points2.y2) + ' ' + this._reflY(points2.x2, points2.y2) + ',' +
319 this._reflX(points2.x2, points2.y2) + ' ' + this._reflY(points2.x1, points2.y1) + ',' +
320 this._reflX(points2.x1, points2.y1) + ' ' + this._reflY(points2.x1, points2.y1) + ',' +
321 this._reflX(points2.x1, points2.y1) + ' ' + this._reflY(points2.x2, points2.y2)
322 )
323
324 this.symCirc.setAttribute('opacity', this.circ.getAttribute('opacity'))
325 this.symPoly2.setAttribute('opacity', this.poly2.getAttribute('opacity'))
326 }
327 }
328
329 _reflX(x,y) {
330 if (data.puzzle.symType == SYM_TYPE_NONE) return x
331
332 if (data.puzzle.symType == SYM_TYPE_VERTICAL || data.puzzle.symType == SYM_TYPE_ROTATIONAL || data.puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) {
333 // Mirror position inside the bounding box
334 return (data.bbox.middle.x - x) + data.symbbox.middle.x
335 }
336 if (data.puzzle.symType == SYM_TYPE_HORIZONTAL || data.puzzle.symType == SYM_TYPE_PARALLEL_H || data.puzzle.symType == SYM_TYPE_PARALLEL_V || data.puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) {
337 // Copy position inside the bounding box
338 return (x - data.bbox.middle.x) + data.symbbox.middle.x
339 }
340 if (data.puzzle.symType == SYM_TYPE_ROTATE_LEFT || data.puzzle.symType == SYM_TYPE_FLIP_XY) {
341 // Rotate position left inside the bounding box
342 return (y - data.bbox.middle.y) + data.symbbox.middle.x
343 }
344 if (data.puzzle.symType == SYM_TYPE_ROTATE_RIGHT || data.puzzle.symType == SYM_TYPE_FLIP_NEG_XY) {
345 // Rotate position right inside the bounding box
346 return (data.bbox.middle.y - y) + data.symbbox.middle.x
347 }
348 }
349
350 _reflY(x,y) {
351 if (data.puzzle.symType == SYM_TYPE_NONE) return y
352
353 if (data.puzzle.symType == SYM_TYPE_HORIZONTAL || data.puzzle.symType == SYM_TYPE_ROTATIONAL || data.puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) {
354 // Mirror position inside the bounding box
355 return (data.bbox.middle.y - y) + data.symbbox.middle.y
356 }
357 if (data.puzzle.symType == SYM_TYPE_VERTICAL || data.puzzle.symType == SYM_TYPE_PARALLEL_V || data.puzzle.symType == SYM_TYPE_PARALLEL_H || data.puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) {
358 // Copy position inside the bounding box
359 return (y - data.bbox.middle.y) + data.symbbox.middle.y
360 }
361 if (data.puzzle.symType == SYM_TYPE_ROTATE_RIGHT || data.puzzle.symType == SYM_TYPE_FLIP_XY) {
362 // Rotate position left inside the bounding box
363 return (x - data.bbox.middle.x) + data.symbbox.middle.y
364 }
365 if (data.puzzle.symType == SYM_TYPE_ROTATE_LEFT || data.puzzle.symType == SYM_TYPE_FLIP_NEG_XY) {
366 // Rotate position right inside the bounding box
367 return (data.bbox.middle.x - x) + data.symbbox.middle.y
368 }
369 }
370}
371
372var data = {}
373
374function clearGrid(svg, puzzle) {
375 if (data.bbox != null && data.bbox.debug != null) {
376 data.svg.removeChild(data.bbox.debug)
377 data.bbox = null
378 }
379 if (data.symbbox != null && data.symbbox.debug != null) {
380 data.svg.removeChild(data.symbbox.debug)
381 data.symbbox = null
382 }
383
384 window.deleteElementsByClassName(svg, 'cursor')
385 window.deleteElementsByClassName(svg, 'line-1')
386 window.deleteElementsByClassName(svg, 'line-2')
387 window.deleteElementsByClassName(svg, 'line-3')
388 window.deleteElementsByClassName(svg, 'line-4')
389 puzzle.clearLines()
390}
391
392// This copy is an exact copy of puzzle.getSymmetricalDir, except that it uses MOVE_* values instead of strings
393function getSymmetricalDir(puzzle, dir) {
394 if (puzzle.symType == SYM_TYPE_VERTICAL || puzzle.symType == SYM_TYPE_ROTATIONAL || puzzle.symType == SYM_TYPE_PARALLEL_H_FLIP) {
395 if (dir === MOVE_LEFT) return MOVE_RIGHT
396 if (dir === MOVE_RIGHT) return MOVE_LEFT
397 }
398 if (puzzle.symType == SYM_TYPE_HORIZONTAL || puzzle.symType == SYM_TYPE_ROTATIONAL || puzzle.symType == SYM_TYPE_PARALLEL_V_FLIP) {
399 if (dir === MOVE_TOP) return MOVE_BOTTOM
400 if (dir === MOVE_BOTTOM) return MOVE_TOP
401 }
402 if (puzzle.symType == SYM_TYPE_ROTATE_LEFT || puzzle.symType == SYM_TYPE_FLIP_NEG_XY) {
403 if (dir === MOVE_LEFT) return MOVE_BOTTOM
404 if (dir === MOVE_RIGHT) return MOVE_TOP
405 }
406 if (puzzle.symType == SYM_TYPE_ROTATE_RIGHT || puzzle.symType == SYM_TYPE_FLIP_NEG_XY) {
407 if (dir === MOVE_TOP) return MOVE_RIGHT
408 if (dir === MOVE_BOTTOM) return MOVE_LEFT
409 }
410 if (puzzle.symType == SYM_TYPE_ROTATE_LEFT || puzzle.symType == SYM_TYPE_FLIP_XY) {
411 if (dir === MOVE_TOP) return MOVE_LEFT
412 if (dir === MOVE_BOTTOM) return MOVE_RIGHT
413 }
414 if (puzzle.symType == SYM_TYPE_ROTATE_RIGHT || puzzle.symType == SYM_TYPE_FLIP_XY) {
415 if (dir === MOVE_RIGHT) return MOVE_BOTTOM
416 if (dir === MOVE_LEFT) return MOVE_TOP
417 }
418 return dir
419}
420
421window.trace = function(event, puzzle, pos, start, symStart=null) {
422 /*if (data.start == null) {*/
423 if (data.tracing !== true) { // could be undefined or false
424 var svg = start.parentElement
425 data.tracing = true
426 window.PLAY_SOUND('start')
427 // Cleans drawn lines & puzzle state
428 clearGrid(svg, puzzle)
429 onTraceStart(puzzle, pos, svg, start, symStart)
430 data.animations.insertRule('.' + svg.id + '.start {animation: 150ms 1 forwards start-grow}\n')
431
432 hookMovementEvents(start)
433 } else {
434 event.stopPropagation()
435 // Signal the onMouseMove to stop accepting input (race condition)
436 data.tracing = false
437
438 // At endpoint and in main box
439 var cell = puzzle.getCell(data.pos.x, data.pos.y)
440 if (cell.end != null && data.bbox.inMain(data.x, data.y)) {
441 data.cursor.onpointerdown = null
442 setTimeout(function() { // Run validation asynchronously so we can free the pointer immediately.
443 puzzle.endPoint = data.pos
444 var puzzleData = window.validate(puzzle, false) // We want all invalid elements so we can show the user.
445
446 for (var negation of puzzleData.negations) {
447 console.debug('Rendering negation', negation)
448 data.animations.insertRule('.' + data.svg.id + '_' + negation.source.x + '_' + negation.source.y + ' {animation: 0.75s 1 forwards fade}\n')
449 data.animations.insertRule('.' + data.svg.id + '_' + negation.target.x + '_' + negation.target.y + ' {animation: 0.75s 1 forwards fade}\n')
450 }
451
452 if (puzzleData.valid()) {
453 window.PLAY_SOUND('success')
454 // !important to override the child animation
455 data.animations.insertRule('.' + data.svg.id + ' {animation: 1s 1 forwards line-success !important}\n')
456
457 // Convert the traced path into something suitable for solve.drawPath (for publishing purposes)
458 var rawPath = [puzzle.startPoint]
459 for (var i=1; i<data.path.length; i++) rawPath.push(data.path[i].dir)
460 rawPath.push(0)
461
462 if (window.TRACE_COMPLETION_FUNC) window.TRACE_COMPLETION_FUNC(puzzle, rawPath)
463 } else {
464 window.PLAY_SOUND('fail')
465 data.animations.insertRule('.' + data.svg.id + ' {animation: 1s 1 forwards line-fail !important}\n')
466 // Get list of invalid elements
467 if (puzzle.settings.FLASH_FOR_ERRORS) {
468 for (var invalidElement of puzzleData.invalidElements) {
469 data.animations.insertRule('.' + data.svg.id + '_' + invalidElement.x + '_' + invalidElement.y + ' {animation: 0.4s 20 alternate-reverse error}\n')
470 }
471 }
472 }
473 }, 1)
474
475 // Right-clicked (or double-tapped) and not at the end: Clear puzzle
476 } else if (event.isRightClick()) {
477 window.PLAY_SOUND('abort')
478 clearGrid(data.svg, puzzle)
479 } else { // Exit lock but allow resuming from the cursor (Desktop only)
480 data.cursor.onpointerdown = function() {
481 if (start.parentElement !== data.svg) return // Another puzzle is live, so data is gone
482 data.tracing = true
483 hookMovementEvents(start)
484 }
485 }
486
487 unhookMovementEvents()
488 }
489}
490
491window.clearAnimations = function() {
492 if (data.animations == null) return
493 for (var i=0; i<data.animations.cssRules.length; i++) {
494 var rule = data.animations.cssRules[i]
495 if (rule.selectorText != null && rule.selectorText.startsWith('.' + data.svg.id)) {
496 data.animations.deleteRule(i--)
497 }
498 }
499}
500
501window.onTraceStart = function(puzzle, pos, svg, start, symStart=null) {
502 var x = parseFloat(start.getAttribute('cx'))
503 var y = parseFloat(start.getAttribute('cy'))
504
505 var cursor = createElement('circle')
506 cursor.setAttribute('r', 12)
507 cursor.setAttribute('fill', window.CURSOR)
508 cursor.setAttribute('stroke', 'black')
509 cursor.setAttribute('stroke-width', '2px')
510 cursor.setAttribute('stroke-opacity', '0.4')
511 cursor.setAttribute('class', 'cursor')
512 cursor.setAttribute('cx', x)
513 cursor.setAttribute('cy', y)
514 svg.insertBefore(cursor, svg.getElementById('cursorPos'))
515
516 data.svg = svg
517 data.cursor = cursor
518 data.x = x
519 data.y = y
520 data.pos = pos
521 data.sym = puzzle.getSymmetricalPos(pos.x, pos.y)
522 data.puzzle = puzzle
523 data.path = []
524 puzzle.startPoint = {'x': pos.x, 'y': pos.y}
525
526 if (pos.x % 2 === 1) { // Start point is on a horizontal segment
527 data.bbox = new BoundingBox(x - 29, x + 29, y - 12, y + 12)
528 } else if (pos.y % 2 === 1) { // Start point is on a vertical segment
529 data.bbox = new BoundingBox(x - 12, x + 12, y - 29, y + 29)
530 } else { // Start point is at an intersection
531 data.bbox = new BoundingBox(x - 12, x + 12, y - 12, y + 12)
532 }
533
534 for (var styleSheet of document.styleSheets) {
535 if (styleSheet.title === 'animations') {
536 data.animations = styleSheet
537 break
538 }
539 }
540
541 clearAnimations()
542
543 // Add initial line segments + secondary symmetry cursor, if needed
544 if (puzzle.symType == SYM_TYPE_NONE) {
545 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'type', 'line')
546 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLACK)
547 } else {
548 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'type', 'line')
549 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLUE)
550 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'type', 'line')
551 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_YELLOW)
552
553 var dx = parseFloat(symStart.getAttribute('cx')) - data.x
554 var dy = parseFloat(symStart.getAttribute('cy')) - data.y
555 data.symbbox = new BoundingBox(
556 data.bbox.raw.x1 + dx,
557 data.bbox.raw.x2 + dx,
558 data.bbox.raw.y1 + dy,
559 data.bbox.raw.y2 + dy,
560 sym = true)
561
562 data.symcursor = createElement('circle')
563 svg.appendChild(data.symcursor)
564 if (data.puzzle.settings.INVISIBLE_SYMMETRY) {
565 data.symcursor.setAttribute('class', 'line-4 ' + data.svg.id)
566 } else {
567 data.symcursor.setAttribute('class', 'line-3 ' + data.svg.id)
568 }
569 data.symcursor.setAttribute('cx', symStart.getAttribute('cx'))
570 data.symcursor.setAttribute('cy', symStart.getAttribute('cy'))
571 data.symcursor.setAttribute('r', 12)
572 }
573
574 // Fixup: Mark out of bounds cells as null, setting inbounds cells as {}
575 // This allows tracing to correctly identify inbounds cells (and thus interior walls) and correctly handle exterior walls for oddly shaped puzzles.
576 {
577 var savedGrid = data.puzzle.switchToMaskedGrid()
578 var maskedGrid = data.puzzle.grid
579 data.puzzle.grid = savedGrid
580
581 for (var x=1; x<data.puzzle.width; x+=2) {
582 for (var y=1; y<data.puzzle.height; y+=2) {
583 if (maskedGrid[x][y] == null) { // null == MASKED_OOB
584 data.puzzle.grid[x][y] = null
585 } else if (data.puzzle.grid[x][y] == null) {
586 data.puzzle.grid[x][y] = {'type':'nonce'}
587 }
588 }
589 }
590 }
591 data.path.push(new PathSegment(MOVE_NONE)) // Must be created after initializing data.symbbox
592}
593
594// In case the user exit the pointer lock via another means (clicking outside the window, hitting esc, etc)
595// we still need to disengage our tracing hooks.
596document.onpointerlockchange = function() {
597 if (document.pointerLockElement == null) unhookMovementEvents()
598}
599
600function unhookMovementEvents() {
601 data.start = null
602 document.onmousemove = null
603 document.ontouchstart = null
604 document.ontouchmove = null
605 document.ontouchend = null
606 if (document.exitPointerLock != null) document.exitPointerLock()
607 if (document.mozExitPointerLock != null) document.mozExitPointerLock()
608}
609
610function hookMovementEvents(start) {
611 data.start = start
612 if (start.requestPointerLock != null) start.requestPointerLock()
613 if (start.mozRequestPointerLock != null) start.mozRequestPointerLock()
614
615 var sens = parseFloat(document.getElementById('sens').value)
616 document.onmousemove = function(event) {
617 // Working around a race condition where movement events fire after the handler is removed.
618 if (data.tracing !== true) return
619 // Prevent accidental fires on mobile platforms (ios and android). They will be handled via ontouchmove instead.
620 if (event.movementX == null) return
621 onMove(sens * event.movementX, sens * event.movementY)
622 }
623 document.ontouchstart = function(event) {
624 if (event.touches.length > 1) {
625 // Stop tracing for two+ finger touches (the equivalent of a right click on desktop)
626 window.trace(event, data.puzzle, null, null, null)
627 return
628 }
629 data.lastTouchPos = event.position
630 }
631 document.ontouchmove = function(event) {
632 if (data.tracing !== true) return
633
634 var eventIsWithinPuzzle = false
635 for (var node = event.target; node != null; node = node.parentElement) {
636 if (node == data.svg) {
637 eventIsWithinPuzzle = true
638 break
639 }
640 }
641 if (!eventIsWithinPuzzle) return // Ignore drag events that aren't within the puzzle
642 event.preventDefault() // Prevent accidental scrolling if the touch event is within the puzzle.
643
644 var newPos = event.position
645 onMove(newPos.x - data.lastTouchPos.x, newPos.y - data.lastTouchPos.y)
646 data.lastTouchPos = newPos
647 }
648 document.ontouchend = function(event) {
649 data.lastTouchPos = null
650 // Only call window.trace (to stop tracing) if we're really in an endpoint.
651 var cell = data.puzzle.getCell(data.pos.x, data.pos.y)
652 if (cell.end != null && data.bbox.inMain(data.x, data.y)) {
653 window.trace(event, data.puzzle, null, null, null)
654 }
655 }
656}
657
658// @Volatile -- must match order of PATH_* in solve
659var MOVE_NONE = 0
660var MOVE_LEFT = 1
661var MOVE_RIGHT = 2
662var MOVE_TOP = 3
663var MOVE_BOTTOM = 4
664
665window.onMove = function(dx, dy) {
666 {
667 // Also handles some collision
668 var collidedWith = pushCursor(dx, dy)
669 console.spam('Collided with', collidedWith)
670 }
671
672 while (true) {
673 hardCollision()
674
675 // Potentially move the location to a new cell, and make absolute boundary checks
676 var moveDir = move()
677 data.path[data.path.length - 1].redraw()
678 if (moveDir === MOVE_NONE) break
679 console.debug('Moved', ['none', 'left', 'right', 'top', 'bottom'][moveDir])
680
681 // Potentially adjust data.x/data.y if our position went around a pillar
682 if (data.puzzle.pillar === true) pillarWrap(moveDir)
683
684 var lastDir = data.path[data.path.length - 1].dir
685 var backedUp = ((moveDir === MOVE_LEFT && lastDir === MOVE_RIGHT)
686 || (moveDir === MOVE_RIGHT && lastDir === MOVE_LEFT)
687 || (moveDir === MOVE_TOP && lastDir === MOVE_BOTTOM)
688 || (moveDir === MOVE_BOTTOM && lastDir === MOVE_TOP))
689
690 if (data.puzzle.symType != SYM_TYPE_NONE) {
691 var symMoveDir = getSymmetricalDir(data.puzzle, moveDir)
692 }
693
694 // If we backed up, remove a path segment and mark the old cell as unvisited
695 if (backedUp) {
696 data.path.pop().destroy()
697 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_NONE)
698 if (data.puzzle.symType != SYM_TYPE_NONE) {
699 if (data.puzzle.getLine(data.sym.x, data.sym.y) == window.LINE_OVERLAP) {
700 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_BLUE)
701 } else {
702 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_NONE)
703 }
704 }
705 }
706
707 // Move to the next cell
708 changePos(data.bbox, data.pos, moveDir)
709 if (data.puzzle.symType != SYM_TYPE_NONE) {
710 changePos(data.symbbox, data.sym, symMoveDir)
711 }
712
713 // If we didn't back up, add a path segment and mark the new cell as visited
714 if (!backedUp) {
715 data.path.push(new PathSegment(moveDir))
716 if (data.puzzle.symType == SYM_TYPE_NONE) {
717 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLACK)
718 } else {
719 data.puzzle.updateCell2(data.pos.x, data.pos.y, 'line', window.LINE_BLUE)
720 if (data.puzzle.getLine(data.sym.x, data.sym.y) == window.LINE_BLUE) {
721 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_OVERLAP)
722 } else {
723 data.puzzle.updateCell2(data.sym.x, data.sym.y, 'line', window.LINE_YELLOW)
724 }
725 }
726 }
727 }
728}
729
730// Helper function for pushCursor. Used to determine the direction and magnitude of redirection.
731function push(dx, dy, dir, targetDir) {
732 // Fraction of movement to redirect in the other direction
733 var movementRatio = null
734 if (targetDir === 'left' || targetDir === 'top') {
735 movementRatio = -3
736 } else if (targetDir === 'right' || targetDir === 'bottom') {
737 movementRatio = 3
738 }
739 if (window.settings.disablePushing === true) movementRatio *= 1000
740
741 if (dir === 'left') {
742 var overshoot = data.bbox.x1 - (data.x + dx) + 12
743 if (overshoot > 0) {
744 data.y += dy + overshoot / movementRatio
745 data.x = data.bbox.x1 + 12
746 return true
747 }
748 } else if (dir === 'right') {
749 var overshoot = (data.x + dx) - data.bbox.x2 + 12
750 if (overshoot > 0) {
751 data.y += dy + overshoot / movementRatio
752 data.x = data.bbox.x2 - 12
753 return true
754 }
755 } else if (dir === 'leftright') {
756 data.y += dy + Math.abs(dx) / movementRatio
757 return true
758 } else if (dir === 'top') {
759 var overshoot = data.bbox.y1 - (data.y + dy) + 12
760 if (overshoot > 0) {
761 data.x += dx + overshoot / movementRatio
762 data.y = data.bbox.y1 + 12
763 return true
764 }
765 } else if (dir === 'bottom') {
766 var overshoot = (data.y + dy) - data.bbox.y2 + 12
767 if (overshoot > 0) {
768 data.x += dx + overshoot / movementRatio
769 data.y = data.bbox.y2 - 12
770 return true
771 }
772 } else if (dir === 'topbottom') {
773 data.x += dx + Math.abs(dy) / movementRatio
774 return true
775 }
776 return false
777}
778
779// Redirect momentum from pushing against walls, so that all further moment steps
780// will be strictly linear. Returns a string for logging purposes only.
781function pushCursor(dx, dy) {
782 // Outer wall collision
783 var cell = data.puzzle.getCell(data.pos.x, data.pos.y)
784 if (cell == null) return 'nothing'
785
786 // Only consider non-endpoints or endpoints which are parallel
787 if ([undefined, 'top', 'bottom'].includes(cell.end)) {
788 var leftCell = data.puzzle.getCell(data.pos.x - 1, data.pos.y)
789 if (leftCell == null || leftCell.gap === window.GAP_FULL) {
790 if (push(dx, dy, 'left', 'top')) return 'left outer wall'
791 }
792 var rightCell = data.puzzle.getCell(data.pos.x + 1, data.pos.y)
793 if (rightCell == null || rightCell.gap === window.GAP_FULL) {
794 if (push(dx, dy, 'right', 'top')) return 'right outer wall'
795 }
796 }
797 // Only consider non-endpoints or endpoints which are parallel
798 if ([undefined, 'left', 'right'].includes(cell.end)) {
799 var topCell = data.puzzle.getCell(data.pos.x, data.pos.y - 1)
800 if (topCell == null || topCell.gap === window.GAP_FULL) {
801 if (push(dx, dy, 'top', 'right')) return 'top outer wall'
802 }
803 var bottomCell = data.puzzle.getCell(data.pos.x, data.pos.y + 1)
804 if (bottomCell == null || bottomCell.gap === window.GAP_FULL) {
805 if (push(dx, dy, 'bottom', 'right')) return 'bottom outer wall'
806 }
807 }
808
809 // Inner wall collision
810 if (cell.end == null) {
811 if (data.pos.x%2 === 1 && data.pos.y%2 === 0) { // Horizontal cell
812 if (data.x < data.bbox.middle.x) {
813 push(dx, dy, 'topbottom', 'left')
814 return 'topbottom inner wall, moved left'
815 } else {
816 push(dx, dy, 'topbottom', 'right')
817 return 'topbottom inner wall, moved right'
818 }
819 } else if (data.pos.x%2 === 0 && data.pos.y%2 === 1) { // Vertical cell
820 if (data.y < data.bbox.middle.y) {
821 push(dx, dy, 'leftright', 'top')
822 return 'leftright inner wall, moved up'
823 } else {
824 push(dx, dy, 'leftright', 'bottom')
825 return 'leftright inner wall, moved down'
826 }
827 }
828 }
829
830 // Intersection & endpoint collision
831 // Ratio of movement to be considered turning at an intersection
832 var turnMod = 2
833 if ((data.pos.x%2 === 0 && data.pos.y%2 === 0) || cell.end != null) {
834 if (data.x < data.bbox.middle.x) {
835 push(dx, dy, 'topbottom', 'right')
836 // Overshot the intersection and appears to be trying to turn
837 if (data.x > data.bbox.middle.x && Math.abs(dy) * turnMod > Math.abs(dx)) {
838 data.y += Math.sign(dy) * (data.x - data.bbox.middle.x)
839 data.x = data.bbox.middle.x
840 return 'overshot moving right'
841 }
842 return 'intersection moving right'
843 } else if (data.x > data.bbox.middle.x) {
844 push(dx, dy, 'topbottom', 'left')
845 // Overshot the intersection and appears to be trying to turn
846 if (data.x < data.bbox.middle.x && Math.abs(dy) * turnMod > Math.abs(dx)) {
847 data.y += Math.sign(dy) * (data.bbox.middle.x - data.x)
848 data.x = data.bbox.middle.x
849 return 'overshot moving left'
850 }
851 return 'intersection moving left'
852 }
853 if (data.y < data.bbox.middle.y) {
854 push(dx, dy, 'leftright', 'bottom')
855 // Overshot the intersection and appears to be trying to turn
856 if (data.y > data.bbox.middle.y && Math.abs(dx) * turnMod > Math.abs(dy)) {
857 data.x += Math.sign(dx) * (data.y - data.bbox.middle.y)
858 data.y = data.bbox.middle.y
859 return 'overshot moving down'
860 }
861 return 'intersection moving down'
862 } else if (data.y > data.bbox.middle.y) {
863 push(dx, dy, 'leftright', 'top')
864 // Overshot the intersection and appears to be trying to turn
865 if (data.y < data.bbox.middle.y && Math.abs(dx) * turnMod > Math.abs(dy)) {
866 data.x += Math.sign(dx) * (data.bbox.middle.y - data.y)
867 data.y = data.bbox.middle.y
868 return 'overshot moving up'
869 }
870 return 'intersection moving up'
871 }
872 }
873
874 // No collision, limit movement to X or Y only to prevent out-of-bounds
875 if (Math.abs(dx) > Math.abs(dy)) {
876 data.x += dx
877 return 'nothing, x'
878 } else {
879 data.y += dy
880 return 'nothing, y'
881 }
882}
883
884// Check to see if we collided with any gaps, or with a symmetrical line, or a startpoint.
885// In any case, abruptly zero momentum.
886function hardCollision() {
887 var lastDir = data.path[data.path.length - 1].dir
888 var cell = data.puzzle.getCell(data.pos.x, data.pos.y)
889 if (cell == null) return
890
891 var gapSize = 0
892 if (cell.gap === window.GAP_BREAK) {
893 console.spam('Collided with a gap')
894 gapSize = 21
895 } else {
896 var nextCell = null
897 if (lastDir === MOVE_LEFT) nextCell = data.puzzle.getCell(data.pos.x - 1, data.pos.y)
898 if (lastDir === MOVE_RIGHT) nextCell = data.puzzle.getCell(data.pos.x + 1, data.pos.y)
899 if (lastDir === MOVE_TOP) nextCell = data.puzzle.getCell(data.pos.x, data.pos.y - 1)
900 if (lastDir === MOVE_BOTTOM) nextCell = data.puzzle.getCell(data.pos.x, data.pos.y + 1)
901 if (nextCell != null && nextCell.start === true && nextCell.line > window.LINE_NONE) {
902 gapSize = -5
903 }
904 }
905
906 if (data.puzzle.symType != SYM_TYPE_NONE) {
907 if (data.sym.x === data.pos.x && data.sym.y === data.pos.y) {
908 console.spam('Collided with our symmetrical line')
909 gapSize = 13
910 } else if (data.puzzle.getCell(data.sym.x, data.sym.y).gap === window.GAP_BREAK) {
911 console.spam('Symmetrical line hit a gap')
912 gapSize = 21
913 }
914 }
915 if (gapSize === 0) return // Didn't collide with anything
916
917 if (lastDir === MOVE_LEFT) {
918 data.x = Math.max(data.bbox.middle.x + gapSize, data.x)
919 } else if (lastDir === MOVE_RIGHT) {
920 data.x = Math.min(data.x, data.bbox.middle.x - gapSize)
921 } else if (lastDir === MOVE_TOP) {
922 data.y = Math.max(data.bbox.middle.y + gapSize, data.y)
923 } else if (lastDir === MOVE_BOTTOM) {
924 data.y = Math.min(data.y, data.bbox.middle.y - gapSize)
925 }
926}
927
928// Check to see if we've gone beyond the edge of puzzle cell, and if the next cell is safe,
929// i.e. not out of bounds. Reports the direction we are going to move (or none),
930// but does not actually change data.pos
931function move() {
932 var lastDir = data.path[data.path.length - 1].dir
933
934 if (data.x < data.bbox.x1 + 12) { // Moving left
935 var cell = data.puzzle.getCell(data.pos.x - 1, data.pos.y)
936 if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) {
937 console.spam('Collided with outside / gap-2', cell)
938 data.x = data.bbox.x1 + 12
939 } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_RIGHT) {
940 console.spam('Collided with other line', cell.line)
941 data.x = data.bbox.x1 + 12
942 } else if (data.puzzle.symType != SYM_TYPE_NONE) {
943 var symCell = data.puzzle.getSymmetricalCell(data.pos.x - 1, data.pos.y)
944 if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) {
945 console.spam('Collided with symmetrical outside / gap-2', cell)
946 data.x = data.bbox.x1 + 12
947 }
948 }
949 if (data.x < data.bbox.x1) {
950 return MOVE_LEFT
951 }
952 } else if (data.x > data.bbox.x2 - 12) { // Moving right
953 var cell = data.puzzle.getCell(data.pos.x + 1, data.pos.y)
954 if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) {
955 console.spam('Collided with outside / gap-2', cell)
956 data.x = data.bbox.x2 - 12
957 } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_LEFT) {
958 console.spam('Collided with other line', cell.line)
959 data.x = data.bbox.x2 - 12
960 } else if (data.puzzle.symType != SYM_TYPE_NONE) {
961 var symCell = data.puzzle.getSymmetricalCell(data.pos.x + 1, data.pos.y)
962 if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) {
963 console.spam('Collided with symmetrical outside / gap-2', cell)
964 data.x = data.bbox.x2 - 12
965 }
966 }
967 if (data.x > data.bbox.x2) {
968 return MOVE_RIGHT
969 }
970 } else if (data.y < data.bbox.y1 + 12) { // Moving up
971 var cell = data.puzzle.getCell(data.pos.x, data.pos.y - 1)
972 if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) {
973 console.spam('Collided with outside / gap-2', cell)
974 data.y = data.bbox.y1 + 12
975 } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_BOTTOM) {
976 console.spam('Collided with other line', cell.line)
977 data.y = data.bbox.y1 + 12
978 } else if (data.puzzle.symType != SYM_TYPE_NONE) {
979 var symCell = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y - 1)
980 if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) {
981 console.spam('Collided with symmetrical outside / gap-2', cell)
982 data.y = data.bbox.y1 + 12
983 }
984 }
985 if (data.y < data.bbox.y1) {
986 return MOVE_TOP
987 }
988 } else if (data.y > data.bbox.y2 - 12) { // Moving down
989 var cell = data.puzzle.getCell(data.pos.x, data.pos.y + 1)
990 if (cell == null || cell.type !== 'line' || cell.gap === window.GAP_FULL) {
991 console.spam('Collided with outside / gap-2')
992 data.y = data.bbox.y2 - 12
993 } else if (cell.line > window.LINE_NONE && lastDir !== MOVE_TOP) {
994 console.spam('Collided with other line', cell.line)
995 data.y = data.bbox.y2 - 12
996 } else if (data.puzzle.symType != SYM_TYPE_NONE) {
997 var symCell = data.puzzle.getSymmetricalCell(data.pos.x, data.pos.y + 1)
998 if (symCell == null || symCell.type !== 'line' || symCell.gap === window.GAP_FULL) {
999 console.spam('Collided with symmetrical outside / gap-2', cell)
1000 data.y = data.bbox.y2 - 12
1001 }
1002 }
1003 if (data.y > data.bbox.y2) {
1004 return MOVE_BOTTOM
1005 }
1006 }
1007 return MOVE_NONE
1008}
1009
1010// Check to see if you moved beyond the edge of a pillar.
1011// If so, wrap the cursor x to preserve momentum.
1012// Note that this still does not change the position.
1013function pillarWrap(moveDir) {
1014 if (moveDir === MOVE_LEFT && data.pos.x === 0) {
1015 data.x += data.puzzle.width * 41
1016 }
1017 if (moveDir === MOVE_RIGHT && data.pos.x === data.puzzle.width - 1) {
1018 data.x -= data.puzzle.width * 41
1019 }
1020}
1021
1022// Actually change the data position. (Note that this takes in pos to allow easier symmetry).
1023// Note that this doesn't zero the momentum, so that we can adjust appropriately on further loops.
1024// This function also shifts the bounding box that we use to determine the bounds of the cell.
1025function changePos(bbox, pos, moveDir) {
1026 if (moveDir === MOVE_LEFT) {
1027 pos.x--
1028 // Wrap around the left
1029 if (data.puzzle.pillar === true && pos.x < 0) {
1030 pos.x += data.puzzle.width
1031 bbox.shift('right', data.puzzle.width * 41 - 82)
1032 bbox.shift('right', 58)
1033 } else {
1034 bbox.shift('left', (pos.x%2 === 0 ? 24 : 58))
1035 }
1036 } else if (moveDir === MOVE_RIGHT) {
1037 pos.x++
1038 // Wrap around to the right
1039 if (data.puzzle.pillar === true && pos.x >= data.puzzle.width) {
1040 pos.x -= data.puzzle.width
1041 bbox.shift('left', data.puzzle.width * 41 - 82)
1042 bbox.shift('left', 24)
1043 } else {
1044 bbox.shift('right', (pos.x%2 === 0 ? 24 : 58))
1045 }
1046 } else if (moveDir === MOVE_TOP) {
1047 pos.y--
1048 bbox.shift('top', (pos.y%2 === 0 ? 24 : 58))
1049 } else if (moveDir === MOVE_BOTTOM) {
1050 pos.y++
1051 bbox.shift('bottom', (pos.y%2 === 0 ? 24 : 58))
1052 }
1053}
1054
1055})