mumuki-puzzle-runner 0.2.0 → 1.0.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/public/css/muzzle-editor.css +49 -1
- data/lib/public/js/muzzle-editor.js +32 -15
- data/lib/public/js/muzzle.js +313 -59
- data/lib/public/vendor/headbreaker.d.ts +47 -7
- 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 +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f021b6da535ba17552c88c6e273d6dcd0aab75e02637420d6859864dac6c31d6
|
4
|
+
data.tar.gz: 173cdc9a8f6dbe9b5e83c2c684168db5c2b05c9bbc0c9ae5bce2016c346ffe20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3263f1c2fd1398d9d159f714cbc0c38308375cde0620bd22f50c21d08a42e17c584bcfc405db4398cca548e69f8dacabee11071e0ae8b26bc51ea795e93483e9
|
7
|
+
data.tar.gz: 9419c14f400ae387ead448c2a68ec5063d371c00d92e4247c10c5ac366cc5c7a92b82e98d79bac435326be53633e0455596f9f2caa9f6f31deb0e829a9c2e1e1
|
@@ -1,4 +1,52 @@
|
|
1
|
+
|
2
|
+
/*
|
3
|
+
* ============
|
4
|
+
* Initial size
|
5
|
+
* ============
|
6
|
+
*/
|
7
|
+
|
8
|
+
|
1
9
|
.mu-kids-state-image img {
|
2
|
-
|
10
|
+
height: 100%;
|
11
|
+
width: auto;
|
3
12
|
padding: 30px;
|
4
13
|
}
|
14
|
+
|
15
|
+
.mu-kids-state.mu-state-initial {
|
16
|
+
width: 100%;
|
17
|
+
}
|
18
|
+
|
19
|
+
/*
|
20
|
+
* ====================
|
21
|
+
* Submit button hiding
|
22
|
+
* ====================
|
23
|
+
*/
|
24
|
+
|
25
|
+
.mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
|
26
|
+
display: none;
|
27
|
+
}
|
28
|
+
|
29
|
+
/*
|
30
|
+
* ===========
|
31
|
+
* Blur effect
|
32
|
+
* ===========
|
33
|
+
*/
|
34
|
+
|
35
|
+
.mu-kids-exercise-workspace.mu-full-workspace .mu-kids-submit-button {
|
36
|
+
margin: 0 30px 30px;
|
37
|
+
}
|
38
|
+
|
39
|
+
.mu-kids-exercise-workspace .mu-kids-blocks {
|
40
|
+
overflow: hidden;
|
41
|
+
}
|
42
|
+
|
43
|
+
.mu-kids-exercise-workspace .mu-kids-blocks:after {
|
44
|
+
content: ' ';
|
45
|
+
box-shadow: inset 0 0 30px 30px #FFFFFF;
|
46
|
+
position: absolute;
|
47
|
+
top: 0;
|
48
|
+
left: 0;
|
49
|
+
right: 0;
|
50
|
+
bottom: 0;
|
51
|
+
pointer-events: none;
|
52
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
// @ts-nocheck
|
2
2
|
$(() => {
|
3
|
+
|
3
4
|
// ================
|
4
5
|
// Muzzle rendering
|
5
6
|
// ================
|
@@ -27,39 +28,55 @@ $(() => {
|
|
27
28
|
getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
|
28
29
|
})
|
29
30
|
|
30
|
-
//
|
31
|
+
// Required to actually bind Muzzle's submit to
|
31
32
|
// mumuki's solution processing
|
32
|
-
|
33
|
-
Muzzle.onSubmit = (submission) => {
|
33
|
+
Muzzle.register('onSubmit', (submission) => {
|
34
34
|
mumuki.submission.processSolution(submission);
|
35
|
-
|
36
|
-
}
|
35
|
+
});
|
37
36
|
|
38
37
|
// ===========
|
39
38
|
// Kids config
|
40
39
|
// ===========
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
mumuki.kids.registerStateScaler(($state, fullMargin) => {
|
42
|
+
const $image = $state.find('img');
|
43
|
+
if (!$image.length) return;
|
44
|
+
|
45
|
+
$image.css('transform', 'scale(1)');
|
46
|
+
const width = ($state.width() - fullMargin) / $image.width();
|
47
|
+
const height = ($state.height() - fullMargin) / $image.height();
|
48
|
+
$image.css('transform', 'scale(' + Math.min(width, height) + ')');
|
46
49
|
});
|
50
|
+
|
47
51
|
mumuki.kids.registerBlocksAreaScaler(($blocks) => {
|
48
|
-
|
49
|
-
|
52
|
+
console.debug("Scaler fired");
|
53
|
+
const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
|
54
|
+
Muzzle.run(() => Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight)));
|
55
|
+
});
|
56
|
+
|
57
|
+
Muzzle.manualScale = true;
|
58
|
+
|
59
|
+
// ====================
|
60
|
+
// Submit button hiding
|
61
|
+
// ====================
|
62
|
+
|
63
|
+
Muzzle.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
|
-
|
73
|
+
Muzzle.register('onReady', () => {
|
74
|
+
console.debug("Muzzle is ready");
|
75
|
+
|
58
76
|
mumuki.assetsLoadedFor('editor');
|
59
77
|
// although layout assets
|
60
78
|
// are actually loaded before this script, puzzle runner is not aimed
|
61
79
|
// to be used without a custom editor
|
62
80
|
mumuki.assetsLoadedFor('layout');
|
63
|
-
|
64
|
-
};
|
81
|
+
});
|
65
82
|
});
|
data/lib/public/js/muzzle.js
CHANGED
@@ -1,9 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* @typedef {object} PieceConfig
|
3
|
-
* @property {string} imagePath
|
4
|
-
* @property {string} structure
|
5
|
-
*/
|
6
|
-
|
7
1
|
/**
|
8
2
|
* @typedef {number[]} Point
|
9
3
|
*/
|
@@ -14,6 +8,14 @@
|
|
14
8
|
*/
|
15
9
|
|
16
10
|
|
11
|
+
class MuzzlePainter extends headbreaker.painters.Konva {
|
12
|
+
_newLine(options) {
|
13
|
+
|
14
|
+
const line = super._newLine(options);
|
15
|
+
line.strokeScaleEnabled(false);
|
16
|
+
return line;
|
17
|
+
}
|
18
|
+
}
|
17
19
|
|
18
20
|
/**
|
19
21
|
* Facade for referencing and creating a global puzzle canvas,
|
@@ -93,7 +95,7 @@ class MuzzleCanvas {
|
|
93
95
|
*
|
94
96
|
* @type {number}
|
95
97
|
*/
|
96
|
-
this.strokeWidth =
|
98
|
+
this.strokeWidth = 3;
|
97
99
|
|
98
100
|
/**
|
99
101
|
* Piece size
|
@@ -103,7 +105,7 @@ class MuzzleCanvas {
|
|
103
105
|
this.pieceSize = 100;
|
104
106
|
|
105
107
|
/**
|
106
|
-
* The x:y aspect ratio of the piece. Set null for automatic
|
108
|
+
* The `x:y` aspect ratio of the piece. Set null for automatic
|
107
109
|
* aspectRatio
|
108
110
|
*
|
109
111
|
* @type {number}
|
@@ -112,11 +114,26 @@ class MuzzleCanvas {
|
|
112
114
|
|
113
115
|
/**
|
114
116
|
* If the images should be adjusted vertically instead of horizontally
|
115
|
-
* to puzzle dimensions.
|
117
|
+
* to puzzle dimensions.
|
118
|
+
*
|
119
|
+
* Set null for automatic fit.
|
116
120
|
*
|
117
121
|
* @type {boolean}
|
118
122
|
*/
|
119
|
-
this.fitImagesVertically =
|
123
|
+
this.fitImagesVertically = null;
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Wether the scaling should ignore the scaler
|
127
|
+
* rise events
|
128
|
+
*/
|
129
|
+
this.manualScale = false;
|
130
|
+
|
131
|
+
/**
|
132
|
+
* The canvas shuffler.
|
133
|
+
*
|
134
|
+
* Set it null to automatic shuffling algorithm selection.
|
135
|
+
*/
|
136
|
+
this.shuffler = null;
|
120
137
|
|
121
138
|
/**
|
122
139
|
* Callback that will be executed
|
@@ -134,7 +151,29 @@ class MuzzleCanvas {
|
|
134
151
|
*
|
135
152
|
* @type {string}
|
136
153
|
*/
|
137
|
-
this.previousSolutionContent = null
|
154
|
+
this.previousSolutionContent = null;
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Whether the current puzzle can be solved in very few tries.
|
158
|
+
*
|
159
|
+
* Set null for automatic configuration of this property. Basic puzzles will be considered
|
160
|
+
* basic and match puzzles will be considered non-simple.
|
161
|
+
*
|
162
|
+
* @type {boolean}
|
163
|
+
*/
|
164
|
+
this.simple = null;
|
165
|
+
|
166
|
+
this.spiky = false;
|
167
|
+
|
168
|
+
/**
|
169
|
+
* The reference insert axis, used at rounded outline to compute insert internal and external diameters
|
170
|
+
*
|
171
|
+
* Set null for default computation of axis - no axis reference for basic boards
|
172
|
+
* and vertical axis for match
|
173
|
+
*
|
174
|
+
* @type {Axis}
|
175
|
+
* */
|
176
|
+
this.referenceInsertAxis = null;
|
138
177
|
|
139
178
|
/**
|
140
179
|
* Callback to be executed when submitting puzzle.
|
@@ -154,32 +193,81 @@ class MuzzleCanvas {
|
|
154
193
|
* property with any code you need the be called here
|
155
194
|
*/
|
156
195
|
this.onValid = () => {};
|
196
|
+
|
197
|
+
/**
|
198
|
+
* @private
|
199
|
+
*/
|
200
|
+
this._ready = false;
|
201
|
+
}
|
202
|
+
|
203
|
+
get painter() {
|
204
|
+
return new MuzzlePainter();
|
157
205
|
}
|
158
206
|
|
159
207
|
/**
|
160
208
|
*/
|
161
209
|
get baseConfig() {
|
162
|
-
|
163
|
-
|
164
|
-
return {
|
210
|
+
return Object.assign({
|
211
|
+
preventOffstageDrag: true,
|
165
212
|
width: this.canvasWidth,
|
166
213
|
height: this.canvasHeight,
|
167
|
-
pieceSize:
|
168
|
-
proximity:
|
169
|
-
borderFill: this.borderFill === null ? headbreaker.Vector.divide(pieceSize, 10) : this.borderFill,
|
214
|
+
pieceSize: this.adjustedPieceSize,
|
215
|
+
proximity: Math.min(this.adjustedPieceSize.x, this.adjustedPieceSize.y) / 5,
|
170
216
|
strokeWidth: this.strokeWidth,
|
171
|
-
lineSoftness: 0.18
|
172
|
-
|
217
|
+
lineSoftness: 0.18,
|
218
|
+
painter: this.painter
|
219
|
+
}, this.outlineConfig);
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
*/
|
224
|
+
get outlineConfig() {
|
225
|
+
if (this.spiky) {
|
226
|
+
return {
|
227
|
+
borderFill: this.borderFill === null ? headbreaker.Vector.divide(this.adjustedPieceSize, 10) : this.borderFill,
|
228
|
+
}
|
229
|
+
} else {
|
230
|
+
return {
|
231
|
+
borderFill: 0,
|
232
|
+
outline: new headbreaker.outline.Rounded({
|
233
|
+
bezelize: true,
|
234
|
+
insertDepth: 3/5,
|
235
|
+
bezelDepth: 9/10,
|
236
|
+
referenceInsertAxis: this.referenceInsertAxis
|
237
|
+
}),
|
238
|
+
}
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* The piece size, adjusted to the aspect ratio
|
244
|
+
*
|
245
|
+
* @returns {Vector}
|
246
|
+
*/
|
247
|
+
get adjustedPieceSize() {
|
248
|
+
if (!this._adjustedPieceSize) {
|
249
|
+
const aspectRatio = this.effectiveAspectRatio;
|
250
|
+
this._adjustedPieceSize = headbreaker.vector(this.pieceSize * aspectRatio, this.pieceSize);
|
251
|
+
}
|
252
|
+
return this._adjustedPieceSize;
|
173
253
|
}
|
174
254
|
|
175
255
|
/**
|
176
256
|
* @type {Axis}
|
177
257
|
*/
|
178
258
|
get imageAdjustmentAxis() {
|
179
|
-
console.log(this.fitImagesVertically)
|
180
259
|
return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
|
181
260
|
}
|
182
261
|
|
262
|
+
/**
|
263
|
+
* The configured aspect ratio, or 1
|
264
|
+
*
|
265
|
+
* @type {number}
|
266
|
+
*/
|
267
|
+
get effectiveAspectRatio() {
|
268
|
+
return this.aspectRatio || 1;
|
269
|
+
}
|
270
|
+
|
183
271
|
/**
|
184
272
|
* The currently active canvas, or null if
|
185
273
|
* it has not yet initialized
|
@@ -193,7 +281,7 @@ class MuzzleCanvas {
|
|
193
281
|
/**
|
194
282
|
* Draws the - previusly built - current canvas.
|
195
283
|
*
|
196
|
-
* Prefer
|
284
|
+
* Prefer `this.currentCanvas.redraw()` when performing
|
197
285
|
* small updates to the pieces.
|
198
286
|
*/
|
199
287
|
draw() {
|
@@ -222,9 +310,9 @@ class MuzzleCanvas {
|
|
222
310
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
223
311
|
*/
|
224
312
|
async basic(x, y, imagePath) {
|
225
|
-
|
226
|
-
|
227
|
-
|
313
|
+
this._config('aspectRatio', y / x);
|
314
|
+
this._config('simple', true);
|
315
|
+
this._config('shuffler', Muzzle.Shuffler.grid);
|
228
316
|
|
229
317
|
/**
|
230
318
|
* @todo take all container size
|
@@ -236,7 +324,6 @@ class MuzzleCanvas {
|
|
236
324
|
canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
|
237
325
|
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
|
238
326
|
this._attachBasicValidator(canvas);
|
239
|
-
canvas.shuffleGrid(0.8);
|
240
327
|
this._configCanvas(canvas);
|
241
328
|
canvas.onValid(() => {
|
242
329
|
setTimeout(() => {
|
@@ -249,52 +336,94 @@ class MuzzleCanvas {
|
|
249
336
|
}
|
250
337
|
|
251
338
|
/**
|
252
|
-
*
|
253
|
-
*
|
254
|
-
*
|
339
|
+
* Creates a choose puzzle, where a single right piece must match the single left piece,
|
340
|
+
* choosing the latter from a bunch of other left odd pieces. By default, `Muzzle.Shuffler.line` shuffling is used.
|
341
|
+
*
|
342
|
+
* This is a particular case of a match puzzle with line
|
343
|
+
*
|
344
|
+
* @param {string} leftUrl the url of the left piece
|
345
|
+
* @param {string} rightUrl the url of the right piece
|
346
|
+
* @param {string[]} leftOddUrls the urls of the off left urls
|
347
|
+
* @param {number} [rightAspectRatio] the `x:y` ratio of the right pieces, that override the general `aspectRatio` of the puzzle.
|
348
|
+
* Use null to have the same aspect ratio as left pieces
|
349
|
+
*
|
255
350
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
256
351
|
*/
|
257
|
-
async
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
const canvas = this._createCanvas();
|
262
|
-
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
|
263
|
-
|
264
|
-
// todo validate
|
265
|
-
// todo set images
|
266
|
-
|
267
|
-
this._configCanvas(canvas);
|
268
|
-
return canvas;
|
352
|
+
async choose(leftUrl, rightUrl, leftOddUrls, rightAspectRatio = null) {
|
353
|
+
this._config('shuffler', Muzzle.Shuffler.line);
|
354
|
+
return this.match([leftUrl], [rightUrl], {leftOddUrls, rightAspectRatio});
|
269
355
|
}
|
270
356
|
|
271
357
|
/**
|
272
|
-
*
|
273
|
-
* with optional odd left and right pieces that don't match
|
358
|
+
* Creates a match puzzle, where left pieces are matched against right pieces,
|
359
|
+
* with optional odd left and right pieces that don't match. By default, `Muzzle.Shuffler.columns`
|
360
|
+
* shuffling is used.
|
274
361
|
*
|
275
362
|
* @param {string[]} leftUrls
|
276
363
|
* @param {string[]} rightUrls must be of the same size of lefts
|
277
|
-
* @param {
|
278
|
-
* @param {string[]}
|
364
|
+
* @param {object} [options]
|
365
|
+
* @param {string[]} [options.leftOddUrls]
|
366
|
+
* @param {string[]} [options.rightOddUrls]
|
367
|
+
* @param {number?} [options.rightAspectRatio] the aspect ratio of the right pieces. Use null to have the same aspect ratio as left pieces
|
279
368
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
280
369
|
*/
|
281
|
-
async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
|
370
|
+
async match(leftUrls, rightUrls, {leftOddUrls = [], rightOddUrls = [], rightAspectRatio = this.effectiveAspectRatio} = {}) {
|
371
|
+
const rightWidthRatio = rightAspectRatio / this.effectiveAspectRatio;
|
372
|
+
|
373
|
+
this._config('simple', false);
|
374
|
+
this._config('shuffler', Muzzle.Shuffler.columns);
|
375
|
+
this._config('fitImagesVertically', rightWidthRatio > 1);
|
376
|
+
this._config('referenceInsertAxis', headbreaker.Vertical);
|
377
|
+
|
282
378
|
/** @private @type {(Promise<Template>)[]} */
|
283
379
|
const templatePromises = [];
|
284
|
-
|
285
|
-
|
380
|
+
|
381
|
+
const rightSize = headbreaker.diameter(
|
382
|
+
headbreaker.Vector.multiply(this.adjustedPieceSize, headbreaker.vector(rightWidthRatio, 1)));
|
383
|
+
|
384
|
+
const pushTemplate = (path, options) =>
|
385
|
+
templatePromises.push(this._createMatchTemplate(path, options));
|
386
|
+
|
387
|
+
const pushLeftTemplate = (index, path, options) =>
|
388
|
+
pushTemplate(path, {
|
389
|
+
left: true,
|
390
|
+
targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(1, index)),
|
391
|
+
...options
|
392
|
+
});
|
393
|
+
|
394
|
+
const pushRightTemplate = (index, path, options) =>
|
395
|
+
pushTemplate(path, {
|
396
|
+
size: rightSize,
|
397
|
+
targetPosition: headbreaker.Vector.multiply(this.pieceSize, headbreaker.vector(2, index)),
|
398
|
+
...options
|
399
|
+
});
|
286
400
|
|
287
401
|
const last = leftUrls.length - 1;
|
288
402
|
for (let i = 0; i <= last; i++) {
|
289
403
|
const leftId = `l${i}`;
|
290
404
|
const rightId = `r${i}`;
|
291
405
|
|
292
|
-
|
293
|
-
|
406
|
+
pushLeftTemplate(i + 1, leftUrls[i], {
|
407
|
+
id: leftId,
|
408
|
+
rightTargetId: rightId
|
409
|
+
});
|
410
|
+
pushRightTemplate(i + 1, rightUrls[i], {
|
411
|
+
id: rightId
|
412
|
+
});
|
294
413
|
}
|
295
414
|
|
296
|
-
leftOddUrls.forEach((it, i) =>
|
297
|
-
|
415
|
+
leftOddUrls.forEach((it, i) =>
|
416
|
+
pushLeftTemplate(i + leftUrls.length, it, {
|
417
|
+
id: `lo${i}`,
|
418
|
+
odd: true
|
419
|
+
})
|
420
|
+
);
|
421
|
+
rightOddUrls.forEach((it, i) =>
|
422
|
+
pushRightTemplate(i + rightUrls.length, it, {
|
423
|
+
id: `ro${i}`,
|
424
|
+
odd: true
|
425
|
+
})
|
426
|
+
);
|
298
427
|
|
299
428
|
// + Math.max(leftOddUrls.length, rightOddUrls.length)
|
300
429
|
const templates = await Promise.all(templatePromises);
|
@@ -302,7 +431,6 @@ class MuzzleCanvas {
|
|
302
431
|
const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
|
303
432
|
canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
|
304
433
|
templates.forEach(it => canvas.sketchPiece(it));
|
305
|
-
canvas.shuffleColumns(0.8);
|
306
434
|
this._attachMatchValidator(canvas);
|
307
435
|
this._configCanvas(canvas);
|
308
436
|
return canvas;
|
@@ -367,11 +495,11 @@ class MuzzleCanvas {
|
|
367
495
|
* @param {object} options
|
368
496
|
* @returns {Promise<object>}
|
369
497
|
*/
|
370
|
-
_createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false}) {
|
498
|
+
_createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false, size = null}) {
|
371
499
|
const structure = left ? 'T-N-' : `N-S-`;
|
372
|
-
|
373
500
|
return this._loadImage(imagePath).then((image) => {
|
374
501
|
return {
|
502
|
+
...(size ? {size} : {}),
|
375
503
|
structure,
|
376
504
|
metadata: { id, left, odd, rightTargetId, image, targetPosition }
|
377
505
|
}
|
@@ -384,25 +512,85 @@ class MuzzleCanvas {
|
|
384
512
|
*/
|
385
513
|
_configCanvas(canvas) {
|
386
514
|
this._canvas = canvas;
|
515
|
+
this._canvas.shuffleWith(0.8, this.shuffler);
|
387
516
|
this._canvas.onValid(() => {
|
388
517
|
setTimeout(() => this.onValid(), 0);
|
389
518
|
});
|
390
|
-
this.
|
519
|
+
this._setUpScaler();
|
391
520
|
this.ready();
|
392
521
|
}
|
393
522
|
|
394
|
-
|
395
|
-
if (this.
|
523
|
+
_setUpScaler() {
|
524
|
+
if (this.manualScale) return;
|
396
525
|
|
397
526
|
['resize', 'load'].forEach((event) => {
|
398
527
|
window.addEventListener(event, () => {
|
528
|
+
console.debug("Scaler event fired:", event);
|
399
529
|
var container = document.getElementById(this.canvasId);
|
400
|
-
this.
|
401
|
-
this.canvas.redraw();
|
530
|
+
this.scale(container.offsetWidth, container.scrollHeight);
|
402
531
|
});
|
403
532
|
});
|
404
533
|
}
|
405
534
|
|
535
|
+
/**
|
536
|
+
* Scales the canvas to the given width and height
|
537
|
+
*
|
538
|
+
* @param {number} width
|
539
|
+
* @param {number} height
|
540
|
+
*/
|
541
|
+
scale(width, height) {
|
542
|
+
if (this.fixedDimensions || !this.canvas) return;
|
543
|
+
|
544
|
+
console.debug("Scaling:", {width, height})
|
545
|
+
const factor = this.optimalScaleFactor(width, height);
|
546
|
+
this.canvas.resize(width, height);
|
547
|
+
this.canvas.scale(factor);
|
548
|
+
this.canvas.redraw();
|
549
|
+
this.focus();
|
550
|
+
}
|
551
|
+
|
552
|
+
/**
|
553
|
+
* Focuses the stage around the canvas center
|
554
|
+
*/
|
555
|
+
focus() {
|
556
|
+
const stage = this.canvas['__konvaLayer__'].getStage();
|
557
|
+
|
558
|
+
const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
|
559
|
+
const realDiameter = (() => {
|
560
|
+
const [xs, ys] = this.coordinates;
|
561
|
+
|
562
|
+
const minX = Math.min(...xs);
|
563
|
+
const minY = Math.min(...ys);
|
564
|
+
|
565
|
+
const maxX = Math.max(...xs);
|
566
|
+
const maxY = Math.max(...ys);
|
567
|
+
|
568
|
+
return headbreaker.vector(maxX - minX, maxY - minY);
|
569
|
+
})();
|
570
|
+
const diff = headbreaker.Vector.minus(area, realDiameter);
|
571
|
+
const semi = headbreaker.Vector.divide(diff, -2);
|
572
|
+
|
573
|
+
stage.setOffset(semi);
|
574
|
+
stage.draw();
|
575
|
+
}
|
576
|
+
|
577
|
+
/**
|
578
|
+
* @private
|
579
|
+
*/
|
580
|
+
get coordinates() {
|
581
|
+
const points = this.canvas.puzzle.points;
|
582
|
+
return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
|
583
|
+
}
|
584
|
+
|
585
|
+
/**
|
586
|
+
* @private
|
587
|
+
* @param {number} width
|
588
|
+
* @param {number} height
|
589
|
+
*/
|
590
|
+
optimalScaleFactor(width, height) {
|
591
|
+
const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
|
592
|
+
return Math.min(factors.x, factors.y) / 1.75;
|
593
|
+
}
|
406
594
|
|
407
595
|
/**
|
408
596
|
* Mark Muzzle as ready, loading previous solution
|
@@ -410,10 +598,16 @@ class MuzzleCanvas {
|
|
410
598
|
*/
|
411
599
|
ready() {
|
412
600
|
this.loadPreviousSolution();
|
601
|
+
this.resetCoordinates();
|
413
602
|
this.draw();
|
603
|
+
this._ready = true;
|
414
604
|
this.onReady();
|
415
605
|
}
|
416
606
|
|
607
|
+
isReady() {
|
608
|
+
return this._ready;
|
609
|
+
}
|
610
|
+
|
417
611
|
// ===========
|
418
612
|
// Persistence
|
419
613
|
// ===========
|
@@ -435,6 +629,7 @@ class MuzzleCanvas {
|
|
435
629
|
*/
|
436
630
|
loadSolution(solution) {
|
437
631
|
this.canvas.puzzle.relocateTo(solution.positions);
|
632
|
+
this.canvas.puzzle.autoconnect();
|
438
633
|
}
|
439
634
|
|
440
635
|
/**
|
@@ -451,6 +646,17 @@ class MuzzleCanvas {
|
|
451
646
|
}
|
452
647
|
}
|
453
648
|
|
649
|
+
/**
|
650
|
+
* Translates the pieces so that
|
651
|
+
* they start at canvas' coordinates origin
|
652
|
+
*/
|
653
|
+
resetCoordinates() {
|
654
|
+
const [xs, ys] = this.coordinates;
|
655
|
+
const minX = Math.min(...xs);
|
656
|
+
const minY = Math.min(...ys);
|
657
|
+
this.canvas.puzzle.translate(-minX, -minY);
|
658
|
+
}
|
659
|
+
|
454
660
|
// ==========
|
455
661
|
// Submitting
|
456
662
|
// ==========
|
@@ -490,12 +696,58 @@ class MuzzleCanvas {
|
|
490
696
|
};
|
491
697
|
}
|
492
698
|
|
699
|
+
/**
|
700
|
+
* @param {string} key
|
701
|
+
* @param {any} value
|
702
|
+
*/
|
703
|
+
_config(key, value) {
|
704
|
+
const current = this[key];
|
705
|
+
console.debug("Setting config: ", [key, value])
|
706
|
+
|
707
|
+
if (current === null) {
|
708
|
+
this[key] = value;
|
709
|
+
}
|
710
|
+
}
|
711
|
+
|
712
|
+
// ==============
|
713
|
+
// Event handling
|
714
|
+
// ==============
|
715
|
+
|
716
|
+
|
717
|
+
/**
|
718
|
+
* Registers an event handler
|
719
|
+
*
|
720
|
+
* @param {string} event
|
721
|
+
* @param {(...args: any) => void} callback
|
722
|
+
*/
|
723
|
+
register(event, callback) {
|
724
|
+
const _event = this[event];
|
725
|
+
this[event] = (...args) => {
|
726
|
+
callback(...args);
|
727
|
+
_event(...args);
|
728
|
+
}
|
729
|
+
}
|
730
|
+
|
731
|
+
/**
|
732
|
+
* Runs the given action if muzzle is ready,
|
733
|
+
* queueing it otherwise
|
734
|
+
* @param {() => void} callback
|
735
|
+
*/
|
736
|
+
run(callback) {
|
737
|
+
if (this.isReady()) {
|
738
|
+
callback();
|
739
|
+
} else {
|
740
|
+
this.register('onReady', callback);
|
741
|
+
}
|
742
|
+
}
|
493
743
|
}
|
494
744
|
|
495
745
|
const Muzzle = new class extends MuzzleCanvas {
|
496
746
|
constructor() {
|
497
747
|
super();
|
498
748
|
this.aux = {};
|
749
|
+
|
750
|
+
this.Shuffler = headbreaker.Shuffler;
|
499
751
|
}
|
500
752
|
|
501
753
|
/**
|
@@ -511,3 +763,5 @@ const Muzzle = new class extends MuzzleCanvas {
|
|
511
763
|
return muzzle;
|
512
764
|
}
|
513
765
|
}
|
766
|
+
|
767
|
+
window['Muzzle'] = Muzzle;
|