mumuki-puzzle-runner 0.3.0 → 1.0.1

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: '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
  /**