mumuki-puzzle-runner 0.1.2 → 0.6.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: 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;