mumuki-puzzle-runner 0.4.0 → 1.0.2

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: 5da4e03efe5edbeb4e6eadfaf79f5d37e4f5bea50127a26e91c5f6d2cedb2438
4
- data.tar.gz: f0eeb8f873165d98ecfcc04854aa4fb781d4753fc4350bcfed90fd24c184744b
3
+ metadata.gz: babd3a8ed60a9345983363242f5e1fc1bee0bb36a471d136c314ba5fba8c4ed1
4
+ data.tar.gz: 62d411ca5bfeca771b9b78e113be587c72fdbea23d811e91635cae4a9c703e9d
5
5
  SHA512:
6
- metadata.gz: c98b0e5f9fe633c8fb0587b3c948ce0a6d6d79b80619befc28bc4c583578be596b1d8d2d9f61fddbb3241a320424b602af417fca96827c632a9537342d2a62e9
7
- data.tar.gz: f40b2c76b102651f25ca76eda5b82d24f6cb037e92efcbd68299d634ec8a4f8a22477f4639893b780f6e8f44e790761189c43c12a925a5421f8e33416b3273af
6
+ metadata.gz: 5baff4c0cdac71f1d0038d3cced0c45fd852be5e6712cb470cdb601981d01dbf0f230bea99c07cca85d914bbcff2a84a042ee853f18ed94f7e1583559142861f
7
+ data.tar.gz: fa0b70e5c7ce9529c28fda9bb9b1f464c62bcc2f768fa4bd082fe2193684267e2c3e239361c7a4645b18bbbf7f9ab27dd374be0978711e53634e6858d83135a7
@@ -1,3 +1,19 @@
1
+
2
+ /*
3
+ * ============
4
+ * Initial size
5
+ * ============
6
+ */
7
+
8
+ .mu-exercise-content {
9
+ border-radius: 10px;
10
+ border: 1px solid #dddddd;
11
+ }
12
+
13
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
14
+ display: none;
15
+ }
16
+
1
17
  .mu-kids-state-image img {
2
18
  height: 100%;
3
19
  width: auto;
@@ -8,6 +24,33 @@
8
24
  width: 100%;
9
25
  }
10
26
 
11
- .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
12
- display: none;
27
+ /*
28
+ * ====================
29
+ * Submit button hiding
30
+ * ====================
31
+ */
32
+
33
+ /*
34
+ * ===========
35
+ * Blur effect
36
+ * ===========
37
+ */
38
+
39
+ .mu-kids-exercise-workspace.mu-full-workspace .mu-kids-submit-button {
40
+ margin: 0 30px 30px;
41
+ }
42
+
43
+ .mu-kids-exercise-workspace .mu-kids-blocks {
44
+ overflow: hidden;
45
+ }
46
+
47
+ .mu-kids-exercise-workspace .mu-kids-blocks:after {
48
+ content: ' ';
49
+ box-shadow: inset 0 0 30px 30px #FFFFFF;
50
+ position: absolute;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ bottom: 0;
55
+ pointer-events: none;
13
56
  }
@@ -1,12 +1,5 @@
1
1
  // @ts-nocheck
2
2
  $(() => {
3
- function register(event, callback) {
4
- const _event = Muzzle[event];
5
- Muzzle[event] = (...args) => {
6
- callback(...args);
7
- _event(...args);
8
- }
9
- }
10
3
 
11
4
  // ================
12
5
  // Muzzle rendering
@@ -35,9 +28,9 @@ $(() => {
35
28
  getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
36
29
  })
37
30
 
38
- // Requiered to actually bind Muzzle's submit to
31
+ // Required to actually bind Muzzle's submit to
39
32
  // mumuki's solution processing
40
- register('onSubmit', (submission) => {
33
+ Muzzle.register('onSubmit', (submission) => {
41
34
  mumuki.submission.processSolution(submission);
42
35
  });
43
36
 
@@ -45,13 +38,20 @@ $(() => {
45
38
  // Kids config
46
39
  // ===========
47
40
 
48
- mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
49
- // no manul image scalling. Leave it to css
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) + ')');
50
49
  });
51
50
 
52
51
  mumuki.kids.registerBlocksAreaScaler(($blocks) => {
52
+ console.debug("Scaler fired");
53
53
  const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
54
- Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight));
54
+ Muzzle.run(() => Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight)));
55
55
  });
56
56
 
57
57
  Muzzle.manualScale = true;
@@ -60,7 +60,7 @@ $(() => {
60
60
  // Submit button hiding
61
61
  // ====================
62
62
 
63
- register('onReady', () => {
63
+ Muzzle.register('onReady', () => {
64
64
  if (Muzzle.simple) {
65
65
  $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
66
66
  }
@@ -70,7 +70,9 @@ $(() => {
70
70
  // Assets loading
71
71
  // ==============
72
72
 
73
- register('onReady', () => {
73
+ Muzzle.register('onReady', () => {
74
+ console.debug("Muzzle is ready");
75
+
74
76
  mumuki.assetsLoadedFor('editor');
75
77
  // although layout assets
76
78
  // are actually loaded before this script, puzzle runner is not aimed
@@ -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
@@ -142,12 +157,24 @@ class MuzzleCanvas {
142
157
  * Whether the current puzzle can be solved in very few tries.
143
158
  *
144
159
  * Set null for automatic configuration of this property. Basic puzzles will be considered
145
- * basic and match puzzles will be considered non-basic.
160
+ * basic and match puzzles will be considered non-simple.
146
161
  *
147
162
  * @type {boolean}
148
163
  */
149
164
  this.simple = null;
150
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;
177
+
151
178
  /**
152
179
  * Callback to be executed when submitting puzzle.
153
180
  *
@@ -166,32 +193,81 @@ class MuzzleCanvas {
166
193
  * property with any code you need the be called here
167
194
  */
168
195
  this.onValid = () => {};
196
+
197
+ /**
198
+ * @private
199
+ */
200
+ this._ready = false;
201
+ }
202
+
203
+ get painter() {
204
+ return new MuzzlePainter();
169
205
  }
170
206
 
171
207
  /**
172
208
  */
173
209
  get baseConfig() {
174
- const aspectRatio = this.aspectRatio || 1;
175
- const pieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
176
- return {
210
+ return Object.assign({
211
+ preventOffstageDrag: true,
177
212
  width: this.canvasWidth,
178
213
  height: this.canvasHeight,
179
- pieceSize: pieceSize,
180
- proximity: Math.min(pieceSize.x, pieceSize.y) / 5,
181
- 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,
182
216
  strokeWidth: this.strokeWidth,
183
- lineSoftness: 0.18
184
- };
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;
185
253
  }
186
254
 
187
255
  /**
188
256
  * @type {Axis}
189
257
  */
190
258
  get imageAdjustmentAxis() {
191
- console.log(this.fitImagesVertically)
192
259
  return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
193
260
  }
194
261
 
262
+ /**
263
+ * The configured aspect ratio, or 1
264
+ *
265
+ * @type {number}
266
+ */
267
+ get effectiveAspectRatio() {
268
+ return this.aspectRatio || 1;
269
+ }
270
+
195
271
  /**
196
272
  * The currently active canvas, or null if
197
273
  * it has not yet initialized
@@ -205,7 +281,7 @@ class MuzzleCanvas {
205
281
  /**
206
282
  * Draws the - previusly built - current canvas.
207
283
  *
208
- * Prefer {@code this.currentCanvas.redraw()} when performing
284
+ * Prefer `this.currentCanvas.redraw()` when performing
209
285
  * small updates to the pieces.
210
286
  */
211
287
  draw() {
@@ -234,13 +310,9 @@ class MuzzleCanvas {
234
310
  * @returns {Promise<Canvas>} the promise of the built canvas
235
311
  */
236
312
  async basic(x, y, imagePath) {
237
- if (!this.aspectRatio) {
238
- this.aspectRatio = x / y;
239
- }
240
-
241
- if (this.simple === null) {
242
- this.simple = true;
243
- }
313
+ this._config('aspectRatio', y / x);
314
+ this._config('simple', true);
315
+ this._config('shuffler', Muzzle.Shuffler.grid);
244
316
 
245
317
  /**
246
318
  * @todo take all container size
@@ -252,7 +324,6 @@ class MuzzleCanvas {
252
324
  canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
253
325
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
254
326
  this._attachBasicValidator(canvas);
255
- canvas.shuffleGrid(0.8);
256
327
  this._configCanvas(canvas);
257
328
  canvas.onValid(() => {
258
329
  setTimeout(() => {
@@ -265,52 +336,94 @@ class MuzzleCanvas {
265
336
  }
266
337
 
267
338
  /**
268
- * @param {number} x
269
- * @param {number} y
270
- * @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
+ *
271
350
  * @returns {Promise<Canvas>} the promise of the built canvas
272
351
  */
273
- async multi(x, y, imagePaths) {
274
- const count = imagePaths.length;
275
- const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
276
-
277
- const canvas = this._createCanvas();
278
- canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
279
-
280
- // todo validate
281
- // todo set images
282
-
283
- this._configCanvas(canvas);
284
- 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});
285
355
  }
286
356
 
287
357
  /**
288
- * Craates a match puzzle, where left pieces are matched against right pieces,
289
- * 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.
290
361
  *
291
362
  * @param {string[]} leftUrls
292
363
  * @param {string[]} rightUrls must be of the same size of lefts
293
- * @param {string[]} leftOddUrls
294
- * @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
295
368
  * @returns {Promise<Canvas>} the promise of the built canvas
296
369
  */
297
- 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
+
298
378
  /** @private @type {(Promise<Template>)[]} */
299
379
  const templatePromises = [];
300
- const pushTemplate = (config, options) =>
301
- 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
+ });
302
400
 
303
401
  const last = leftUrls.length - 1;
304
402
  for (let i = 0; i <= last; i++) {
305
403
  const leftId = `l${i}`;
306
404
  const rightId = `r${i}`;
307
405
 
308
- pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
309
- 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
+ });
310
413
  }
311
414
 
312
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
313
- 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
+ );
314
427
 
315
428
  // + Math.max(leftOddUrls.length, rightOddUrls.length)
316
429
  const templates = await Promise.all(templatePromises);
@@ -318,7 +431,6 @@ class MuzzleCanvas {
318
431
  const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
319
432
  canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
320
433
  templates.forEach(it => canvas.sketchPiece(it));
321
- canvas.shuffleColumns(0.8);
322
434
  this._attachMatchValidator(canvas);
323
435
  this._configCanvas(canvas);
324
436
  return canvas;
@@ -383,11 +495,11 @@ class MuzzleCanvas {
383
495
  * @param {object} options
384
496
  * @returns {Promise<object>}
385
497
  */
386
- _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}) {
387
499
  const structure = left ? 'T-N-' : `N-S-`;
388
-
389
500
  return this._loadImage(imagePath).then((image) => {
390
501
  return {
502
+ ...(size ? {size} : {}),
391
503
  structure,
392
504
  metadata: { id, left, odd, rightTargetId, image, targetPosition }
393
505
  }
@@ -400,6 +512,7 @@ class MuzzleCanvas {
400
512
  */
401
513
  _configCanvas(canvas) {
402
514
  this._canvas = canvas;
515
+ this._canvas.shuffleWith(0.8, this.shuffler);
403
516
  this._canvas.onValid(() => {
404
517
  setTimeout(() => this.onValid(), 0);
405
518
  });
@@ -412,14 +525,23 @@ class MuzzleCanvas {
412
525
 
413
526
  ['resize', 'load'].forEach((event) => {
414
527
  window.addEventListener(event, () => {
528
+ console.debug("Scaler event fired:", event);
415
529
  var container = document.getElementById(this.canvasId);
416
530
  this.scale(container.offsetWidth, container.scrollHeight);
417
531
  });
418
532
  });
419
533
  }
420
534
 
535
+ /**
536
+ * Scales the canvas to the given width and height
537
+ *
538
+ * @param {number} width
539
+ * @param {number} height
540
+ */
421
541
  scale(width, height) {
422
542
  if (this.fixedDimensions || !this.canvas) return;
543
+
544
+ console.debug("Scaling:", {width, height})
423
545
  const factor = this.optimalScaleFactor(width, height);
424
546
  this.canvas.resize(width, height);
425
547
  this.canvas.scale(factor);
@@ -427,6 +549,9 @@ class MuzzleCanvas {
427
549
  this.focus();
428
550
  }
429
551
 
552
+ /**
553
+ * Focuses the stage around the canvas center
554
+ */
430
555
  focus() {
431
556
  const stage = this.canvas['__konvaLayer__'].getStage();
432
557
 
@@ -440,7 +565,7 @@ class MuzzleCanvas {
440
565
  const maxX = Math.max(...xs);
441
566
  const maxY = Math.max(...ys);
442
567
 
443
- return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
568
+ return headbreaker.vector(maxX - minX, maxY - minY);
444
569
  })();
445
570
  const diff = headbreaker.Vector.minus(area, realDiameter);
446
571
  const semi = headbreaker.Vector.divide(diff, -2);
@@ -449,11 +574,19 @@ class MuzzleCanvas {
449
574
  stage.draw();
450
575
  }
451
576
 
577
+ /**
578
+ * @private
579
+ */
452
580
  get coordinates() {
453
581
  const points = this.canvas.puzzle.points;
454
582
  return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
455
583
  }
456
584
 
585
+ /**
586
+ * @private
587
+ * @param {number} width
588
+ * @param {number} height
589
+ */
457
590
  optimalScaleFactor(width, height) {
458
591
  const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
459
592
  return Math.min(factors.x, factors.y) / 1.75;
@@ -467,9 +600,14 @@ class MuzzleCanvas {
467
600
  this.loadPreviousSolution();
468
601
  this.resetCoordinates();
469
602
  this.draw();
603
+ this._ready = true;
470
604
  this.onReady();
471
605
  }
472
606
 
607
+ isReady() {
608
+ return this._ready;
609
+ }
610
+
473
611
  // ===========
474
612
  // Persistence
475
613
  // ===========
@@ -508,6 +646,10 @@ class MuzzleCanvas {
508
646
  }
509
647
  }
510
648
 
649
+ /**
650
+ * Translates the pieces so that
651
+ * they start at canvas' coordinates origin
652
+ */
511
653
  resetCoordinates() {
512
654
  const [xs, ys] = this.coordinates;
513
655
  const minX = Math.min(...xs);
@@ -554,12 +696,58 @@ class MuzzleCanvas {
554
696
  };
555
697
  }
556
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
+ }
557
743
  }
558
744
 
559
745
  const Muzzle = new class extends MuzzleCanvas {
560
746
  constructor() {
561
747
  super();
562
748
  this.aux = {};
749
+
750
+ this.Shuffler = headbreaker.Shuffler;
563
751
  }
564
752
 
565
753
  /**