mumuki-puzzle-runner 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;