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