mumuki-puzzle-runner 0.1.0 → 0.5.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: c06f3a9c0ab060ce1746fbd08687e44bdd3edc9d2bd7bc9f27991cfa56d34e96
4
- data.tar.gz: 3fd982c69ed60580166016ad4824be96a67b5480d238311e347c7588e86ededd
3
+ metadata.gz: 2b959c734b9bad4afe953699049bf7ecc09a37176eae6d38fdb9734a63e5a1d2
4
+ data.tar.gz: bd617cf58d7bd0d870d4d620dfca48dc329dd560786a0f0b9a912cea3fa6d400
5
5
  SHA512:
6
- metadata.gz: 594855995d69d145ceb3294c3bfc571d681a18e5983e9130884bde9508feea77519764066ceb7fa6b843923dfebc728bfe8a5cf3f08058c9aacc5362f4e784c5
7
- data.tar.gz: 6f2d79e5203433e39a8d0bcea6ca80442f8eac32e3eb3d72f04fbadd82490578836444f0c57ad93163d74c9bbbcbcec268b04c97a77d33a2152d191b73cdfba8
6
+ metadata.gz: 8972354d9f4c3c474e497ae29f50b0b09025b0da69a4b632bcdfabf8cbc7df18480ee78a0a3450af5d904c9bdb235ced2eada0b4aa52c8ce2ec55b026f4d255f
7
+ data.tar.gz: a5aea1cba3903c0032f55dd35c148172272aedd8a0b9e3cea7b7cd383ce75a82a8120557ef1d8088eaa722d90de301c2a07bd62ea79ca417e4d3aca83f12de37
@@ -3,14 +3,15 @@ class PuzzleMetadataHook < Mumukit::Hook
3
3
  {
4
4
  language: {
5
5
  name: 'muzzle',
6
- version: '1.0.0',
6
+ version: PuzzleVersionHook::VERSION,
7
7
  extension: 'js',
8
8
  ace_mode: 'javascript'
9
9
  },
10
10
  test_framework: {
11
11
  name: 'muzzle',
12
- version: '1.0.0',
13
- test_extension: 'js'
12
+ version: PuzzleVersionHook::VERSION,
13
+ test_extension: 'js',
14
+ template: "// see more examples at https://github.com/mumuki/mumuki-puzzle-runner\nMuzzle.basic(3, 2, 'https://flbulgarelli.github.io/headbreaker/static/berni.jpg');"
14
15
  },
15
16
  layout_assets_urls: {
16
17
  js: [
@@ -1,4 +1,13 @@
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
+ }
@@ -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
  // ================
@@ -29,37 +37,44 @@ $(() => {
29
37
 
30
38
  // Requiered to actually bind Muzzle's submit to
31
39
  // mumuki's solution processing
32
- const _onSubmit = Muzzle.onSubmit;
33
- Muzzle.onSubmit = (submission) => {
40
+ register('onSubmit', (submission) => {
34
41
  mumuki.submission.processSolution(submission);
35
- _onSubmit(submission);
36
- }
42
+ });
37
43
 
38
44
  // ===========
39
45
  // Kids config
40
46
  // ===========
41
47
 
42
- // Required to make scaler work
43
48
  mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
44
- // nothing
45
- // no state scaling needed
49
+ // no manul image scalling. Leave it to css
46
50
  });
51
+
47
52
  mumuki.kids.registerBlocksAreaScaler(($blocks) => {
48
- // nothing
49
- // no blocks scaling needed
53
+ const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
54
+ Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight));
55
+ });
56
+
57
+ Muzzle.manualScale = true;
58
+
59
+ // ====================
60
+ // Submit button hiding
61
+ // ====================
62
+
63
+ register('onReady', () => {
64
+ if (Muzzle.simple) {
65
+ $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
66
+ }
50
67
  });
51
68
 
52
69
  // ==============
53
70
  // Assets loading
54
71
  // ==============
55
72
 
56
- const _onReady = Muzzle.onReady;
57
- Muzzle.onReady = () => {
73
+ register('onReady', () => {
58
74
  mumuki.assetsLoadedFor('editor');
59
75
  // although layout assets
60
76
  // are actually loaded before this script, puzzle runner is not aimed
61
77
  // to be used without a custom editor
62
78
  mumuki.assetsLoadedFor('layout');
63
- _onReady();
64
- };
79
+ });
65
80
  });
@@ -64,14 +64,22 @@ class MuzzleCanvas {
64
64
  *
65
65
  * @type {number}
66
66
  */
67
- this.canvasWidth = 800;
67
+ this.canvasWidth = 600;
68
68
 
69
69
  /**
70
70
  * Height of canvas
71
71
  *
72
72
  * @type {number}
73
73
  */
74
- this.canvasHeight = 800;
74
+ this.canvasHeight = 600;
75
+
76
+ /**
77
+ * Wether canvas shoud **not** be resized.
78
+ * Default is `false`
79
+ *
80
+ * @type {boolean}
81
+ */
82
+ this.fixedDimensions = false;
75
83
 
76
84
  /**
77
85
  * Size of fill. Set null for perfect-match
@@ -80,6 +88,13 @@ class MuzzleCanvas {
80
88
  */
81
89
  this.borderFill = null;
82
90
 
91
+ /**
92
+ * Canvas line width
93
+ *
94
+ * @type {number}
95
+ */
96
+ this.strokeWidth = 3;
97
+
83
98
  /**
84
99
  * Piece size
85
100
  *
@@ -88,11 +103,22 @@ class MuzzleCanvas {
88
103
  this.pieceSize = 100;
89
104
 
90
105
  /**
91
- * * 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
92
116
  *
93
117
  * @type {boolean}
94
118
  */
95
- this.scaleImageWidthToFit = true;
119
+ this.fitImagesVertically = false;
120
+
121
+ this.manualScale = false;
96
122
 
97
123
  /**
98
124
  * Callback that will be executed
@@ -110,7 +136,19 @@ class MuzzleCanvas {
110
136
  *
111
137
  * @type {string}
112
138
  */
113
- 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;
114
152
 
115
153
  /**
116
154
  * Callback to be executed when submitting puzzle.
@@ -132,18 +170,55 @@ class MuzzleCanvas {
132
170
  this.onValid = () => {};
133
171
  }
134
172
 
173
+
174
+
135
175
  /**
136
176
  */
137
177
  get baseConfig() {
138
- return {
178
+ return Object.assign({
139
179
  width: this.canvasWidth,
140
180
  height: this.canvasHeight,
141
- pieceSize: this.pieceSize,
142
- proximity: this.pieceSize / 5,
143
- borderFill: this.borderFill === null ? this.pieceSize / 10 : this.borderFill,
144
- strokeWidth: 1.5,
181
+ pieceSize: this.adjustedPieceSize,
182
+ proximity: Math.min(this.adjustedPieceSize.x, this.adjustedPieceSize.y) / 5,
183
+ strokeWidth: this.strokeWidth,
145
184
  lineSoftness: 0.18
146
- };
185
+ }, this.outlineConfig);
186
+ }
187
+
188
+ /**
189
+ */
190
+ get outlineConfig() {
191
+ if (this.spiky) {
192
+ return {
193
+ borderFill: this.borderFill === null ? headbreaker.Vector.divide(this.adjustedPieceSize, 10) : this.borderFill,
194
+ }
195
+ } else {
196
+ return {
197
+ borderFill: 0,
198
+ outline: new headbreaker.outline.Rounded({bezelize: true, insertDepth: 3/5, bezelDepth: 9/10}),
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * The piece size, adjusted to the aspect ratio
205
+ *
206
+ * @returns {Vector}
207
+ */
208
+ get adjustedPieceSize() {
209
+ if (!this._adjustedPieceSize) {
210
+ const aspectRatio = this.aspectRatio || 1;
211
+ this._adjustedPieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
212
+ }
213
+ return this._adjustedPieceSize;
214
+ }
215
+
216
+ /**
217
+ * @type {Axis}
218
+ */
219
+ get imageAdjustmentAxis() {
220
+ console.log(this.fitImagesVertically)
221
+ return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
147
222
  }
148
223
 
149
224
  /**
@@ -188,15 +263,25 @@ class MuzzleCanvas {
188
263
  * @returns {Promise<Canvas>} the promise of the built canvas
189
264
  */
190
265
  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
+ }
273
+
191
274
  /**
192
275
  * @todo take all container size
193
276
  **/
194
277
  const image = await this._loadImage(imagePath);
195
278
  /** @type {Canvas} */
196
279
  // @ts-ignore
197
- const canvas = this._createCanvas(image);
280
+ const canvas = this._createCanvas({ image: image });
281
+ canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
198
282
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
199
283
  this._attachBasicValidator(canvas);
284
+ canvas.shuffleGrid(0.8);
200
285
  this._configCanvas(canvas);
201
286
  canvas.onValid(() => {
202
287
  setTimeout(() => {
@@ -218,7 +303,7 @@ class MuzzleCanvas {
218
303
  const count = imagePaths.length;
219
304
  const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
220
305
 
221
- const canvas = this._createCanvas(null);
306
+ const canvas = this._createCanvas();
222
307
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
223
308
 
224
309
  // todo validate
@@ -249,17 +334,20 @@ class MuzzleCanvas {
249
334
  const leftId = `l${i}`;
250
335
  const rightId = `r${i}`;
251
336
 
252
- pushTemplate(leftUrls[i], {id: leftId, left: true, rightTargetId: rightId});
253
- pushTemplate(rightUrls[i], {id: rightId});
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) }});
254
339
  }
255
340
 
256
- leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true}));
257
- rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true}));
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) },}));
258
343
 
344
+ // + Math.max(leftOddUrls.length, rightOddUrls.length)
259
345
  const templates = await Promise.all(templatePromises);
260
346
  /** @type {Canvas} */
261
- const canvas = this._createCanvas();
347
+ const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
348
+ canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
262
349
  templates.forEach(it => canvas.sketchPiece(it));
350
+ canvas.shuffleColumns(0.8);
263
351
  this._attachMatchValidator(canvas);
264
352
  this._configCanvas(canvas);
265
353
  return canvas;
@@ -276,19 +364,11 @@ class MuzzleCanvas {
276
364
 
277
365
  /**
278
366
  * @private
279
- * @param {HTMLImageElement} image
367
+ * @param {any} config
280
368
  * @return {Canvas}
281
369
  */
282
- _createCanvas(image = null) {
283
- return new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
284
- }
285
-
286
- /**
287
- * @private
288
- * @param {HTMLImageElement} image
289
- */
290
- _canvasConfig(image) {
291
- return Object.assign({ image }, this.baseConfig);
370
+ _createCanvas(config = {}) {
371
+ return new headbreaker.Canvas(this.canvasId, Object.assign(config, this.baseConfig));
292
372
  }
293
373
 
294
374
  /**
@@ -332,37 +412,17 @@ class MuzzleCanvas {
332
412
  * @param {object} options
333
413
  * @returns {Promise<object>}
334
414
  */
335
- _createMatchTemplate(imagePath, {id, left = false, rightTargetId = null, odd = false}) {
415
+ _createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false}) {
336
416
  const structure = left ? 'T-N-' : `N-S-`;
337
417
 
338
418
  return this._loadImage(imagePath).then((image) => {
339
- const scale = this._imageScale(image);
340
- const offset = this.baseConfig.borderFill / scale;
341
419
  return {
342
420
  structure,
343
- metadata: {
344
- id,
345
- left,
346
- odd,
347
- rightTargetId,
348
- image: {
349
- scale,
350
- content: image,
351
- offset: { x: offset, y: offset }
352
- }
353
- }
421
+ metadata: { id, left, odd, rightTargetId, image, targetPosition }
354
422
  }
355
423
  });
356
424
  }
357
425
 
358
- /**
359
- * @private
360
- * @param {HTMLImageElement} image
361
- */
362
- _imageScale(image) {
363
- return this.scaleImageWidthToFit ? this.pieceSize / image.width : 1;
364
- }
365
-
366
426
  /**
367
427
  * @private
368
428
  * @param {Canvas} canvas
@@ -372,15 +432,86 @@ class MuzzleCanvas {
372
432
  this._canvas.onValid(() => {
373
433
  setTimeout(() => this.onValid(), 0);
374
434
  });
435
+ this._setUpScaler();
375
436
  this.ready();
376
437
  }
377
438
 
439
+ _setUpScaler() {
440
+ if (this.manualScale) return;
441
+
442
+ ['resize', 'load'].forEach((event) => {
443
+ window.addEventListener(event, () => {
444
+ var container = document.getElementById(this.canvasId);
445
+ this.scale(container.offsetWidth, container.scrollHeight);
446
+ });
447
+ });
448
+ }
449
+
450
+ /**
451
+ * Scales the canvas to the given width and height
452
+ *
453
+ * @param {number} width
454
+ * @param {number} height
455
+ */
456
+ scale(width, height) {
457
+ if (this.fixedDimensions || !this.canvas) return;
458
+ const factor = this.optimalScaleFactor(width, height);
459
+ this.canvas.resize(width, height);
460
+ this.canvas.scale(factor);
461
+ this.canvas.redraw();
462
+ this.focus();
463
+ }
464
+
465
+ /**
466
+ * Focuses the stage around the canvas center
467
+ */
468
+ focus() {
469
+ const stage = this.canvas['__konvaLayer__'].getStage();
470
+
471
+ const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
472
+ const realDiameter = (() => {
473
+ const [xs, ys] = this.coordinates;
474
+
475
+ const minX = Math.min(...xs);
476
+ const minY = Math.min(...ys);
477
+
478
+ const maxX = Math.max(...xs);
479
+ const maxY = Math.max(...ys);
480
+
481
+ return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
482
+ })();
483
+ const diff = headbreaker.Vector.minus(area, realDiameter);
484
+ const semi = headbreaker.Vector.divide(diff, -2);
485
+
486
+ stage.setOffset(semi);
487
+ stage.draw();
488
+ }
489
+
490
+ /**
491
+ * @private
492
+ */
493
+ get coordinates() {
494
+ const points = this.canvas.puzzle.points;
495
+ return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
496
+ }
497
+
498
+ /**
499
+ * @private
500
+ * @param {number} width
501
+ * @param {number} height
502
+ */
503
+ optimalScaleFactor(width, height) {
504
+ const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
505
+ return Math.min(factors.x, factors.y) / 1.75;
506
+ }
507
+
378
508
  /**
379
509
  * Mark Muzzle as ready, loading previous solution
380
510
  * and drawing the canvas
381
511
  */
382
512
  ready() {
383
513
  this.loadPreviousSolution();
514
+ this.resetCoordinates();
384
515
  this.draw();
385
516
  this.onReady();
386
517
  }
@@ -406,6 +537,7 @@ class MuzzleCanvas {
406
537
  */
407
538
  loadSolution(solution) {
408
539
  this.canvas.puzzle.relocateTo(solution.positions);
540
+ this.canvas.puzzle.autoconnect();
409
541
  }
410
542
 
411
543
  /**
@@ -419,11 +551,20 @@ class MuzzleCanvas {
419
551
  } catch (e) {
420
552
  console.warn("Ignoring unparseabe editor value");
421
553
  }
422
- } else {
423
- this.canvas.shuffle(0.8);
424
554
  }
425
555
  }
426
556
 
557
+ /**
558
+ * Translates the pieces so that
559
+ * they start at canvas' coordinates origin
560
+ */
561
+ resetCoordinates() {
562
+ const [xs, ys] = this.coordinates;
563
+ const minX = Math.min(...xs);
564
+ const minY = Math.min(...ys);
565
+ this.canvas.puzzle.translate(-minX, -minY);
566
+ }
567
+
427
568
  // ==========
428
569
  // Submitting
429
570
  // ==========
@@ -470,9 +611,19 @@ const Muzzle = new class extends MuzzleCanvas {
470
611
  super();
471
612
  this.aux = {};
472
613
  }
473
- another = (id) => {
614
+
615
+ /**
616
+ * Creates a suplementary canvas at the element
617
+ * of the given id
618
+ *
619
+ * @param {string} id
620
+ * @returns {MuzzleCanvas}
621
+ */
622
+ another(id) {
474
623
  const muzzle = new MuzzleCanvas(id);
475
624
  Muzzle.aux[id] = muzzle
476
625
  return muzzle;
477
626
  }
478
627
  }
628
+
629
+ window['Muzzle'] = Muzzle;