-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
480 lines (385 loc) · 15.4 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
<!DOCTYPE html>
<!-- adapted from url=https://javascript.info/mouse-drag-and-drop -->
<!-- saved from url=(0051)https://en.js.cx/article/mouse-drag-and-drop/ball3/ -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="PersistentRingBuffer.js"></script>
</head>
<body style="height: 200px">
<p>Drag the Disks to the pegs. Once a disk is on a peg, drag to rotate. Drag away to move pegs again. <button id="ResetButton">Reset</button> <button id="BackButton">Back</button></p>
<h1 id="solved">SOLVED</h1>
<img src="assets/Disc 1.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 15px; top: 45px;" id="Disc1">
<img src="assets/Disc 4.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 15px; top: 85px;" id="Disc4">
<img src="assets/Disc 9.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 15px; top: 125px;" id="Disc9">
<img src="assets/Disc 11.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 15px; top: 165px;" id="Disc11">
<img src="assets/Disc 8.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 74px; top: 252px;" id="Disc8">
<img src="assets/Disc 3.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 104px; top: 310px;" id="Disc3">
<img src="assets/Disc 10.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 109px; top: 353px;" id="Disc10">
<img src="assets/Disc 5.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 119px; top: 393px;" id="Disc5">
<img src="assets/Disc 2.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 132px; top: 428px;" id="Disc2">
<img src="assets/Disc 7.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 147px; top: 457px;" id="Disc7">
<img src="assets/Disc 6.png" style="cursor: pointer; position: absolute; z-index: 1000; left: 148px; top: 478px;" id="Disc6">
<script>
"use strict";
/* CSS uses degrees, so we'll convert to those units */
const RADIANS_TO_DEGREES = 180 / Math.PI;
// Override the default drag and drop behavior of the browser.
let noDragFunc = function() { return false; };
/* The class Peg is used to create the HTML element for the peg positions, and snap the discs to
those positions later. */
class Peg {
constructor(left, top) {
this.left = left;
this.top = top;
let peg = document.createElement("img");
peg.src = "assets/Peg.png";
peg.style.position = "absolute";
peg.style.zIndex = 900;
peg.style.left = this.left + "px";
peg.style.top = this.top + "px";
peg.width = 20;
peg.height = 20;
document.body.appendChild(peg);
}
snap(position) {
if (Math.abs(position.x - this.left) < 50 && Math.abs(position.y - this.top) < 50) {
position.x = this.left + 9;
position.y = this.top + 9;
position.snapped = true;
}
return position;
}
}
/* The class Disc and variable discs are to assign the specific disc numbers to be used later on
in the code. name is for the string of the disc, size is for the radius of the disc, defaultX is
for the default X position and same for defaultY except for the Y position.*/
class Disc {
// This will cache a map of all discs, keyed by their name.
static byName = new Map();
constructor(name, size, defaultX, defaultY, finalX, finalY) {
this.name = name;
this.size = size;
this.defaultX = defaultX;
this.defaultY = defaultY;
this.finalX = finalX;
this.finalY = finalY;
this.element = document.getElementById(name);
this.element.ondragstart = noDragFunc;
this.element.onmousedown = discMover.startMove;
this.element.disc = this; // enable a reverse lookup in diskMove
Disc.byName.set(this.name, this); // enable a reverse lookup from undos
if (!this.loadState()) { // load position and rotation from localStorage
this.resetState(); // or reset and save it if it wasn't in localStorage
this.saveState();
}
}
// Retrieve and return the last transform saved to localStorage. Not necessarily the current transform.
getSavedState() {
return JSON.parse(localStorage.getItem(this.name + 'Xform'));
}
// Retrieve and return the current state. Not necessarily what is saved in localStorage.
getState() {
return {
name: this.name, // we can use this to record the whole state
left: this.element.style.left,
top: this.element.style.top,
rotate: this.rotation,
onPeg: this.onPeg,
};
}
// Set the current transform, without changing the transform in localStorage.
setState(state) {
this.element.style.left = state.left;
this.element.style.top = state.top;
this.rotation = state.rotate;
this.onPeg = state.onPeg;
}
loadState() {
/* This function loads the position and rotation from localStorage */
let state = this.getSavedState();
if (state == null) {
return false;
}
this.setState(state);
return true;
}
saveState() {
/* This stores the place of where the Disc was if the player were to want
to close the window and come back later. */
localStorage.setItem(this.name + 'Xform', JSON.stringify(this.getState()));
}
resetState() {
// Reset the transform to the default location and random rotation
this.element.style.left = this.defaultX + 'px';
this.element.style.top = this.defaultY + 'px';
//sets random disk rotation
this.rotation = Math.random() * 360;
this.onPeg = false;
// overwrite the previous saved transform
this.saveState();
}
getScrolledBounds() {
// This object has the pixel positions of each edge of the disc element.
let rect = this.element.getBoundingClientRect();
let result = {
left: rect.left + window.scrollX,
right: rect.right + window.scrollX,
top: rect.top + window.scrollY,
bottom: rect.bottom + window.scrollY,
}
return result;
}
getScrolledCenter() {
// This retrieves the center of the disc element, regardless of rotation.
let rect = this.getScrolledBounds();
let result = {
x: (rect.left + rect.right)/2,
y: (rect.top + rect.bottom)/2,
}
return result;
}
setScrolledCenter(x, y) {
this.element.style.left = x - this.size + 'px';
this.element.style.top = y - this.size + 'px';
}
get rotation() {
return this._rotation;
}
set rotation(rot) {
rot = Math.round(rot);
/* Replace negative rotations with positive ones. */
if (rot < 0) rot += 360;
//While loop so that disc rotation doesn't exceed 360
while (rot > 360) {
rot = rot - 360;
}
this._rotation = rot;
/* Assign the CSS transform attribute */
this.element.style.transform = "rotate(" + rot + "deg)";
}
isCorrect() {
/*checks if the discs are on the correct pegs and are on the correct rotations(technically the correct
rotations are 0 sine the correct position will always be no rotation relitive to the starting png element)*/
if (this.element.style.left.match(/\d+/)[0] == this.finalX &&
this.element.style.top.match(/\d+/)[0] == this.finalY &&
((this.element.style.transform.match(/\d+/)[0] < 10) || (this.element.style.transform.match(/\d+/)[0] > 350))) {
return true;
} else {
return false;
}
}
}
class DiscMover {
constructor() {
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.move = this.move.bind(this);
}
startMove(event) {
// Every disc element has a disc object attached to it
this.disc = event.target.disc;
// Cache the center of this disc;
this.center = this.disc.getScrolledCenter();
// Are we snapped to a peg at the end of the move?
this.snapped = this.disc.onPeg;
/* To handle dragging, we just keep track of where we started, relative to the
center of the disc. */
this.startVecX = event.pageX - this.center.x;
this.startVecY = event.pageY - this.center.y;
/* To handle rotation mode, we consider startVec as an
imaginary vector from the center of the object to the mouse position.
As we move the mouse around, the angle between the original vector and
the current vector is how much we need to rotate. We can get the starting
angle using the arctangent: */
let startAngle = Math.atan2(this.startVecY, this.startVecX) * RADIANS_TO_DEGREES;
/* Sometimes the object already has some rotation! In that
case we would compute the final rotation as
finalRotation = startRotation + (endAngle - startAngle)
But only the endAngle is actually changing, so we can rewrite this as:
angleOffset = startRotation - startAngle;
finalRotation = angleOffset + endAngle;
angleOffset only needs to be computed once: */
this.angleOffset = this.disc.rotation - startAngle;
this.rotateMode = event.shiftKey;
/* Assign the appropriate handler. Note that an object covering the whole window
- in this case the document itself - has to be the target, since we
might move the pointer off the object. */
document.addEventListener('mousemove', this.move);
// Make sure we clean up when the mouse lifts up.
document.onmouseup = this.endMove;
}
endMove(event) {
// When the move is over, remove the event listeners.
document.removeEventListener('mousemove', this.move);
document.onmouseup = null;
// Push an array containing one entry: the previously saved transform of this disc.
undoStack.push([this.disc.getSavedState()]);
this.disc.saveState();
// Don't set this until the move is completed
this.disc.onPeg = this.snapped;
checkCompletion();
}
translate(event) {
/* As we move the mouse, keep that offset constant with the current position. */
let center = {
x: event.pageX - this.startVecX,
y: event.pageY - this.startVecY,
snapped: false,
}
center = pegs.reduce((position, peg) => peg.snap(position), center);
this.snapped = center.snapped;
if (this.snapped) {
soundManager.stop(); // make the plunk sound!
} else {
soundManager.start(); // keep scraping
}
/* Finally, set the disc to that location */
this.disc.setScrolledCenter(center.x, center.y);
}
rotate(event) {
/* Find the vector from center to current mouse position. */
let endVecX = event.pageX - this.center.x;
let endVecY = event.pageY - this.center.y;
/* Compute the angle of that vector in degrees: */
let endAngle = Math.atan2(endVecY, endVecX) * RADIANS_TO_DEGREES;
let lastRotation = this.disc.rotation;
/* Using the formula above, update the ball rotation. */
this.disc.rotation = endAngle + this.angleOffset;
if (this.disc.rotation != lastRotation) {
soundManager.start(); // only scraping
}
}
move(event) {
/* Find the vector from center to current mouse position. */
let endVecX = event.pageX - this.center.x;
let endVecY = event.pageY - this.center.y;
let startLength = Math.sqrt(this.startVecX**2 + this.startVecY**2);
let endLength = Math.sqrt(endVecX**2 + endVecY**2);
let currentCenter = this.disc.getScrolledCenter();
let centersUnchanged = Math.abs(this.center.x - currentCenter.x) < .01 &&
Math.abs(this.center.y - currentCenter.y) < .01;
/* Only rotate if the disc is on the same peg it started on, and the radius
of the vector to the center is not stretched more than 50 pixels */
if (this.disc.onPeg && centersUnchanged && endLength - startLength < 50) {
this.rotate(event);
} else {
this.disc.onPeg = false;
this.translate(event);
}
}
}
class SoundManager {
constructor() {
this.scrapeSound = new Audio("assets/scrape.mp3");
this.scrapeSound.loop = true;
this.dropSound = new Audio("assets/drop.mp3");
let sm = this;
this.scrapeSound.addEventListener('timeupdate', function(){
// If we haven't been started in a while, pause.
if (Date.now() - sm.lastStart > 100) {
this.pause();
sm.playing = false;
return;
}
// If we're near the end of the sound, repeat it.
let buffer = .5;
if(this.currentTime > this.duration - buffer){
this.currentTime = 0;
this.play();
}
});
this.playing = false;
}
start() {
// Keep track of when we last requested a start
this.lastStart = Date.now();
// Can only be started when stopped.
if (this.playing) {
return;
}
this.scrapeSound.play();
this.playing = true;
}
stop() {
// Can only be stopped when started.
if (!this.playing) {
return;
}
this.scrapeSound.pause();
this.dropSound.currentTime = 0.23; // queues up the thunk for less delay.
this.dropSound.play();
this.playing = false;
}
}
function checkCompletion() {
/*This function checks if all the Discs are in the correct position to end the game*/
let discsCorrect = discs.map(disc => disc.isCorrect())
.reduce( (accumulator, currentValue) => accumulator + currentValue, 0);
if (discsCorrect === 11) {
//If all the discs are in the right place the completion function runs
solved.style.display = "block";
} else {
//If all the discs aren't in the right position nothing happens
solved.style.display = "none";
};
}
var discMover = new DiscMover(); // a singleton
var soundManager = new SoundManager();
//Array is set up for each peg position in the X and Y cordinates.
var pegs = [
new Peg(556, 211),
new Peg(779, 196),
new Peg(904, 254),
new Peg(1155, 212),
new Peg(768, 320),
new Peg(990, 341),
new Peg(696, 400),
new Peg(889, 453),
new Peg(553, 555),
new Peg(794, 619),
new Peg(1155, 554)
]
var discs = [
new Disc("Disc1", 174, 15, 45, 0b110000111, 0b101110),
new Disc("Disc2", 57, 132, 428, 0b1011011011, 0b10010100),
new Disc("Disc3", 85, 104, 310, 0b1100111100, 0b10110010),
new Disc("Disc4", 174, 15, 85, 0b1111011110, 0b101111),
new Disc("Disc5", 70, 119, 393, 0b1011000011, 0b100000011),
new Disc("Disc6", 41, 148, 478, 0b1110111110, 0b100110101),
new Disc("Disc7", 42, 147, 457, 0b1010010111, 0b101101111),
new Disc("Disc8", 115, 74, 252, 0b1100001111, 0b101011011),
new Disc("Disc9", 174, 15, 125, 0b110000100, 0b110000110),
new Disc("Disc10", 80, 109, 353, 0b1011010011, 0b1000100100),
new Disc("Disc11", 174, 15, 165, 0b1111011110, 0b110000101)
];
let ResetButton = document.getElementById("ResetButton");
let BackButton = document.getElementById("BackButton");
let SolvedSignal = document.getElementById("solved");
let undoStack = new PersistentRingBuffer("undo", 20); // save 20 previous actions
/*Simple reset button. Just resets all of the X and Y cordinates for all the Discs. As well as the rotations.*/
ResetButton.onmousedown = function() {
// Save the previous state of ALL the discs in the undo stack
undoStack.push(discs.map(disc => disc.getSavedState()));
//Sets all discs to their default positions
discs.forEach(disc => disc.resetState());
}
//This function undoes the user's previous action
BackButton.onmousedown = function() {
// Get the most recent element on the undo stack
let states = undoStack.pop();
// Check if the stack was empty.
if (states === undefined) {
return;
}
// Loop through all the states in the undo operation:
for (const state of states) {
// Get the corresponding disc
let disc = Disc.byName.get(state.name);
// Update its transform
disc.setState(state);
// Ensure that transform is saved in localStorage
disc.saveState();
}
}
checkCompletion();
</script>
</body></html>