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 +4 -4
- data/lib/metadata_hook.rb +4 -3
- data/lib/public/css/muzzle-editor.css +10 -1
- data/lib/public/js/muzzle-editor.js +28 -13
- data/lib/public/js/muzzle.js +205 -54
- data/lib/public/vendor/headbreaker.d.ts +243 -65
- data/lib/public/vendor/headbreaker.js +1 -1
- data/lib/public/vendor/headbreaker.js.map +1 -1
- data/lib/version_hook.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b959c734b9bad4afe953699049bf7ecc09a37176eae6d38fdb9734a63e5a1d2
|
4
|
+
data.tar.gz: bd617cf58d7bd0d870d4d620dfca48dc329dd560786a0f0b9a912cea3fa6d400
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8972354d9f4c3c474e497ae29f50b0b09025b0da69a4b632bcdfabf8cbc7df18480ee78a0a3450af5d904c9bdb235ced2eada0b4aa52c8ce2ec55b026f4d255f
|
7
|
+
data.tar.gz: a5aea1cba3903c0032f55dd35c148172272aedd8a0b9e3cea7b7cd383ce75a82a8120557ef1d8088eaa722d90de301c2a07bd62ea79ca417e4d3aca83f12de37
|
data/lib/metadata_hook.rb
CHANGED
@@ -3,14 +3,15 @@ class PuzzleMetadataHook < Mumukit::Hook
|
|
3
3
|
{
|
4
4
|
language: {
|
5
5
|
name: 'muzzle',
|
6
|
-
version:
|
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:
|
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,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
|
-
|
33
|
-
Muzzle.onSubmit = (submission) => {
|
40
|
+
register('onSubmit', (submission) => {
|
34
41
|
mumuki.submission.processSolution(submission);
|
35
|
-
|
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
|
-
//
|
45
|
-
// no state scaling needed
|
49
|
+
// no manul image scalling. Leave it to css
|
46
50
|
});
|
51
|
+
|
47
52
|
mumuki.kids.registerBlocksAreaScaler(($blocks) => {
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
64
|
-
};
|
79
|
+
});
|
65
80
|
});
|
data/lib/public/js/muzzle.js
CHANGED
@@ -64,14 +64,22 @@ class MuzzleCanvas {
|
|
64
64
|
*
|
65
65
|
* @type {number}
|
66
66
|
*/
|
67
|
-
this.canvasWidth =
|
67
|
+
this.canvasWidth = 600;
|
68
68
|
|
69
69
|
/**
|
70
70
|
* Height of canvas
|
71
71
|
*
|
72
72
|
* @type {number}
|
73
73
|
*/
|
74
|
-
this.canvasHeight =
|
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
|
-
*
|
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.
|
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.
|
142
|
-
proximity: this.
|
143
|
-
|
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(
|
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 {
|
367
|
+
* @param {any} config
|
280
368
|
* @return {Canvas}
|
281
369
|
*/
|
282
|
-
_createCanvas(
|
283
|
-
return new headbreaker.Canvas(this.canvasId, this.
|
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
|
-
|
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;
|