diff options
Diffstat (limited to 'app/assets/javascripts/trace2.js')
| -rw-r--r-- | app/assets/javascripts/trace2.js | 1055 |
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 @@ | |||
| 1 | namespace(function() { | ||
| 2 | |||
| 3 | var BBOX_DEBUG = false | ||
| 4 | |||
| 5 | function clamp(value, min, max) { | ||
| 6 | return value < min ? min : value > max ? max : value | ||
| 7 | } | ||
| 8 | |||
| 9 | class 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 | |||
| 105 | class 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 | |||
| 372 | var data = {} | ||
| 373 | |||
| 374 | function 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 | ||
| 393 | function 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 | |||
| 421 | window.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 | |||
| 491 | window.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 | |||
| 501 | window.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. | ||
| 596 | document.onpointerlockchange = function() { | ||
| 597 | if (document.pointerLockElement == null) unhookMovementEvents() | ||
| 598 | } | ||
| 599 | |||
| 600 | function 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 | |||
| 610 | function 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 | ||
| 659 | var MOVE_NONE = 0 | ||
| 660 | var MOVE_LEFT = 1 | ||
| 661 | var MOVE_RIGHT = 2 | ||
| 662 | var MOVE_TOP = 3 | ||
| 663 | var MOVE_BOTTOM = 4 | ||
| 664 | |||
| 665 | window.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. | ||
| 731 | function 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. | ||
| 781 | function 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. | ||
| 886 | function 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 | ||
| 931 | function 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. | ||
| 1013 | function 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. | ||
| 1025 | function 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 | }) | ||
