mumuki-puzzle-runner 0.1.2 → 0.6.0

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: 441d4b26b7b8308cc9ed1ec7aca6de3c03905a6018af2db5d1d5e9889b72b55d
4
- data.tar.gz: 5ccb1d263bc6e5c60f62d247006faa81d9a5647518939c0e9b2159092a3fdc45
3
+ metadata.gz: 20cbf33422b40662d79aa0662f45011525a7260454eccfb1f64e5f0a4cdab780
4
+ data.tar.gz: e84cc1ba0a61dffe0be0ccb9bd26840ff5e9de7dc3b2d1ecff7a9010ac1a36ef
5
5
  SHA512:
6
- metadata.gz: d68ffd2617b06416d0e377a46c0bbb89f2728267d79866e31dc7afbaea88d9c22f1df2390ae0bac3a662c610ecc1f8234dec6bd9ca658d9714eea4412ef8e2fb
7
- data.tar.gz: 5c83e99fc78f6f4043e65b5da66a1587a4398a3247d322985d5140976601d69feec3ebadcf9c1de0e86b1ce2208055414af1c87da08e97bf809ee256ff856877
6
+ metadata.gz: ac96c26344814c63d596c1cb012781b18799eb683603ec738beea160ace4e677dd650926cd2720a3e4fb85e712d19a432137040233be99a7a69534823c82d655
7
+ data.tar.gz: c56d817d8240de55674cad90192c59f823da755ce9d3eabdf8a4f373cf84be5c22b1e15fdad64c1c5ff0f40770d9102c11fcd65674621910a07bbdccd95ed065
@@ -1,4 +1,28 @@
1
1
  .mu-kids-state-image img {
2
- width: 90%;
2
+ height: 100%;
3
+ width: auto;
3
4
  padding: 30px;
4
5
  }
6
+
7
+ .mu-kids-state.mu-state-initial {
8
+ width: 100%;
9
+ }
10
+
11
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
12
+ display: none;
13
+ }
14
+
15
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-blocks {
16
+ overflow: hidden;
17
+ }
18
+
19
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-blocks:after {
20
+ content: ' ';
21
+ box-shadow: inset 0 0 30px 30px #FFFFFF;
22
+ position: absolute;
23
+ top: 0;
24
+ left: 0;
25
+ right: 0;
26
+ bottom: 0;
27
+ pointer-events: none;
28
+ }
@@ -1,5 +1,13 @@
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
  // Muzzle rendering
5
13
  // ================
@@ -27,40 +35,51 @@ $(() => {
27
35
  getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
28
36
  })
29
37
 
30
- // Requiered to actually bind Muzzle's submit to
38
+ // Required to actually bind Muzzle's submit to
31
39
  // mumuki's solution processing
32
- const _onSubmit = Muzzle.onSubmit;
33
- Muzzle.onSubmit = (submission) => {
34
- console.log(`submitting ${submission}`)
40
+ register('onSubmit', (submission) => {
35
41
  mumuki.submission.processSolution(submission);
36
- _onSubmit(submission);
37
- }
42
+ });
38
43
 
39
44
  // ===========
40
45
  // Kids config
41
46
  // ===========
42
47
 
43
- // Required to make scaler work
44
- mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
45
- // nothing
46
- // no state scaling needed
48
+ mumuki.kids.registerStateScaler(($state, fullMargin) => {
49
+ const $image = $state.find('img');
50
+
51
+ $image.css('transform', 'scale(1)');
52
+ const width = ($state.width() - fullMargin) / $image.width();
53
+ const height = ($state.height() - fullMargin) / $image.height();
54
+ $image.css('transform', 'scale(' + Math.min(width, height) + ')');
47
55
  });
56
+
48
57
  mumuki.kids.registerBlocksAreaScaler(($blocks) => {
49
- // nothing
50
- // no blocks scaling needed
58
+ const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
59
+ Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight));
60
+ });
61
+
62
+ Muzzle.manualScale = true;
63
+
64
+ // ====================
65
+ // Submit button hiding
66
+ // ====================
67
+
68
+ register('onReady', () => {
69
+ if (Muzzle.simple) {
70
+ $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
71
+ }
51
72
  });
52
73
 
53
74
  // ==============
54
75
  // Assets loading
55
76
  // ==============
56
77
 
57
- const _onReady = Muzzle.onReady;
58
- Muzzle.onReady = () => {
78
+ register('onReady', () => {
59
79
  mumuki.assetsLoadedFor('editor');
60
80
  // although layout assets
61
81
  // are actually loaded before this script, puzzle runner is not aimed
62
82
  // to be used without a custom editor
63
83
  mumuki.assetsLoadedFor('layout');
64
- _onReady();
65
- };
84
+ });
66
85
  });
@@ -73,6 +73,14 @@ class MuzzleCanvas {
73
73
  */
74
74
  this.canvasHeight = 600;
75
75
 
76
+ /**
77
+ * Wether canvas shoud **not** be resized.
78
+ * Default is `false`
79
+ *
80
+ * @type {boolean}
81
+ */
82
+ this.fixedDimensions = false;
83
+
76
84
  /**
77
85
  * Size of fill. Set null for perfect-match
78
86
  *
@@ -85,7 +93,7 @@ class MuzzleCanvas {
85
93
  *
86
94
  * @type {number}
87
95
  */
88
- this.strokeWidth = 1.5;
96
+ this.strokeWidth = 3;
89
97
 
90
98
  /**
91
99
  * Piece size
@@ -95,11 +103,22 @@ class MuzzleCanvas {
95
103
  this.pieceSize = 100;
96
104
 
97
105
  /**
98
- * * Whether image's width should be scaled to piece
106
+ * The x:y aspect ratio of the piece. Set null for automatic
107
+ * aspectRatio
108
+ *
109
+ * @type {number}
110
+ */
111
+ this.aspectRatio = null;
112
+
113
+ /**
114
+ * If the images should be adjusted vertically instead of horizontally
115
+ * to puzzle dimensions. `false` by default
99
116
  *
100
117
  * @type {boolean}
101
118
  */
102
- this.scaleImageWidthToFit = true;
119
+ this.fitImagesVertically = false;
120
+
121
+ this.manualScale = false;
103
122
 
104
123
  /**
105
124
  * Callback that will be executed
@@ -117,7 +136,28 @@ class MuzzleCanvas {
117
136
  *
118
137
  * @type {string}
119
138
  */
120
- this.previousSolutionContent = null
139
+ this.previousSolutionContent = null;
140
+
141
+ /**
142
+ * Whether the current puzzle can be solved in very few tries.
143
+ *
144
+ * Set null for automatic configuration of this property. Basic puzzles will be considered
145
+ * basic and match puzzles will be considered non-basic.
146
+ *
147
+ * @type {boolean}
148
+ */
149
+ this.simple = null;
150
+
151
+ this.spiky = false;
152
+
153
+ /**
154
+ * The reference insert axis, used at rounded outline to compute insert internal and external diameters
155
+ * Set null for default computation of axis - no axis reference for basic boards
156
+ * and vertical axis for match
157
+ *
158
+ * @type {Axis}
159
+ * */
160
+ this.referenceInsertAxis = null;
121
161
 
122
162
  /**
123
163
  * Callback to be executed when submitting puzzle.
@@ -139,18 +179,59 @@ class MuzzleCanvas {
139
179
  this.onValid = () => {};
140
180
  }
141
181
 
182
+
183
+
142
184
  /**
143
185
  */
144
186
  get baseConfig() {
145
- return {
187
+ return Object.assign({
146
188
  width: this.canvasWidth,
147
189
  height: this.canvasHeight,
148
- pieceSize: this.pieceSize,
149
- proximity: this.pieceSize / 5,
150
- borderFill: this.borderFill === null ? this.pieceSize / 10 : this.borderFill,
190
+ pieceSize: this.adjustedPieceSize,
191
+ proximity: Math.min(this.adjustedPieceSize.x, this.adjustedPieceSize.y) / 5,
151
192
  strokeWidth: this.strokeWidth,
152
193
  lineSoftness: 0.18
153
- };
194
+ }, this.outlineConfig);
195
+ }
196
+
197
+ /**
198
+ */
199
+ get outlineConfig() {
200
+ if (this.spiky) {
201
+ return {
202
+ borderFill: this.borderFill === null ? headbreaker.Vector.divide(this.adjustedPieceSize, 10) : this.borderFill,
203
+ }
204
+ } else {
205
+ return {
206
+ borderFill: 0,
207
+ outline: new headbreaker.outline.Rounded({
208
+ bezelize: true,
209
+ insertDepth: 3/5,
210
+ bezelDepth: 9/10,
211
+ referenceInsertAxis: this.referenceInsertAxis
212
+ }),
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * The piece size, adjusted to the aspect ratio
219
+ *
220
+ * @returns {Vector}
221
+ */
222
+ get adjustedPieceSize() {
223
+ if (!this._adjustedPieceSize) {
224
+ const aspectRatio = this.aspectRatio || 1;
225
+ this._adjustedPieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
226
+ }
227
+ return this._adjustedPieceSize;
228
+ }
229
+
230
+ /**
231
+ * @type {Axis}
232
+ */
233
+ get imageAdjustmentAxis() {
234
+ return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
154
235
  }
155
236
 
156
237
  /**
@@ -195,15 +276,25 @@ class MuzzleCanvas {
195
276
  * @returns {Promise<Canvas>} the promise of the built canvas
196
277
  */
197
278
  async basic(x, y, imagePath) {
279
+ if (!this.aspectRatio) {
280
+ this.aspectRatio = x / y;
281
+ }
282
+
283
+ if (this.simple === null) {
284
+ this.simple = true;
285
+ }
286
+
198
287
  /**
199
288
  * @todo take all container size
200
289
  **/
201
290
  const image = await this._loadImage(imagePath);
202
291
  /** @type {Canvas} */
203
292
  // @ts-ignore
204
- const canvas = this._createCanvas(image);
293
+ const canvas = this._createCanvas({ image: image });
294
+ canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
205
295
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
206
296
  this._attachBasicValidator(canvas);
297
+ canvas.shuffleGrid(0.8);
207
298
  this._configCanvas(canvas);
208
299
  canvas.onValid(() => {
209
300
  setTimeout(() => {
@@ -225,7 +316,7 @@ class MuzzleCanvas {
225
316
  const count = imagePaths.length;
226
317
  const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
227
318
 
228
- const canvas = this._createCanvas(null);
319
+ const canvas = this._createCanvas();
229
320
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
230
321
 
231
322
  // todo validate
@@ -243,30 +334,70 @@ class MuzzleCanvas {
243
334
  * @param {string[]} rightUrls must be of the same size of lefts
244
335
  * @param {string[]} leftOddUrls
245
336
  * @param {string[]} rightOddUrls
337
+ * @param {number?} rightWidthRatio a multiplicator to apply to the right piece's width
246
338
  * @returns {Promise<Canvas>} the promise of the built canvas
247
339
  */
248
- async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
340
+ async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = [], rightWidthRatio = 1) {
341
+ if (!this.referenceInsertAxis) {
342
+ this.referenceInsertAxis = headbreaker.Vertical;
343
+ }
344
+
249
345
  /** @private @type {(Promise<Template>)[]} */
250
346
  const templatePromises = [];
251
- const pushTemplate = (config, options) =>
252
- templatePromises.push(this._createMatchTemplate(config, options));
347
+
348
+ const rightSize = headbreaker.diameter(headbreaker.Vector.multiply(this.adjustedPieceSize, headbreaker.vector(rightWidthRatio, 1)));
349
+
350
+ const pushTemplate = (path, options) =>
351
+ templatePromises.push(this._createMatchTemplate(path, options));
352
+
353
+ const pushLeftTemplate = (index, path, options) =>
354
+ pushTemplate(path, {
355
+ left: true,
356
+ targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(1, index)),
357
+ ...options
358
+ });
359
+
360
+ const pushRightTemplate = (index, path, options) =>
361
+ pushTemplate(path, {
362
+ size: rightSize,
363
+ targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(2, index)),
364
+ ...options
365
+ });
253
366
 
254
367
  const last = leftUrls.length - 1;
255
368
  for (let i = 0; i <= last; i++) {
256
369
  const leftId = `l${i}`;
257
370
  const rightId = `r${i}`;
258
371
 
259
- pushTemplate(leftUrls[i], {id: leftId, left: true, rightTargetId: rightId});
260
- pushTemplate(rightUrls[i], {id: rightId});
372
+ pushLeftTemplate(i + 1, leftUrls[i], {
373
+ id: leftId,
374
+ rightTargetId: rightId
375
+ });
376
+ pushRightTemplate(i + 1, rightUrls[i], {
377
+ id: rightId
378
+ });
261
379
  }
262
380
 
263
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true}));
264
- rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true}));
265
-
381
+ leftOddUrls.forEach((it, i) =>
382
+ pushLeftTemplate(i + leftUrls.length, it, {
383
+ id: `lo${i}`,
384
+ odd: true
385
+ })
386
+ );
387
+ rightOddUrls.forEach((it, i) =>
388
+ pushRightTemplate(i + rightUrls.length, it, {
389
+ id: `ro${i}`,
390
+ odd: true
391
+ })
392
+ );
393
+
394
+ // + Math.max(leftOddUrls.length, rightOddUrls.length)
266
395
  const templates = await Promise.all(templatePromises);
267
396
  /** @type {Canvas} */
268
- const canvas = this._createCanvas();
397
+ const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
398
+ canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
269
399
  templates.forEach(it => canvas.sketchPiece(it));
400
+ canvas.shuffleColumns(0.8);
270
401
  this._attachMatchValidator(canvas);
271
402
  this._configCanvas(canvas);
272
403
  return canvas;
@@ -283,19 +414,11 @@ class MuzzleCanvas {
283
414
 
284
415
  /**
285
416
  * @private
286
- * @param {HTMLImageElement} image
417
+ * @param {any} config
287
418
  * @return {Canvas}
288
419
  */
289
- _createCanvas(image = null) {
290
- return new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
291
- }
292
-
293
- /**
294
- * @private
295
- * @param {HTMLImageElement} image
296
- */
297
- _canvasConfig(image) {
298
- return Object.assign({ image }, this.baseConfig);
420
+ _createCanvas(config = {}) {
421
+ return new headbreaker.Canvas(this.canvasId, Object.assign(config, this.baseConfig));
299
422
  }
300
423
 
301
424
  /**
@@ -339,37 +462,17 @@ class MuzzleCanvas {
339
462
  * @param {object} options
340
463
  * @returns {Promise<object>}
341
464
  */
342
- _createMatchTemplate(imagePath, {id, left = false, rightTargetId = null, odd = false}) {
465
+ _createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false, size = null}) {
343
466
  const structure = left ? 'T-N-' : `N-S-`;
344
-
345
467
  return this._loadImage(imagePath).then((image) => {
346
- const scale = this._imageScale(image);
347
- const offset = this.baseConfig.borderFill / scale;
348
468
  return {
469
+ ...(size ? {size} : {}),
349
470
  structure,
350
- metadata: {
351
- id,
352
- left,
353
- odd,
354
- rightTargetId,
355
- image: {
356
- scale,
357
- content: image,
358
- offset: { x: offset, y: offset }
359
- }
360
- }
471
+ metadata: { id, left, odd, rightTargetId, image, targetPosition }
361
472
  }
362
473
  });
363
474
  }
364
475
 
365
- /**
366
- * @private
367
- * @param {HTMLImageElement} image
368
- */
369
- _imageScale(image) {
370
- return this.scaleImageWidthToFit ? this.pieceSize / image.width : 1;
371
- }
372
-
373
476
  /**
374
477
  * @private
375
478
  * @param {Canvas} canvas
@@ -379,15 +482,86 @@ class MuzzleCanvas {
379
482
  this._canvas.onValid(() => {
380
483
  setTimeout(() => this.onValid(), 0);
381
484
  });
485
+ this._setUpScaler();
382
486
  this.ready();
383
487
  }
384
488
 
489
+ _setUpScaler() {
490
+ if (this.manualScale) return;
491
+
492
+ ['resize', 'load'].forEach((event) => {
493
+ window.addEventListener(event, () => {
494
+ var container = document.getElementById(this.canvasId);
495
+ this.scale(container.offsetWidth, container.scrollHeight);
496
+ });
497
+ });
498
+ }
499
+
500
+ /**
501
+ * Scales the canvas to the given width and height
502
+ *
503
+ * @param {number} width
504
+ * @param {number} height
505
+ */
506
+ scale(width, height) {
507
+ if (this.fixedDimensions || !this.canvas) return;
508
+ const factor = this.optimalScaleFactor(width, height);
509
+ this.canvas.resize(width, height);
510
+ this.canvas.scale(factor);
511
+ this.canvas.redraw();
512
+ this.focus();
513
+ }
514
+
515
+ /**
516
+ * Focuses the stage around the canvas center
517
+ */
518
+ focus() {
519
+ const stage = this.canvas['__konvaLayer__'].getStage();
520
+
521
+ const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
522
+ const realDiameter = (() => {
523
+ const [xs, ys] = this.coordinates;
524
+
525
+ const minX = Math.min(...xs);
526
+ const minY = Math.min(...ys);
527
+
528
+ const maxX = Math.max(...xs);
529
+ const maxY = Math.max(...ys);
530
+
531
+ return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
532
+ })();
533
+ const diff = headbreaker.Vector.minus(area, realDiameter);
534
+ const semi = headbreaker.Vector.divide(diff, -2);
535
+
536
+ stage.setOffset(semi);
537
+ stage.draw();
538
+ }
539
+
540
+ /**
541
+ * @private
542
+ */
543
+ get coordinates() {
544
+ const points = this.canvas.puzzle.points;
545
+ return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
546
+ }
547
+
548
+ /**
549
+ * @private
550
+ * @param {number} width
551
+ * @param {number} height
552
+ */
553
+ optimalScaleFactor(width, height) {
554
+ const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
555
+ return Math.min(factors.x, factors.y) / 1.75;
556
+ }
557
+
385
558
  /**
386
559
  * Mark Muzzle as ready, loading previous solution
387
560
  * and drawing the canvas
388
561
  */
389
562
  ready() {
390
563
  this.loadPreviousSolution();
564
+ this.resetCoordinates();
391
565
  this.draw();
392
566
  this.onReady();
393
567
  }
@@ -413,6 +587,7 @@ class MuzzleCanvas {
413
587
  */
414
588
  loadSolution(solution) {
415
589
  this.canvas.puzzle.relocateTo(solution.positions);
590
+ this.canvas.puzzle.autoconnect();
416
591
  }
417
592
 
418
593
  /**
@@ -426,11 +601,20 @@ class MuzzleCanvas {
426
601
  } catch (e) {
427
602
  console.warn("Ignoring unparseabe editor value");
428
603
  }
429
- } else {
430
- this.canvas.shuffle(0.8);
431
604
  }
432
605
  }
433
606
 
607
+ /**
608
+ * Translates the pieces so that
609
+ * they start at canvas' coordinates origin
610
+ */
611
+ resetCoordinates() {
612
+ const [xs, ys] = this.coordinates;
613
+ const minX = Math.min(...xs);
614
+ const minY = Math.min(...ys);
615
+ this.canvas.puzzle.translate(-minX, -minY);
616
+ }
617
+
434
618
  // ==========
435
619
  // Submitting
436
620
  // ==========
@@ -491,3 +675,5 @@ const Muzzle = new class extends MuzzleCanvas {
491
675
  return muzzle;
492
676
  }
493
677
  }
678
+
679
+ window['Muzzle'] = Muzzle;