mumuki-puzzle-runner 0.5.0 → 1.0.3

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: 2b959c734b9bad4afe953699049bf7ecc09a37176eae6d38fdb9734a63e5a1d2
4
- data.tar.gz: bd617cf58d7bd0d870d4d620dfca48dc329dd560786a0f0b9a912cea3fa6d400
3
+ metadata.gz: 969e3f9fc0351a8ef3700be527ff4a977df0d5be6f49a55d9857255ffda6b9af
4
+ data.tar.gz: f388dce7952f3abd843b27a2f82744d5519716a0a6c5d461fc9842d3f8c0562e
5
5
  SHA512:
6
- metadata.gz: 8972354d9f4c3c474e497ae29f50b0b09025b0da69a4b632bcdfabf8cbc7df18480ee78a0a3450af5d904c9bdb235ced2eada0b4aa52c8ce2ec55b026f4d255f
7
- data.tar.gz: a5aea1cba3903c0032f55dd35c148172272aedd8a0b9e3cea7b7cd383ce75a82a8120557ef1d8088eaa722d90de301c2a07bd62ea79ca417e4d3aca83f12de37
6
+ metadata.gz: 3fb7208972a9c6efa975a6272bb95c9b62613f63a5fea7c6b115058b708d44b199455e2b90f3ebffca01d41076c8a178191e0e960f0889938356253fc91b64af
7
+ data.tar.gz: 8e8f8c653af72c67e7199ff72d6be49f1c790f4a9cd4c700c699f66f8aa913b01ab0b7725ae1c71efca6bb062471e8b799471e3078e26ab32daa457dce186e79
@@ -1,3 +1,15 @@
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
14
  height: 100%;
3
15
  width: auto;
@@ -8,6 +20,33 @@
8
20
  width: 100%;
9
21
  }
10
22
 
11
- .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
12
- display: none;
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;
13
52
  }
@@ -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,9 +60,9 @@ $(() => {
60
60
  // Submit button hiding
61
61
  // ====================
62
62
 
63
- register('onReady', () => {
63
+ Muzzle.register('onReady', () => {
64
64
  if (Muzzle.simple) {
65
- $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
65
+ $('.mu-kids-exercise-workspace').addClass('mu-submitless-exercise');
66
66
  }
67
67
  });
68
68
 
@@ -70,11 +70,28 @@ $(() => {
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
77
79
  // to be used without a custom editor
78
80
  mumuki.assetsLoadedFor('layout');
81
+
82
+ mumuki.I18n.register({
83
+ 'es': {
84
+ 'kindergarten_passed': '¡Muy bien! Armaste el rompecabezas'
85
+ },
86
+ 'es-CL': {
87
+ 'kindergarten_passed': '¡Muy bien! Armaste el rompecabezas'
88
+ },
89
+ 'en': {
90
+ 'kindergarten_passed': 'Very well! You solve the puzzle'
91
+ },
92
+ 'pt': {
93
+ 'kindergarten_passed': 'Muito bom! Você mentou o quebra-cabeça'
94
+ },
95
+ });
79
96
  });
80
97
  });
@@ -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,
@@ -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,7 +157,7 @@ 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
  */
@@ -150,6 +165,16 @@ class MuzzleCanvas {
150
165
 
151
166
  this.spiky = false;
152
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
+
153
178
  /**
154
179
  * Callback to be executed when submitting puzzle.
155
180
  *
@@ -168,20 +193,29 @@ class MuzzleCanvas {
168
193
  * property with any code you need the be called here
169
194
  */
170
195
  this.onValid = () => {};
171
- }
172
196
 
197
+ /**
198
+ * @private
199
+ */
200
+ this._ready = false;
201
+ }
173
202
 
203
+ get painter() {
204
+ return new MuzzlePainter();
205
+ }
174
206
 
175
207
  /**
176
208
  */
177
209
  get baseConfig() {
178
210
  return Object.assign({
211
+ preventOffstageDrag: true,
179
212
  width: this.canvasWidth,
180
213
  height: this.canvasHeight,
181
214
  pieceSize: this.adjustedPieceSize,
182
215
  proximity: Math.min(this.adjustedPieceSize.x, this.adjustedPieceSize.y) / 5,
183
216
  strokeWidth: this.strokeWidth,
184
- lineSoftness: 0.18
217
+ lineSoftness: 0.18,
218
+ painter: this.painter
185
219
  }, this.outlineConfig);
186
220
  }
187
221
 
@@ -195,7 +229,12 @@ class MuzzleCanvas {
195
229
  } else {
196
230
  return {
197
231
  borderFill: 0,
198
- outline: new headbreaker.outline.Rounded({bezelize: true, insertDepth: 3/5, bezelDepth: 9/10}),
232
+ outline: new headbreaker.outline.Rounded({
233
+ bezelize: true,
234
+ insertDepth: 3/5,
235
+ bezelDepth: 9/10,
236
+ referenceInsertAxis: this.referenceInsertAxis
237
+ }),
199
238
  }
200
239
  }
201
240
  }
@@ -207,8 +246,8 @@ class MuzzleCanvas {
207
246
  */
208
247
  get adjustedPieceSize() {
209
248
  if (!this._adjustedPieceSize) {
210
- const aspectRatio = this.aspectRatio || 1;
211
- this._adjustedPieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
249
+ const aspectRatio = this.effectiveAspectRatio;
250
+ this._adjustedPieceSize = headbreaker.vector(this.pieceSize * aspectRatio, this.pieceSize);
212
251
  }
213
252
  return this._adjustedPieceSize;
214
253
  }
@@ -217,10 +256,18 @@ class MuzzleCanvas {
217
256
  * @type {Axis}
218
257
  */
219
258
  get imageAdjustmentAxis() {
220
- console.log(this.fitImagesVertically)
221
259
  return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
222
260
  }
223
261
 
262
+ /**
263
+ * The configured aspect ratio, or 1
264
+ *
265
+ * @type {number}
266
+ */
267
+ get effectiveAspectRatio() {
268
+ return this.aspectRatio || 1;
269
+ }
270
+
224
271
  /**
225
272
  * The currently active canvas, or null if
226
273
  * it has not yet initialized
@@ -234,7 +281,7 @@ class MuzzleCanvas {
234
281
  /**
235
282
  * Draws the - previusly built - current canvas.
236
283
  *
237
- * Prefer {@code this.currentCanvas.redraw()} when performing
284
+ * Prefer `this.currentCanvas.redraw()` when performing
238
285
  * small updates to the pieces.
239
286
  */
240
287
  draw() {
@@ -263,13 +310,9 @@ class MuzzleCanvas {
263
310
  * @returns {Promise<Canvas>} the promise of the built canvas
264
311
  */
265
312
  async basic(x, y, imagePath) {
266
- if (!this.aspectRatio) {
267
- this.aspectRatio = x / y;
268
- }
269
-
270
- if (this.simple === null) {
271
- this.simple = true;
272
- }
313
+ this._config('aspectRatio', y / x);
314
+ this._config('simple', true);
315
+ this._config('shuffler', Muzzle.Shuffler.grid);
273
316
 
274
317
  /**
275
318
  * @todo take all container size
@@ -281,7 +324,6 @@ class MuzzleCanvas {
281
324
  canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
282
325
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
283
326
  this._attachBasicValidator(canvas);
284
- canvas.shuffleGrid(0.8);
285
327
  this._configCanvas(canvas);
286
328
  canvas.onValid(() => {
287
329
  setTimeout(() => {
@@ -294,52 +336,94 @@ class MuzzleCanvas {
294
336
  }
295
337
 
296
338
  /**
297
- * @param {number} x
298
- * @param {number} y
299
- * @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
+ *
300
350
  * @returns {Promise<Canvas>} the promise of the built canvas
301
351
  */
302
- async multi(x, y, imagePaths) {
303
- const count = imagePaths.length;
304
- const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
305
-
306
- const canvas = this._createCanvas();
307
- canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
308
-
309
- // todo validate
310
- // todo set images
311
-
312
- this._configCanvas(canvas);
313
- 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});
314
355
  }
315
356
 
316
357
  /**
317
- * Craates a match puzzle, where left pieces are matched against right pieces,
318
- * 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.
319
361
  *
320
362
  * @param {string[]} leftUrls
321
363
  * @param {string[]} rightUrls must be of the same size of lefts
322
- * @param {string[]} leftOddUrls
323
- * @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
324
368
  * @returns {Promise<Canvas>} the promise of the built canvas
325
369
  */
326
- 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
+
327
378
  /** @private @type {(Promise<Template>)[]} */
328
379
  const templatePromises = [];
329
- const pushTemplate = (config, options) =>
330
- 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
+ });
331
400
 
332
401
  const last = leftUrls.length - 1;
333
402
  for (let i = 0; i <= last; i++) {
334
403
  const leftId = `l${i}`;
335
404
  const rightId = `r${i}`;
336
405
 
337
- pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
338
- 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
+ });
339
413
  }
340
414
 
341
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
342
- 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
+ );
343
427
 
344
428
  // + Math.max(leftOddUrls.length, rightOddUrls.length)
345
429
  const templates = await Promise.all(templatePromises);
@@ -347,7 +431,6 @@ class MuzzleCanvas {
347
431
  const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
348
432
  canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
349
433
  templates.forEach(it => canvas.sketchPiece(it));
350
- canvas.shuffleColumns(0.8);
351
434
  this._attachMatchValidator(canvas);
352
435
  this._configCanvas(canvas);
353
436
  return canvas;
@@ -412,11 +495,11 @@ class MuzzleCanvas {
412
495
  * @param {object} options
413
496
  * @returns {Promise<object>}
414
497
  */
415
- _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}) {
416
499
  const structure = left ? 'T-N-' : `N-S-`;
417
-
418
500
  return this._loadImage(imagePath).then((image) => {
419
501
  return {
502
+ ...(size ? {size} : {}),
420
503
  structure,
421
504
  metadata: { id, left, odd, rightTargetId, image, targetPosition }
422
505
  }
@@ -429,6 +512,7 @@ class MuzzleCanvas {
429
512
  */
430
513
  _configCanvas(canvas) {
431
514
  this._canvas = canvas;
515
+ this._canvas.shuffleWith(0.8, this.shuffler);
432
516
  this._canvas.onValid(() => {
433
517
  setTimeout(() => this.onValid(), 0);
434
518
  });
@@ -441,6 +525,7 @@ class MuzzleCanvas {
441
525
 
442
526
  ['resize', 'load'].forEach((event) => {
443
527
  window.addEventListener(event, () => {
528
+ console.debug("Scaler event fired:", event);
444
529
  var container = document.getElementById(this.canvasId);
445
530
  this.scale(container.offsetWidth, container.scrollHeight);
446
531
  });
@@ -455,6 +540,8 @@ class MuzzleCanvas {
455
540
  */
456
541
  scale(width, height) {
457
542
  if (this.fixedDimensions || !this.canvas) return;
543
+
544
+ console.debug("Scaling:", {width, height})
458
545
  const factor = this.optimalScaleFactor(width, height);
459
546
  this.canvas.resize(width, height);
460
547
  this.canvas.scale(factor);
@@ -478,7 +565,7 @@ class MuzzleCanvas {
478
565
  const maxX = Math.max(...xs);
479
566
  const maxY = Math.max(...ys);
480
567
 
481
- return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
568
+ return headbreaker.vector(maxX - minX, maxY - minY);
482
569
  })();
483
570
  const diff = headbreaker.Vector.minus(area, realDiameter);
484
571
  const semi = headbreaker.Vector.divide(diff, -2);
@@ -513,9 +600,14 @@ class MuzzleCanvas {
513
600
  this.loadPreviousSolution();
514
601
  this.resetCoordinates();
515
602
  this.draw();
603
+ this._ready = true;
516
604
  this.onReady();
517
605
  }
518
606
 
607
+ isReady() {
608
+ return this._ready;
609
+ }
610
+
519
611
  // ===========
520
612
  // Persistence
521
613
  // ===========
@@ -604,12 +696,58 @@ class MuzzleCanvas {
604
696
  };
605
697
  }
606
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
+ }
607
743
  }
608
744
 
609
745
  const Muzzle = new class extends MuzzleCanvas {
610
746
  constructor() {
611
747
  super();
612
748
  this.aux = {};
749
+
750
+ this.Shuffler = headbreaker.Shuffler;
613
751
  }
614
752
 
615
753
  /**