mumuki-puzzle-runner 0.3.0 → 1.0.1

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: '0524959e5526b6f764f91b27d939eaf1869fc21a6aa4682d052d216dea0807f2'
4
- data.tar.gz: 41cd6df65522b8039cfb933c785f392f1c44f23e40b49b06ff8d1b2859506fe8
3
+ metadata.gz: 2f709148cf4642ed88174c0629fe0376dfab9f572418306eaa5ea620b174c78d
4
+ data.tar.gz: f1c8bf12e4aabaf26e94ee345a3ea732b31f8958b7c8aa877ebd889136915411
5
5
  SHA512:
6
- metadata.gz: 6ec608fc1f4162f6664e14b3f3b623602d400a366ffb7f6e66d40df9d9719a65853063692bff98a300c7d9485d297f524d7b0b8a42c7a3a33e3528f02cac3fe4
7
- data.tar.gz: fd403eb6301bdc0dd39f53b4b9fd315b654608cd575076d702f23074e3efde98785202765c879d72be76125f060d2410db619ca7fa7cebc0c71c530878adb88e
6
+ metadata.gz: 3804856c30566ce86e408bb940a7c3a13712fba26045680ba0c9abfacab52c19c54bd4b8fa5f22df5e2e59bbbd6fa649f2994ccfe234c4e84b9923aeac8d1a53
7
+ data.tar.gz: ab0c527e90d2dfa56c17ffd10984f09c671274e1f3f94cb56853a763166ba7ca8b13208e80f21c991cdcf63f0804f98aa15aa5447cd33da76887746e9d6aa8f9
@@ -1,4 +1,52 @@
1
+
2
+ /*
3
+ * ============
4
+ * Initial size
5
+ * ============
6
+ */
7
+
8
+ .mu-exercise-content {
9
+ border-radius: 10px;
10
+ border: 1px solid #dddddd;
11
+ }
12
+
1
13
  .mu-kids-state-image img {
2
- width: 90%;
14
+ height: 100%;
15
+ width: auto;
3
16
  padding: 30px;
4
17
  }
18
+
19
+ .mu-kids-state.mu-state-initial {
20
+ width: 100%;
21
+ }
22
+
23
+ /*
24
+ * ====================
25
+ * Submit button hiding
26
+ * ====================
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,41 +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
- const maxSize = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
49
- Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxSize));
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)));
50
55
  });
51
56
 
52
57
  Muzzle.manualScale = true;
53
58
 
59
+ // ====================
60
+ // Submit button hiding
61
+ // ====================
62
+
63
+ Muzzle.register('onReady', () => {
64
+ if (Muzzle.simple) {
65
+ $('.mu-kids-exercise-workspace').addClass('mu-submitless-exercise');
66
+ }
67
+ });
68
+
54
69
  // ==============
55
70
  // Assets loading
56
71
  // ==============
57
72
 
58
- const _onReady = Muzzle.onReady;
59
- Muzzle.onReady = () => {
73
+ Muzzle.register('onReady', () => {
74
+ console.debug("Muzzle is ready");
75
+
60
76
  mumuki.assetsLoadedFor('editor');
61
77
  // although layout assets
62
78
  // are actually loaded before this script, puzzle runner is not aimed
63
79
  // to be used without a custom editor
64
80
  mumuki.assetsLoadedFor('layout');
65
- _onReady();
66
- };
81
+ });
67
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,14 +114,27 @@ 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;
120
124
 
125
+ /**
126
+ * Wether the scaling should ignore the scaler
127
+ * rise events
128
+ */
121
129
  this.manualScale = false;
122
130
 
131
+ /**
132
+ * The canvas shuffler.
133
+ *
134
+ * Set it null to automatic shuffling algorithm selection.
135
+ */
136
+ this.shuffler = null;
137
+
123
138
  /**
124
139
  * Callback that will be executed
125
140
  * when muzzle has fully loaded and rendered its first
@@ -136,7 +151,29 @@ class MuzzleCanvas {
136
151
  *
137
152
  * @type {string}
138
153
  */
139
- 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;
140
177
 
141
178
  /**
142
179
  * Callback to be executed when submitting puzzle.
@@ -156,32 +193,81 @@ class MuzzleCanvas {
156
193
  * property with any code you need the be called here
157
194
  */
158
195
  this.onValid = () => {};
196
+
197
+ /**
198
+ * @private
199
+ */
200
+ this._ready = false;
201
+ }
202
+
203
+ get painter() {
204
+ return new MuzzlePainter();
159
205
  }
160
206
 
161
207
  /**
162
208
  */
163
209
  get baseConfig() {
164
- const aspectRatio = this.aspectRatio || 1;
165
- const pieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
166
- return {
210
+ return Object.assign({
211
+ preventOffstageDrag: true,
167
212
  width: this.canvasWidth,
168
213
  height: this.canvasHeight,
169
- pieceSize: pieceSize,
170
- proximity: pieceSize.x / 5,
171
- 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,
172
216
  strokeWidth: this.strokeWidth,
173
- lineSoftness: 0.18
174
- };
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;
175
253
  }
176
254
 
177
255
  /**
178
256
  * @type {Axis}
179
257
  */
180
258
  get imageAdjustmentAxis() {
181
- console.log(this.fitImagesVertically)
182
259
  return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
183
260
  }
184
261
 
262
+ /**
263
+ * The configured aspect ratio, or 1
264
+ *
265
+ * @type {number}
266
+ */
267
+ get effectiveAspectRatio() {
268
+ return this.aspectRatio || 1;
269
+ }
270
+
185
271
  /**
186
272
  * The currently active canvas, or null if
187
273
  * it has not yet initialized
@@ -195,7 +281,7 @@ class MuzzleCanvas {
195
281
  /**
196
282
  * Draws the - previusly built - current canvas.
197
283
  *
198
- * Prefer {@code this.currentCanvas.redraw()} when performing
284
+ * Prefer `this.currentCanvas.redraw()` when performing
199
285
  * small updates to the pieces.
200
286
  */
201
287
  draw() {
@@ -224,9 +310,9 @@ class MuzzleCanvas {
224
310
  * @returns {Promise<Canvas>} the promise of the built canvas
225
311
  */
226
312
  async basic(x, y, imagePath) {
227
- if (!this.aspectRatio) {
228
- this.aspectRatio = x / y;
229
- }
313
+ this._config('aspectRatio', y / x);
314
+ this._config('simple', true);
315
+ this._config('shuffler', Muzzle.Shuffler.grid);
230
316
 
231
317
  /**
232
318
  * @todo take all container size
@@ -238,7 +324,6 @@ class MuzzleCanvas {
238
324
  canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
239
325
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
240
326
  this._attachBasicValidator(canvas);
241
- canvas.shuffleGrid(0.8);
242
327
  this._configCanvas(canvas);
243
328
  canvas.onValid(() => {
244
329
  setTimeout(() => {
@@ -251,52 +336,94 @@ class MuzzleCanvas {
251
336
  }
252
337
 
253
338
  /**
254
- * @param {number} x
255
- * @param {number} y
256
- * @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
+ *
257
350
  * @returns {Promise<Canvas>} the promise of the built canvas
258
351
  */
259
- async multi(x, y, imagePaths) {
260
- const count = imagePaths.length;
261
- const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
262
-
263
- const canvas = this._createCanvas();
264
- canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
265
-
266
- // todo validate
267
- // todo set images
268
-
269
- this._configCanvas(canvas);
270
- 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});
271
355
  }
272
356
 
273
357
  /**
274
- * Craates a match puzzle, where left pieces are matched against right pieces,
275
- * 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.
276
361
  *
277
362
  * @param {string[]} leftUrls
278
363
  * @param {string[]} rightUrls must be of the same size of lefts
279
- * @param {string[]} leftOddUrls
280
- * @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
281
368
  * @returns {Promise<Canvas>} the promise of the built canvas
282
369
  */
283
- 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
+
284
378
  /** @private @type {(Promise<Template>)[]} */
285
379
  const templatePromises = [];
286
- const pushTemplate = (config, options) =>
287
- 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
+ });
288
400
 
289
401
  const last = leftUrls.length - 1;
290
402
  for (let i = 0; i <= last; i++) {
291
403
  const leftId = `l${i}`;
292
404
  const rightId = `r${i}`;
293
405
 
294
- pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
295
- 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
+ });
296
413
  }
297
414
 
298
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
299
- 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
+ );
300
427
 
301
428
  // + Math.max(leftOddUrls.length, rightOddUrls.length)
302
429
  const templates = await Promise.all(templatePromises);
@@ -304,7 +431,6 @@ class MuzzleCanvas {
304
431
  const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
305
432
  canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
306
433
  templates.forEach(it => canvas.sketchPiece(it));
307
- canvas.shuffleColumns(0.8);
308
434
  this._attachMatchValidator(canvas);
309
435
  this._configCanvas(canvas);
310
436
  return canvas;
@@ -369,11 +495,11 @@ class MuzzleCanvas {
369
495
  * @param {object} options
370
496
  * @returns {Promise<object>}
371
497
  */
372
- _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}) {
373
499
  const structure = left ? 'T-N-' : `N-S-`;
374
-
375
500
  return this._loadImage(imagePath).then((image) => {
376
501
  return {
502
+ ...(size ? {size} : {}),
377
503
  structure,
378
504
  metadata: { id, left, odd, rightTargetId, image, targetPosition }
379
505
  }
@@ -386,31 +512,81 @@ class MuzzleCanvas {
386
512
  */
387
513
  _configCanvas(canvas) {
388
514
  this._canvas = canvas;
515
+ this._canvas.shuffleWith(0.8, this.shuffler);
389
516
  this._canvas.onValid(() => {
390
517
  setTimeout(() => this.onValid(), 0);
391
518
  });
392
- this._setupScaler();
519
+ this._setUpScaler();
393
520
  this.ready();
394
521
  }
395
522
 
396
- _setupScaler() {
523
+ _setUpScaler() {
397
524
  if (this.manualScale) return;
398
525
 
399
526
  ['resize', 'load'].forEach((event) => {
400
527
  window.addEventListener(event, () => {
528
+ console.debug("Scaler event fired:", event);
401
529
  var container = document.getElementById(this.canvasId);
402
530
  this.scale(container.offsetWidth, container.scrollHeight);
403
531
  });
404
532
  });
405
533
  }
406
534
 
535
+ /**
536
+ * Scales the canvas to the given width and height
537
+ *
538
+ * @param {number} width
539
+ * @param {number} height
540
+ */
407
541
  scale(width, height) {
408
- if (this.fixedDimensions) return;
542
+ if (this.fixedDimensions || !this.canvas) return;
543
+
544
+ console.debug("Scaling:", {width, height})
545
+ const factor = this.optimalScaleFactor(width, height);
409
546
  this.canvas.resize(width, height);
410
- this.canvas.scale(this.optimalScaleFactor(width, height));
547
+ this.canvas.scale(factor);
411
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)];
412
583
  }
413
584
 
585
+ /**
586
+ * @private
587
+ * @param {number} width
588
+ * @param {number} height
589
+ */
414
590
  optimalScaleFactor(width, height) {
415
591
  const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
416
592
  return Math.min(factors.x, factors.y) / 1.75;
@@ -422,10 +598,16 @@ class MuzzleCanvas {
422
598
  */
423
599
  ready() {
424
600
  this.loadPreviousSolution();
601
+ this.resetCoordinates();
425
602
  this.draw();
603
+ this._ready = true;
426
604
  this.onReady();
427
605
  }
428
606
 
607
+ isReady() {
608
+ return this._ready;
609
+ }
610
+
429
611
  // ===========
430
612
  // Persistence
431
613
  // ===========
@@ -447,6 +629,7 @@ class MuzzleCanvas {
447
629
  */
448
630
  loadSolution(solution) {
449
631
  this.canvas.puzzle.relocateTo(solution.positions);
632
+ this.canvas.puzzle.autoconnect();
450
633
  }
451
634
 
452
635
  /**
@@ -463,6 +646,17 @@ class MuzzleCanvas {
463
646
  }
464
647
  }
465
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
+
466
660
  // ==========
467
661
  // Submitting
468
662
  // ==========
@@ -502,12 +696,58 @@ class MuzzleCanvas {
502
696
  };
503
697
  }
504
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
+ }
505
743
  }
506
744
 
507
745
  const Muzzle = new class extends MuzzleCanvas {
508
746
  constructor() {
509
747
  super();
510
748
  this.aux = {};
749
+
750
+ this.Shuffler = headbreaker.Shuffler;
511
751
  }
512
752
 
513
753
  /**