mumuki-puzzle-runner 0.3.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
/**
|