mumuki-puzzle-runner 0.1.0 → 0.5.0

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: 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;