mumuki-puzzle-runner 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725586c41840661a2deeb9f2cb179b0ca4181e9e7116f72d5a91cc0deaf2ac44
4
- data.tar.gz: db8a8081c4f4f13e95300abce70767e2fe54649c98da2f0686c5712634fbf71f
3
+ metadata.gz: f021b6da535ba17552c88c6e273d6dcd0aab75e02637420d6859864dac6c31d6
4
+ data.tar.gz: 173cdc9a8f6dbe9b5e83c2c684168db5c2b05c9bbc0c9ae5bce2016c346ffe20
5
5
  SHA512:
6
- metadata.gz: 98d5444f8a2ad9430ed1bb744770e522225a9295c3b1d1a478767c5bf255cb542e27ddfa1ba7ee2f625dc1536f37fe9850715b30501094649073d02caddb683a
7
- data.tar.gz: a69f232d47d551d976d854856b4c40e5be94e8312e0dc2e32db2f8c16321136845fa6aca1565282ed66fe1728f0602142c05452313335fc3e8ba996936eb7016
6
+ metadata.gz: 3263f1c2fd1398d9d159f714cbc0c38308375cde0620bd22f50c21d08a42e17c584bcfc405db4398cca548e69f8dacabee11071e0ae8b26bc51ea795e93483e9
7
+ data.tar.gz: 9419c14f400ae387ead448c2a68ec5063d371c00d92e4247c10c5ac366cc5c7a92b82e98d79bac435326be53633e0455596f9f2caa9f6f31deb0e829a9c2e1e1
@@ -1,4 +1,52 @@
1
+
2
+ /*
3
+ * ============
4
+ * Initial size
5
+ * ============
6
+ */
7
+
8
+
1
9
  .mu-kids-state-image img {
2
- width: 90%;
10
+ height: 100%;
11
+ width: auto;
3
12
  padding: 30px;
4
13
  }
14
+
15
+ .mu-kids-state.mu-state-initial {
16
+ width: 100%;
17
+ }
18
+
19
+ /*
20
+ * ====================
21
+ * Submit button hiding
22
+ * ====================
23
+ */
24
+
25
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
26
+ display: none;
27
+ }
28
+
29
+ /*
30
+ * ===========
31
+ * Blur effect
32
+ * ===========
33
+ */
34
+
35
+ .mu-kids-exercise-workspace.mu-full-workspace .mu-kids-submit-button {
36
+ margin: 0 30px 30px;
37
+ }
38
+
39
+ .mu-kids-exercise-workspace .mu-kids-blocks {
40
+ overflow: hidden;
41
+ }
42
+
43
+ .mu-kids-exercise-workspace .mu-kids-blocks:after {
44
+ content: ' ';
45
+ box-shadow: inset 0 0 30px 30px #FFFFFF;
46
+ position: absolute;
47
+ top: 0;
48
+ left: 0;
49
+ right: 0;
50
+ bottom: 0;
51
+ pointer-events: none;
52
+ }
@@ -1,5 +1,6 @@
1
1
  // @ts-nocheck
2
2
  $(() => {
3
+
3
4
  // ================
4
5
  // Muzzle rendering
5
6
  // ================
@@ -27,39 +28,55 @@ $(() => {
27
28
  getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
28
29
  })
29
30
 
30
- // Requiered to actually bind Muzzle's submit to
31
+ // Required to actually bind Muzzle's submit to
31
32
  // mumuki's solution processing
32
- const _onSubmit = Muzzle.onSubmit;
33
- Muzzle.onSubmit = (submission) => {
33
+ Muzzle.register('onSubmit', (submission) => {
34
34
  mumuki.submission.processSolution(submission);
35
- _onSubmit(submission);
36
- }
35
+ });
37
36
 
38
37
  // ===========
39
38
  // Kids config
40
39
  // ===========
41
40
 
42
- // Required to make scaler work
43
- mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
44
- // nothing
45
- // no state scaling needed
41
+ mumuki.kids.registerStateScaler(($state, fullMargin) => {
42
+ const $image = $state.find('img');
43
+ if (!$image.length) return;
44
+
45
+ $image.css('transform', 'scale(1)');
46
+ const width = ($state.width() - fullMargin) / $image.width();
47
+ const height = ($state.height() - fullMargin) / $image.height();
48
+ $image.css('transform', 'scale(' + Math.min(width, height) + ')');
46
49
  });
50
+
47
51
  mumuki.kids.registerBlocksAreaScaler(($blocks) => {
48
- // nothing
49
- // no blocks scaling needed
52
+ console.debug("Scaler fired");
53
+ const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
54
+ Muzzle.run(() => Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight)));
55
+ });
56
+
57
+ Muzzle.manualScale = true;
58
+
59
+ // ====================
60
+ // Submit button hiding
61
+ // ====================
62
+
63
+ Muzzle.register('onReady', () => {
64
+ if (Muzzle.simple) {
65
+ $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
66
+ }
50
67
  });
51
68
 
52
69
  // ==============
53
70
  // Assets loading
54
71
  // ==============
55
72
 
56
- const _onReady = Muzzle.onReady;
57
- Muzzle.onReady = () => {
73
+ Muzzle.register('onReady', () => {
74
+ console.debug("Muzzle is ready");
75
+
58
76
  mumuki.assetsLoadedFor('editor');
59
77
  // although layout assets
60
78
  // are actually loaded before this script, puzzle runner is not aimed
61
79
  // to be used without a custom editor
62
80
  mumuki.assetsLoadedFor('layout');
63
- _onReady();
64
- };
81
+ });
65
82
  });
@@ -1,9 +1,3 @@
1
- /**
2
- * @typedef {object} PieceConfig
3
- * @property {string} imagePath
4
- * @property {string} structure
5
- */
6
-
7
1
  /**
8
2
  * @typedef {number[]} Point
9
3
  */
@@ -14,6 +8,14 @@
14
8
  */
15
9
 
16
10
 
11
+ class MuzzlePainter extends headbreaker.painters.Konva {
12
+ _newLine(options) {
13
+
14
+ const line = super._newLine(options);
15
+ line.strokeScaleEnabled(false);
16
+ return line;
17
+ }
18
+ }
17
19
 
18
20
  /**
19
21
  * Facade for referencing and creating a global puzzle canvas,
@@ -93,7 +95,7 @@ class MuzzleCanvas {
93
95
  *
94
96
  * @type {number}
95
97
  */
96
- this.strokeWidth = 1.5;
98
+ this.strokeWidth = 3;
97
99
 
98
100
  /**
99
101
  * Piece size
@@ -103,7 +105,7 @@ class MuzzleCanvas {
103
105
  this.pieceSize = 100;
104
106
 
105
107
  /**
106
- * The x:y aspect ratio of the piece. Set null for automatic
108
+ * The `x:y` aspect ratio of the piece. Set null for automatic
107
109
  * aspectRatio
108
110
  *
109
111
  * @type {number}
@@ -112,11 +114,26 @@ class MuzzleCanvas {
112
114
 
113
115
  /**
114
116
  * If the images should be adjusted vertically instead of horizontally
115
- * to puzzle dimensions. `false` by default
117
+ * to puzzle dimensions.
118
+ *
119
+ * Set null for automatic fit.
116
120
  *
117
121
  * @type {boolean}
118
122
  */
119
- this.fitImagesVertically = false;
123
+ this.fitImagesVertically = null;
124
+
125
+ /**
126
+ * Wether the scaling should ignore the scaler
127
+ * rise events
128
+ */
129
+ this.manualScale = false;
130
+
131
+ /**
132
+ * The canvas shuffler.
133
+ *
134
+ * Set it null to automatic shuffling algorithm selection.
135
+ */
136
+ this.shuffler = null;
120
137
 
121
138
  /**
122
139
  * Callback that will be executed
@@ -134,7 +151,29 @@ class MuzzleCanvas {
134
151
  *
135
152
  * @type {string}
136
153
  */
137
- this.previousSolutionContent = null
154
+ this.previousSolutionContent = null;
155
+
156
+ /**
157
+ * Whether the current puzzle can be solved in very few tries.
158
+ *
159
+ * Set null for automatic configuration of this property. Basic puzzles will be considered
160
+ * basic and match puzzles will be considered non-simple.
161
+ *
162
+ * @type {boolean}
163
+ */
164
+ this.simple = null;
165
+
166
+ this.spiky = false;
167
+
168
+ /**
169
+ * The reference insert axis, used at rounded outline to compute insert internal and external diameters
170
+ *
171
+ * Set null for default computation of axis - no axis reference for basic boards
172
+ * and vertical axis for match
173
+ *
174
+ * @type {Axis}
175
+ * */
176
+ this.referenceInsertAxis = null;
138
177
 
139
178
  /**
140
179
  * Callback to be executed when submitting puzzle.
@@ -154,32 +193,81 @@ class MuzzleCanvas {
154
193
  * property with any code you need the be called here
155
194
  */
156
195
  this.onValid = () => {};
196
+
197
+ /**
198
+ * @private
199
+ */
200
+ this._ready = false;
201
+ }
202
+
203
+ get painter() {
204
+ return new MuzzlePainter();
157
205
  }
158
206
 
159
207
  /**
160
208
  */
161
209
  get baseConfig() {
162
- const aspectRatio = this.aspectRatio || 1;
163
- const pieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
164
- return {
210
+ return Object.assign({
211
+ preventOffstageDrag: true,
165
212
  width: this.canvasWidth,
166
213
  height: this.canvasHeight,
167
- pieceSize: pieceSize,
168
- proximity: pieceSize.x / 5,
169
- borderFill: this.borderFill === null ? headbreaker.Vector.divide(pieceSize, 10) : this.borderFill,
214
+ pieceSize: this.adjustedPieceSize,
215
+ proximity: Math.min(this.adjustedPieceSize.x, this.adjustedPieceSize.y) / 5,
170
216
  strokeWidth: this.strokeWidth,
171
- lineSoftness: 0.18
172
- };
217
+ lineSoftness: 0.18,
218
+ painter: this.painter
219
+ }, this.outlineConfig);
220
+ }
221
+
222
+ /**
223
+ */
224
+ get outlineConfig() {
225
+ if (this.spiky) {
226
+ return {
227
+ borderFill: this.borderFill === null ? headbreaker.Vector.divide(this.adjustedPieceSize, 10) : this.borderFill,
228
+ }
229
+ } else {
230
+ return {
231
+ borderFill: 0,
232
+ outline: new headbreaker.outline.Rounded({
233
+ bezelize: true,
234
+ insertDepth: 3/5,
235
+ bezelDepth: 9/10,
236
+ referenceInsertAxis: this.referenceInsertAxis
237
+ }),
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * The piece size, adjusted to the aspect ratio
244
+ *
245
+ * @returns {Vector}
246
+ */
247
+ get adjustedPieceSize() {
248
+ if (!this._adjustedPieceSize) {
249
+ const aspectRatio = this.effectiveAspectRatio;
250
+ this._adjustedPieceSize = headbreaker.vector(this.pieceSize * aspectRatio, this.pieceSize);
251
+ }
252
+ return this._adjustedPieceSize;
173
253
  }
174
254
 
175
255
  /**
176
256
  * @type {Axis}
177
257
  */
178
258
  get imageAdjustmentAxis() {
179
- console.log(this.fitImagesVertically)
180
259
  return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
181
260
  }
182
261
 
262
+ /**
263
+ * The configured aspect ratio, or 1
264
+ *
265
+ * @type {number}
266
+ */
267
+ get effectiveAspectRatio() {
268
+ return this.aspectRatio || 1;
269
+ }
270
+
183
271
  /**
184
272
  * The currently active canvas, or null if
185
273
  * it has not yet initialized
@@ -193,7 +281,7 @@ class MuzzleCanvas {
193
281
  /**
194
282
  * Draws the - previusly built - current canvas.
195
283
  *
196
- * Prefer {@code this.currentCanvas.redraw()} when performing
284
+ * Prefer `this.currentCanvas.redraw()` when performing
197
285
  * small updates to the pieces.
198
286
  */
199
287
  draw() {
@@ -222,9 +310,9 @@ class MuzzleCanvas {
222
310
  * @returns {Promise<Canvas>} the promise of the built canvas
223
311
  */
224
312
  async basic(x, y, imagePath) {
225
- if (!this.aspectRatio) {
226
- this.aspectRatio = x / y;
227
- }
313
+ this._config('aspectRatio', y / x);
314
+ this._config('simple', true);
315
+ this._config('shuffler', Muzzle.Shuffler.grid);
228
316
 
229
317
  /**
230
318
  * @todo take all container size
@@ -236,7 +324,6 @@ class MuzzleCanvas {
236
324
  canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
237
325
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
238
326
  this._attachBasicValidator(canvas);
239
- canvas.shuffleGrid(0.8);
240
327
  this._configCanvas(canvas);
241
328
  canvas.onValid(() => {
242
329
  setTimeout(() => {
@@ -249,52 +336,94 @@ class MuzzleCanvas {
249
336
  }
250
337
 
251
338
  /**
252
- * @param {number} x
253
- * @param {number} y
254
- * @param {string[]} [imagePaths]
339
+ * Creates a choose puzzle, where a single right piece must match the single left piece,
340
+ * choosing the latter from a bunch of other left odd pieces. By default, `Muzzle.Shuffler.line` shuffling is used.
341
+ *
342
+ * This is a particular case of a match puzzle with line
343
+ *
344
+ * @param {string} leftUrl the url of the left piece
345
+ * @param {string} rightUrl the url of the right piece
346
+ * @param {string[]} leftOddUrls the urls of the off left urls
347
+ * @param {number} [rightAspectRatio] the `x:y` ratio of the right pieces, that override the general `aspectRatio` of the puzzle.
348
+ * Use null to have the same aspect ratio as left pieces
349
+ *
255
350
  * @returns {Promise<Canvas>} the promise of the built canvas
256
351
  */
257
- async multi(x, y, imagePaths) {
258
- const count = imagePaths.length;
259
- const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
260
-
261
- const canvas = this._createCanvas();
262
- canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
263
-
264
- // todo validate
265
- // todo set images
266
-
267
- this._configCanvas(canvas);
268
- return canvas;
352
+ async choose(leftUrl, rightUrl, leftOddUrls, rightAspectRatio = null) {
353
+ this._config('shuffler', Muzzle.Shuffler.line);
354
+ return this.match([leftUrl], [rightUrl], {leftOddUrls, rightAspectRatio});
269
355
  }
270
356
 
271
357
  /**
272
- * Craates a match puzzle, where left pieces are matched against right pieces,
273
- * with optional odd left and right pieces that don't match
358
+ * Creates a match puzzle, where left pieces are matched against right pieces,
359
+ * with optional odd left and right pieces that don't match. By default, `Muzzle.Shuffler.columns`
360
+ * shuffling is used.
274
361
  *
275
362
  * @param {string[]} leftUrls
276
363
  * @param {string[]} rightUrls must be of the same size of lefts
277
- * @param {string[]} leftOddUrls
278
- * @param {string[]} rightOddUrls
364
+ * @param {object} [options]
365
+ * @param {string[]} [options.leftOddUrls]
366
+ * @param {string[]} [options.rightOddUrls]
367
+ * @param {number?} [options.rightAspectRatio] the aspect ratio of the right pieces. Use null to have the same aspect ratio as left pieces
279
368
  * @returns {Promise<Canvas>} the promise of the built canvas
280
369
  */
281
- async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
370
+ async match(leftUrls, rightUrls, {leftOddUrls = [], rightOddUrls = [], rightAspectRatio = this.effectiveAspectRatio} = {}) {
371
+ const rightWidthRatio = rightAspectRatio / this.effectiveAspectRatio;
372
+
373
+ this._config('simple', false);
374
+ this._config('shuffler', Muzzle.Shuffler.columns);
375
+ this._config('fitImagesVertically', rightWidthRatio > 1);
376
+ this._config('referenceInsertAxis', headbreaker.Vertical);
377
+
282
378
  /** @private @type {(Promise<Template>)[]} */
283
379
  const templatePromises = [];
284
- const pushTemplate = (config, options) =>
285
- templatePromises.push(this._createMatchTemplate(config, options));
380
+
381
+ const rightSize = headbreaker.diameter(
382
+ headbreaker.Vector.multiply(this.adjustedPieceSize, headbreaker.vector(rightWidthRatio, 1)));
383
+
384
+ const pushTemplate = (path, options) =>
385
+ templatePromises.push(this._createMatchTemplate(path, options));
386
+
387
+ const pushLeftTemplate = (index, path, options) =>
388
+ pushTemplate(path, {
389
+ left: true,
390
+ targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(1, index)),
391
+ ...options
392
+ });
393
+
394
+ const pushRightTemplate = (index, path, options) =>
395
+ pushTemplate(path, {
396
+ size: rightSize,
397
+ targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(2, index)),
398
+ ...options
399
+ });
286
400
 
287
401
  const last = leftUrls.length - 1;
288
402
  for (let i = 0; i <= last; i++) {
289
403
  const leftId = `l${i}`;
290
404
  const rightId = `r${i}`;
291
405
 
292
- pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
293
- pushTemplate(rightUrls[i], {id: rightId, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + 1) }});
406
+ pushLeftTemplate(i + 1, leftUrls[i], {
407
+ id: leftId,
408
+ rightTargetId: rightId
409
+ });
410
+ pushRightTemplate(i + 1, rightUrls[i], {
411
+ id: rightId
412
+ });
294
413
  }
295
414
 
296
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
297
- rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + rightUrls.length) },}));
415
+ leftOddUrls.forEach((it, i) =>
416
+ pushLeftTemplate(i + leftUrls.length, it, {
417
+ id: `lo${i}`,
418
+ odd: true
419
+ })
420
+ );
421
+ rightOddUrls.forEach((it, i) =>
422
+ pushRightTemplate(i + rightUrls.length, it, {
423
+ id: `ro${i}`,
424
+ odd: true
425
+ })
426
+ );
298
427
 
299
428
  // + Math.max(leftOddUrls.length, rightOddUrls.length)
300
429
  const templates = await Promise.all(templatePromises);
@@ -302,7 +431,6 @@ class MuzzleCanvas {
302
431
  const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
303
432
  canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
304
433
  templates.forEach(it => canvas.sketchPiece(it));
305
- canvas.shuffleColumns(0.8);
306
434
  this._attachMatchValidator(canvas);
307
435
  this._configCanvas(canvas);
308
436
  return canvas;
@@ -367,11 +495,11 @@ class MuzzleCanvas {
367
495
  * @param {object} options
368
496
  * @returns {Promise<object>}
369
497
  */
370
- _createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false}) {
498
+ _createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false, size = null}) {
371
499
  const structure = left ? 'T-N-' : `N-S-`;
372
-
373
500
  return this._loadImage(imagePath).then((image) => {
374
501
  return {
502
+ ...(size ? {size} : {}),
375
503
  structure,
376
504
  metadata: { id, left, odd, rightTargetId, image, targetPosition }
377
505
  }
@@ -384,25 +512,85 @@ class MuzzleCanvas {
384
512
  */
385
513
  _configCanvas(canvas) {
386
514
  this._canvas = canvas;
515
+ this._canvas.shuffleWith(0.8, this.shuffler);
387
516
  this._canvas.onValid(() => {
388
517
  setTimeout(() => this.onValid(), 0);
389
518
  });
390
- this._setupResponsiveness();
519
+ this._setUpScaler();
391
520
  this.ready();
392
521
  }
393
522
 
394
- _setupResponsiveness() {
395
- if (this.fixedDimensions) return;
523
+ _setUpScaler() {
524
+ if (this.manualScale) return;
396
525
 
397
526
  ['resize', 'load'].forEach((event) => {
398
527
  window.addEventListener(event, () => {
528
+ console.debug("Scaler event fired:", event);
399
529
  var container = document.getElementById(this.canvasId);
400
- this.canvas.resize(container.offsetWidth, container.scrollHeight);
401
- this.canvas.redraw();
530
+ this.scale(container.offsetWidth, container.scrollHeight);
402
531
  });
403
532
  });
404
533
  }
405
534
 
535
+ /**
536
+ * Scales the canvas to the given width and height
537
+ *
538
+ * @param {number} width
539
+ * @param {number} height
540
+ */
541
+ scale(width, height) {
542
+ if (this.fixedDimensions || !this.canvas) return;
543
+
544
+ console.debug("Scaling:", {width, height})
545
+ const factor = this.optimalScaleFactor(width, height);
546
+ this.canvas.resize(width, height);
547
+ this.canvas.scale(factor);
548
+ this.canvas.redraw();
549
+ this.focus();
550
+ }
551
+
552
+ /**
553
+ * Focuses the stage around the canvas center
554
+ */
555
+ focus() {
556
+ const stage = this.canvas['__konvaLayer__'].getStage();
557
+
558
+ const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
559
+ const realDiameter = (() => {
560
+ const [xs, ys] = this.coordinates;
561
+
562
+ const minX = Math.min(...xs);
563
+ const minY = Math.min(...ys);
564
+
565
+ const maxX = Math.max(...xs);
566
+ const maxY = Math.max(...ys);
567
+
568
+ return headbreaker.vector(maxX - minX, maxY - minY);
569
+ })();
570
+ const diff = headbreaker.Vector.minus(area, realDiameter);
571
+ const semi = headbreaker.Vector.divide(diff, -2);
572
+
573
+ stage.setOffset(semi);
574
+ stage.draw();
575
+ }
576
+
577
+ /**
578
+ * @private
579
+ */
580
+ get coordinates() {
581
+ const points = this.canvas.puzzle.points;
582
+ return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
583
+ }
584
+
585
+ /**
586
+ * @private
587
+ * @param {number} width
588
+ * @param {number} height
589
+ */
590
+ optimalScaleFactor(width, height) {
591
+ const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
592
+ return Math.min(factors.x, factors.y) / 1.75;
593
+ }
406
594
 
407
595
  /**
408
596
  * Mark Muzzle as ready, loading previous solution
@@ -410,10 +598,16 @@ class MuzzleCanvas {
410
598
  */
411
599
  ready() {
412
600
  this.loadPreviousSolution();
601
+ this.resetCoordinates();
413
602
  this.draw();
603
+ this._ready = true;
414
604
  this.onReady();
415
605
  }
416
606
 
607
+ isReady() {
608
+ return this._ready;
609
+ }
610
+
417
611
  // ===========
418
612
  // Persistence
419
613
  // ===========
@@ -435,6 +629,7 @@ class MuzzleCanvas {
435
629
  */
436
630
  loadSolution(solution) {
437
631
  this.canvas.puzzle.relocateTo(solution.positions);
632
+ this.canvas.puzzle.autoconnect();
438
633
  }
439
634
 
440
635
  /**
@@ -451,6 +646,17 @@ class MuzzleCanvas {
451
646
  }
452
647
  }
453
648
 
649
+ /**
650
+ * Translates the pieces so that
651
+ * they start at canvas' coordinates origin
652
+ */
653
+ resetCoordinates() {
654
+ const [xs, ys] = this.coordinates;
655
+ const minX = Math.min(...xs);
656
+ const minY = Math.min(...ys);
657
+ this.canvas.puzzle.translate(-minX, -minY);
658
+ }
659
+
454
660
  // ==========
455
661
  // Submitting
456
662
  // ==========
@@ -490,12 +696,58 @@ class MuzzleCanvas {
490
696
  };
491
697
  }
492
698
 
699
+ /**
700
+ * @param {string} key
701
+ * @param {any} value
702
+ */
703
+ _config(key, value) {
704
+ const current = this[key];
705
+ console.debug("Setting config: ", [key, value])
706
+
707
+ if (current === null) {
708
+ this[key] = value;
709
+ }
710
+ }
711
+
712
+ // ==============
713
+ // Event handling
714
+ // ==============
715
+
716
+
717
+ /**
718
+ * Registers an event handler
719
+ *
720
+ * @param {string} event
721
+ * @param {(...args: any) => void} callback
722
+ */
723
+ register(event, callback) {
724
+ const _event = this[event];
725
+ this[event] = (...args) => {
726
+ callback(...args);
727
+ _event(...args);
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Runs the given action if muzzle is ready,
733
+ * queueing it otherwise
734
+ * @param {() => void} callback
735
+ */
736
+ run(callback) {
737
+ if (this.isReady()) {
738
+ callback();
739
+ } else {
740
+ this.register('onReady', callback);
741
+ }
742
+ }
493
743
  }
494
744
 
495
745
  const Muzzle = new class extends MuzzleCanvas {
496
746
  constructor() {
497
747
  super();
498
748
  this.aux = {};
749
+
750
+ this.Shuffler = headbreaker.Shuffler;
499
751
  }
500
752
 
501
753
  /**
@@ -511,3 +763,5 @@ const Muzzle = new class extends MuzzleCanvas {
511
763
  return muzzle;
512
764
  }
513
765
  }
766
+
767
+ window['Muzzle'] = Muzzle;