mumuki-puzzle-runner 0.3.0 → 1.0.1
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 +30 -15
- data/lib/public/js/muzzle.js +298 -58
- data/lib/public/vendor/headbreaker.d.ts +39 -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 +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f709148cf4642ed88174c0629fe0376dfab9f572418306eaa5ea620b174c78d
|
4
|
+
data.tar.gz: f1c8bf12e4aabaf26e94ee345a3ea732b31f8958b7c8aa877ebd889136915411
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3804856c30566ce86e408bb940a7c3a13712fba26045680ba0c9abfacab52c19c54bd4b8fa5f22df5e2e59bbbd6fa649f2994ccfe234c4e84b9923aeac8d1a53
|
7
|
+
data.tar.gz: ab0c527e90d2dfa56c17ffd10984f09c671274e1f3f94cb56853a763166ba7ca8b13208e80f21c991cdcf63f0804f98aa15aa5447cd33da76887746e9d6aa8f9
|
@@ -1,4 +1,52 @@
|
|
1
|
+
|
2
|
+
/*
|
3
|
+
* ============
|
4
|
+
* Initial size
|
5
|
+
* ============
|
6
|
+
*/
|
7
|
+
|
8
|
+
.mu-exercise-content {
|
9
|
+
border-radius: 10px;
|
10
|
+
border: 1px solid #dddddd;
|
11
|
+
}
|
12
|
+
|
1
13
|
.mu-kids-state-image img {
|
2
|
-
|
14
|
+
height: 100%;
|
15
|
+
width: auto;
|
3
16
|
padding: 30px;
|
4
17
|
}
|
18
|
+
|
19
|
+
.mu-kids-state.mu-state-initial {
|
20
|
+
width: 100%;
|
21
|
+
}
|
22
|
+
|
23
|
+
/*
|
24
|
+
* ====================
|
25
|
+
* Submit button hiding
|
26
|
+
* ====================
|
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,41 +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)));
|
50
55
|
});
|
51
56
|
|
52
57
|
Muzzle.manualScale = true;
|
53
58
|
|
59
|
+
// ====================
|
60
|
+
// Submit button hiding
|
61
|
+
// ====================
|
62
|
+
|
63
|
+
Muzzle.register('onReady', () => {
|
64
|
+
if (Muzzle.simple) {
|
65
|
+
$('.mu-kids-exercise-workspace').addClass('mu-submitless-exercise');
|
66
|
+
}
|
67
|
+
});
|
68
|
+
|
54
69
|
// ==============
|
55
70
|
// Assets loading
|
56
71
|
// ==============
|
57
72
|
|
58
|
-
|
59
|
-
|
73
|
+
Muzzle.register('onReady', () => {
|
74
|
+
console.debug("Muzzle is ready");
|
75
|
+
|
60
76
|
mumuki.assetsLoadedFor('editor');
|
61
77
|
// although layout assets
|
62
78
|
// are actually loaded before this script, puzzle runner is not aimed
|
63
79
|
// to be used without a custom editor
|
64
80
|
mumuki.assetsLoadedFor('layout');
|
65
|
-
|
66
|
-
};
|
81
|
+
});
|
67
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,14 +114,27 @@ 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;
|
120
124
|
|
125
|
+
/**
|
126
|
+
* Wether the scaling should ignore the scaler
|
127
|
+
* rise events
|
128
|
+
*/
|
121
129
|
this.manualScale = false;
|
122
130
|
|
131
|
+
/**
|
132
|
+
* The canvas shuffler.
|
133
|
+
*
|
134
|
+
* Set it null to automatic shuffling algorithm selection.
|
135
|
+
*/
|
136
|
+
this.shuffler = null;
|
137
|
+
|
123
138
|
/**
|
124
139
|
* Callback that will be executed
|
125
140
|
* when muzzle has fully loaded and rendered its first
|
@@ -136,7 +151,29 @@ class MuzzleCanvas {
|
|
136
151
|
*
|
137
152
|
* @type {string}
|
138
153
|
*/
|
139
|
-
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;
|
140
177
|
|
141
178
|
/**
|
142
179
|
* Callback to be executed when submitting puzzle.
|
@@ -156,32 +193,81 @@ class MuzzleCanvas {
|
|
156
193
|
* property with any code you need the be called here
|
157
194
|
*/
|
158
195
|
this.onValid = () => {};
|
196
|
+
|
197
|
+
/**
|
198
|
+
* @private
|
199
|
+
*/
|
200
|
+
this._ready = false;
|
201
|
+
}
|
202
|
+
|
203
|
+
get painter() {
|
204
|
+
return new MuzzlePainter();
|
159
205
|
}
|
160
206
|
|
161
207
|
/**
|
162
208
|
*/
|
163
209
|
get baseConfig() {
|
164
|
-
|
165
|
-
|
166
|
-
return {
|
210
|
+
return Object.assign({
|
211
|
+
preventOffstageDrag: true,
|
167
212
|
width: this.canvasWidth,
|
168
213
|
height: this.canvasHeight,
|
169
|
-
pieceSize:
|
170
|
-
proximity:
|
171
|
-
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,
|
172
216
|
strokeWidth: this.strokeWidth,
|
173
|
-
lineSoftness: 0.18
|
174
|
-
|
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;
|
175
253
|
}
|
176
254
|
|
177
255
|
/**
|
178
256
|
* @type {Axis}
|
179
257
|
*/
|
180
258
|
get imageAdjustmentAxis() {
|
181
|
-
console.log(this.fitImagesVertically)
|
182
259
|
return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
|
183
260
|
}
|
184
261
|
|
262
|
+
/**
|
263
|
+
* The configured aspect ratio, or 1
|
264
|
+
*
|
265
|
+
* @type {number}
|
266
|
+
*/
|
267
|
+
get effectiveAspectRatio() {
|
268
|
+
return this.aspectRatio || 1;
|
269
|
+
}
|
270
|
+
|
185
271
|
/**
|
186
272
|
* The currently active canvas, or null if
|
187
273
|
* it has not yet initialized
|
@@ -195,7 +281,7 @@ class MuzzleCanvas {
|
|
195
281
|
/**
|
196
282
|
* Draws the - previusly built - current canvas.
|
197
283
|
*
|
198
|
-
* Prefer
|
284
|
+
* Prefer `this.currentCanvas.redraw()` when performing
|
199
285
|
* small updates to the pieces.
|
200
286
|
*/
|
201
287
|
draw() {
|
@@ -224,9 +310,9 @@ class MuzzleCanvas {
|
|
224
310
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
225
311
|
*/
|
226
312
|
async basic(x, y, imagePath) {
|
227
|
-
|
228
|
-
|
229
|
-
|
313
|
+
this._config('aspectRatio', y / x);
|
314
|
+
this._config('simple', true);
|
315
|
+
this._config('shuffler', Muzzle.Shuffler.grid);
|
230
316
|
|
231
317
|
/**
|
232
318
|
* @todo take all container size
|
@@ -238,7 +324,6 @@ class MuzzleCanvas {
|
|
238
324
|
canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
|
239
325
|
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
|
240
326
|
this._attachBasicValidator(canvas);
|
241
|
-
canvas.shuffleGrid(0.8);
|
242
327
|
this._configCanvas(canvas);
|
243
328
|
canvas.onValid(() => {
|
244
329
|
setTimeout(() => {
|
@@ -251,52 +336,94 @@ class MuzzleCanvas {
|
|
251
336
|
}
|
252
337
|
|
253
338
|
/**
|
254
|
-
*
|
255
|
-
*
|
256
|
-
*
|
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
|
+
*
|
257
350
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
258
351
|
*/
|
259
|
-
async
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
const canvas = this._createCanvas();
|
264
|
-
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
|
265
|
-
|
266
|
-
// todo validate
|
267
|
-
// todo set images
|
268
|
-
|
269
|
-
this._configCanvas(canvas);
|
270
|
-
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});
|
271
355
|
}
|
272
356
|
|
273
357
|
/**
|
274
|
-
*
|
275
|
-
* 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.
|
276
361
|
*
|
277
362
|
* @param {string[]} leftUrls
|
278
363
|
* @param {string[]} rightUrls must be of the same size of lefts
|
279
|
-
* @param {
|
280
|
-
* @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
|
281
368
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
282
369
|
*/
|
283
|
-
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
|
+
|
284
378
|
/** @private @type {(Promise<Template>)[]} */
|
285
379
|
const templatePromises = [];
|
286
|
-
|
287
|
-
|
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
|
+
});
|
288
400
|
|
289
401
|
const last = leftUrls.length - 1;
|
290
402
|
for (let i = 0; i <= last; i++) {
|
291
403
|
const leftId = `l${i}`;
|
292
404
|
const rightId = `r${i}`;
|
293
405
|
|
294
|
-
|
295
|
-
|
406
|
+
pushLeftTemplate(i + 1, leftUrls[i], {
|
407
|
+
id: leftId,
|
408
|
+
rightTargetId: rightId
|
409
|
+
});
|
410
|
+
pushRightTemplate(i + 1, rightUrls[i], {
|
411
|
+
id: rightId
|
412
|
+
});
|
296
413
|
}
|
297
414
|
|
298
|
-
leftOddUrls.forEach((it, i) =>
|
299
|
-
|
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
|
+
);
|
300
427
|
|
301
428
|
// + Math.max(leftOddUrls.length, rightOddUrls.length)
|
302
429
|
const templates = await Promise.all(templatePromises);
|
@@ -304,7 +431,6 @@ class MuzzleCanvas {
|
|
304
431
|
const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
|
305
432
|
canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
|
306
433
|
templates.forEach(it => canvas.sketchPiece(it));
|
307
|
-
canvas.shuffleColumns(0.8);
|
308
434
|
this._attachMatchValidator(canvas);
|
309
435
|
this._configCanvas(canvas);
|
310
436
|
return canvas;
|
@@ -369,11 +495,11 @@ class MuzzleCanvas {
|
|
369
495
|
* @param {object} options
|
370
496
|
* @returns {Promise<object>}
|
371
497
|
*/
|
372
|
-
_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}) {
|
373
499
|
const structure = left ? 'T-N-' : `N-S-`;
|
374
|
-
|
375
500
|
return this._loadImage(imagePath).then((image) => {
|
376
501
|
return {
|
502
|
+
...(size ? {size} : {}),
|
377
503
|
structure,
|
378
504
|
metadata: { id, left, odd, rightTargetId, image, targetPosition }
|
379
505
|
}
|
@@ -386,31 +512,81 @@ class MuzzleCanvas {
|
|
386
512
|
*/
|
387
513
|
_configCanvas(canvas) {
|
388
514
|
this._canvas = canvas;
|
515
|
+
this._canvas.shuffleWith(0.8, this.shuffler);
|
389
516
|
this._canvas.onValid(() => {
|
390
517
|
setTimeout(() => this.onValid(), 0);
|
391
518
|
});
|
392
|
-
this.
|
519
|
+
this._setUpScaler();
|
393
520
|
this.ready();
|
394
521
|
}
|
395
522
|
|
396
|
-
|
523
|
+
_setUpScaler() {
|
397
524
|
if (this.manualScale) return;
|
398
525
|
|
399
526
|
['resize', 'load'].forEach((event) => {
|
400
527
|
window.addEventListener(event, () => {
|
528
|
+
console.debug("Scaler event fired:", event);
|
401
529
|
var container = document.getElementById(this.canvasId);
|
402
530
|
this.scale(container.offsetWidth, container.scrollHeight);
|
403
531
|
});
|
404
532
|
});
|
405
533
|
}
|
406
534
|
|
535
|
+
/**
|
536
|
+
* Scales the canvas to the given width and height
|
537
|
+
*
|
538
|
+
* @param {number} width
|
539
|
+
* @param {number} height
|
540
|
+
*/
|
407
541
|
scale(width, height) {
|
408
|
-
if (this.fixedDimensions) return;
|
542
|
+
if (this.fixedDimensions || !this.canvas) return;
|
543
|
+
|
544
|
+
console.debug("Scaling:", {width, height})
|
545
|
+
const factor = this.optimalScaleFactor(width, height);
|
409
546
|
this.canvas.resize(width, height);
|
410
|
-
this.canvas.scale(
|
547
|
+
this.canvas.scale(factor);
|
411
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)];
|
412
583
|
}
|
413
584
|
|
585
|
+
/**
|
586
|
+
* @private
|
587
|
+
* @param {number} width
|
588
|
+
* @param {number} height
|
589
|
+
*/
|
414
590
|
optimalScaleFactor(width, height) {
|
415
591
|
const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
|
416
592
|
return Math.min(factors.x, factors.y) / 1.75;
|
@@ -422,10 +598,16 @@ class MuzzleCanvas {
|
|
422
598
|
*/
|
423
599
|
ready() {
|
424
600
|
this.loadPreviousSolution();
|
601
|
+
this.resetCoordinates();
|
425
602
|
this.draw();
|
603
|
+
this._ready = true;
|
426
604
|
this.onReady();
|
427
605
|
}
|
428
606
|
|
607
|
+
isReady() {
|
608
|
+
return this._ready;
|
609
|
+
}
|
610
|
+
|
429
611
|
// ===========
|
430
612
|
// Persistence
|
431
613
|
// ===========
|
@@ -447,6 +629,7 @@ class MuzzleCanvas {
|
|
447
629
|
*/
|
448
630
|
loadSolution(solution) {
|
449
631
|
this.canvas.puzzle.relocateTo(solution.positions);
|
632
|
+
this.canvas.puzzle.autoconnect();
|
450
633
|
}
|
451
634
|
|
452
635
|
/**
|
@@ -463,6 +646,17 @@ class MuzzleCanvas {
|
|
463
646
|
}
|
464
647
|
}
|
465
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
|
+
|
466
660
|
// ==========
|
467
661
|
// Submitting
|
468
662
|
// ==========
|
@@ -502,12 +696,58 @@ class MuzzleCanvas {
|
|
502
696
|
};
|
503
697
|
}
|
504
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
|
+
}
|
505
743
|
}
|
506
744
|
|
507
745
|
const Muzzle = new class extends MuzzleCanvas {
|
508
746
|
constructor() {
|
509
747
|
super();
|
510
748
|
this.aux = {};
|
749
|
+
|
750
|
+
this.Shuffler = headbreaker.Shuffler;
|
511
751
|
}
|
512
752
|
|
513
753
|
/**
|