mumuki-puzzle-runner 0.4.0 → 1.0.2

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