mumuki-puzzle-runner 0.5.0 → 1.0.3

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