mumuki-puzzle-runner 0.0.1 → 0.4.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 +36 -21
- data/lib/public/js/muzzle.js +323 -64
- data/lib/public/vendor/headbreaker.d.ts +313 -99
- data/lib/public/vendor/headbreaker.js +1 -1
- data/lib/public/vendor/headbreaker.js.map +1 -1
- data/lib/test_hook.rb +12 -6
- 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: 5da4e03efe5edbeb4e6eadfaf79f5d37e4f5bea50127a26e91c5f6d2cedb2438
|
4
|
+
data.tar.gz: f0eeb8f873165d98ecfcc04854aa4fb781d4753fc4350bcfed90fd24c184744b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c98b0e5f9fe633c8fb0587b3c948ce0a6d6d79b80619befc28bc4c583578be596b1d8d2d9f61fddbb3241a320424b602af417fca96827c632a9537342d2a62e9
|
7
|
+
data.tar.gz: f40b2c76b102651f25ca76eda5b82d24f6cb037e92efcbd68299d634ec8a4f8a22477f4639893b780f6e8f44e790761189c43c12a925a5421f8e33416b3273af
|
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,12 +1,18 @@
|
|
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
|
// ================
|
6
14
|
|
7
|
-
|
8
|
-
const $customEditorValue = $('#mu-custom-editor-value');
|
9
|
-
Muzzle.previousSolutionJson = $customEditorValue.val();
|
15
|
+
Muzzle.previousSolutionContent = $('#mu-custom-editor-value').val();
|
10
16
|
|
11
17
|
$('#mu-puzzle-custom-editor').append(`
|
12
18
|
<div id="muzzle-canvas">
|
@@ -20,46 +26,55 @@ $(() => {
|
|
20
26
|
// Submission config
|
21
27
|
// =================
|
22
28
|
|
29
|
+
|
23
30
|
// Required to sync state before submitting
|
24
|
-
mumuki.
|
25
|
-
Muzzle.
|
26
|
-
$customEditorValue.val(Muzzle.previousSolutionJson);
|
31
|
+
mumuki.CustomEditor.addSource({
|
32
|
+
getContent() { return { name: "solution[content]", value: Muzzle.solutionContent }; }
|
27
33
|
});
|
34
|
+
mumuki.CustomEditor.addSource({
|
35
|
+
getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
|
36
|
+
})
|
28
37
|
|
29
38
|
// Requiered to actually bind Muzzle's submit to
|
30
39
|
// mumuki's solution processing
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
mumuki.submission.processSolution({solution: {content: solutionJson}});
|
35
|
-
_onSubmit(solutionJson, valid);
|
36
|
-
}
|
40
|
+
register('onSubmit', (submission) => {
|
41
|
+
mumuki.submission.processSolution(submission);
|
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
@@ -32,19 +32,12 @@ class MuzzleCanvas {
|
|
32
32
|
* @type {Canvas}
|
33
33
|
**/
|
34
34
|
this._canvas = null;
|
35
|
-
this.baseConfig = {
|
36
|
-
width: 800,
|
37
|
-
height: 650,
|
38
|
-
pieceSize: 100,
|
39
|
-
proximity: 20,
|
40
|
-
borderFill: 10,
|
41
|
-
strokeWidth: 1.5,
|
42
|
-
lineSoftness: 0.18
|
43
|
-
};
|
44
35
|
|
45
36
|
/**
|
46
37
|
* The id of the HTML element that will contain the canvas
|
47
38
|
* Override it you are going to place in a non-standard way
|
39
|
+
*
|
40
|
+
* @type {string}
|
48
41
|
*/
|
49
42
|
this.canvasId = id;
|
50
43
|
|
@@ -61,9 +54,72 @@ class MuzzleCanvas {
|
|
61
54
|
* Wether expected refs shall be ignored by Muzzle.
|
62
55
|
*
|
63
56
|
* They will still be evaluated server-side.
|
57
|
+
*
|
58
|
+
* @type {boolean}
|
64
59
|
*/
|
65
60
|
this.expectedRefsAreOnlyDescriptive = false;
|
66
61
|
|
62
|
+
/**
|
63
|
+
* Width of canvas
|
64
|
+
*
|
65
|
+
* @type {number}
|
66
|
+
*/
|
67
|
+
this.canvasWidth = 600;
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Height of canvas
|
71
|
+
*
|
72
|
+
* @type {number}
|
73
|
+
*/
|
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;
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Size of fill. Set null for perfect-match
|
86
|
+
*
|
87
|
+
* @type {number}
|
88
|
+
*/
|
89
|
+
this.borderFill = null;
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Canvas line width
|
93
|
+
*
|
94
|
+
* @type {number}
|
95
|
+
*/
|
96
|
+
this.strokeWidth = 1.5;
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Piece size
|
100
|
+
*
|
101
|
+
* @type {number}
|
102
|
+
*/
|
103
|
+
this.pieceSize = 100;
|
104
|
+
|
105
|
+
/**
|
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
|
116
|
+
*
|
117
|
+
* @type {boolean}
|
118
|
+
*/
|
119
|
+
this.fitImagesVertically = false;
|
120
|
+
|
121
|
+
this.manualScale = false;
|
122
|
+
|
67
123
|
/**
|
68
124
|
* Callback that will be executed
|
69
125
|
* when muzzle has fully loaded and rendered its first
|
@@ -75,12 +131,22 @@ class MuzzleCanvas {
|
|
75
131
|
this.onReady = () => {};
|
76
132
|
|
77
133
|
/**
|
78
|
-
* The previous solution to the current puzzle in
|
134
|
+
* The previous solution to the current puzzle in a past session,
|
79
135
|
* if any
|
80
136
|
*
|
81
137
|
* @type {string}
|
82
138
|
*/
|
83
|
-
this.
|
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;
|
84
150
|
|
85
151
|
/**
|
86
152
|
* Callback to be executed when submitting puzzle.
|
@@ -88,15 +154,49 @@ class MuzzleCanvas {
|
|
88
154
|
* Does nothing by default but you can
|
89
155
|
* override it to perform additional actions
|
90
156
|
*
|
91
|
-
* @param {string}
|
92
|
-
|
157
|
+
* @param {{solution: {content: string}, client_result: {status: "passed" | "failed"}}} submission
|
158
|
+
*/
|
159
|
+
this.onSubmit = (submission) => {};
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Callback that will be executed
|
163
|
+
* when muzzle's puzzle becomes valid
|
164
|
+
*
|
165
|
+
* It does nothing by default but you can override this
|
166
|
+
* property with any code you need the be called here
|
93
167
|
*/
|
94
|
-
this.
|
168
|
+
this.onValid = () => {};
|
169
|
+
}
|
170
|
+
|
171
|
+
/**
|
172
|
+
*/
|
173
|
+
get baseConfig() {
|
174
|
+
const aspectRatio = this.aspectRatio || 1;
|
175
|
+
const pieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
|
176
|
+
return {
|
177
|
+
width: this.canvasWidth,
|
178
|
+
height: this.canvasHeight,
|
179
|
+
pieceSize: pieceSize,
|
180
|
+
proximity: Math.min(pieceSize.x, pieceSize.y) / 5,
|
181
|
+
borderFill: this.borderFill === null ? headbreaker.Vector.divide(pieceSize, 10) : this.borderFill,
|
182
|
+
strokeWidth: this.strokeWidth,
|
183
|
+
lineSoftness: 0.18
|
184
|
+
};
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* @type {Axis}
|
189
|
+
*/
|
190
|
+
get imageAdjustmentAxis() {
|
191
|
+
console.log(this.fitImagesVertically)
|
192
|
+
return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
|
95
193
|
}
|
96
194
|
|
97
195
|
/**
|
98
196
|
* The currently active canvas, or null if
|
99
197
|
* it has not yet initialized
|
198
|
+
*
|
199
|
+
* @returns {Canvas}
|
100
200
|
*/
|
101
201
|
get canvas() {
|
102
202
|
return this._canvas;
|
@@ -134,53 +234,93 @@ class MuzzleCanvas {
|
|
134
234
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
135
235
|
*/
|
136
236
|
async basic(x, y, imagePath) {
|
237
|
+
if (!this.aspectRatio) {
|
238
|
+
this.aspectRatio = x / y;
|
239
|
+
}
|
240
|
+
|
241
|
+
if (this.simple === null) {
|
242
|
+
this.simple = true;
|
243
|
+
}
|
244
|
+
|
137
245
|
/**
|
138
246
|
* @todo take all container size
|
139
247
|
**/
|
140
248
|
const image = await this._loadImage(imagePath);
|
141
249
|
/** @type {Canvas} */
|
142
250
|
// @ts-ignore
|
143
|
-
const canvas =
|
251
|
+
const canvas = this._createCanvas({ image: image });
|
252
|
+
canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
|
144
253
|
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
|
145
|
-
this.
|
254
|
+
this._attachBasicValidator(canvas);
|
255
|
+
canvas.shuffleGrid(0.8);
|
256
|
+
this._configCanvas(canvas);
|
146
257
|
canvas.onValid(() => {
|
147
258
|
setTimeout(() => {
|
148
|
-
if (canvas.
|
149
|
-
this.submit()
|
259
|
+
if (canvas.valid) {
|
260
|
+
this.submit();
|
150
261
|
}
|
151
262
|
}, 1500);
|
152
263
|
});
|
153
|
-
this._configInitialCanvas(canvas);
|
154
264
|
return canvas;
|
155
265
|
}
|
156
266
|
|
157
267
|
/**
|
158
|
-
* @param {
|
268
|
+
* @param {number} x
|
269
|
+
* @param {number} y
|
270
|
+
* @param {string[]} [imagePaths]
|
159
271
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
160
272
|
*/
|
161
|
-
multi(
|
162
|
-
|
273
|
+
async multi(x, y, imagePaths) {
|
274
|
+
const count = imagePaths.length;
|
275
|
+
const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
|
276
|
+
|
277
|
+
const canvas = this._createCanvas();
|
278
|
+
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
|
279
|
+
|
280
|
+
// todo validate
|
281
|
+
// todo set images
|
282
|
+
|
283
|
+
this._configCanvas(canvas);
|
284
|
+
return canvas;
|
163
285
|
}
|
164
286
|
|
165
287
|
/**
|
166
|
-
*
|
167
|
-
*
|
288
|
+
* Craates a match puzzle, where left pieces are matched against right pieces,
|
289
|
+
* with optional odd left and right pieces that don't match
|
290
|
+
*
|
291
|
+
* @param {string[]} leftUrls
|
292
|
+
* @param {string[]} rightUrls must be of the same size of lefts
|
293
|
+
* @param {string[]} leftOddUrls
|
294
|
+
* @param {string[]} rightOddUrls
|
168
295
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
169
296
|
*/
|
170
|
-
async match(
|
171
|
-
/** @type {(Promise<Template>)[]} */
|
297
|
+
async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
|
298
|
+
/** @private @type {(Promise<Template>)[]} */
|
172
299
|
const templatePromises = [];
|
173
|
-
const
|
300
|
+
const pushTemplate = (config, options) =>
|
301
|
+
templatePromises.push(this._createMatchTemplate(config, options));
|
302
|
+
|
303
|
+
const last = leftUrls.length - 1;
|
174
304
|
for (let i = 0; i <= last; i++) {
|
175
|
-
|
176
|
-
|
305
|
+
const leftId = `l${i}`;
|
306
|
+
const rightId = `r${i}`;
|
307
|
+
|
308
|
+
pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
|
309
|
+
pushTemplate(rightUrls[i], {id: rightId, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + 1) }});
|
177
310
|
}
|
311
|
+
|
312
|
+
leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
|
313
|
+
rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + rightUrls.length) },}));
|
314
|
+
|
315
|
+
// + Math.max(leftOddUrls.length, rightOddUrls.length)
|
178
316
|
const templates = await Promise.all(templatePromises);
|
179
317
|
/** @type {Canvas} */
|
180
|
-
const canvas =
|
318
|
+
const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
|
319
|
+
canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
|
181
320
|
templates.forEach(it => canvas.sketchPiece(it));
|
182
|
-
|
183
|
-
this.
|
321
|
+
canvas.shuffleColumns(0.8);
|
322
|
+
this._attachMatchValidator(canvas);
|
323
|
+
this._configCanvas(canvas);
|
184
324
|
return canvas;
|
185
325
|
}
|
186
326
|
|
@@ -189,32 +329,43 @@ class MuzzleCanvas {
|
|
189
329
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
190
330
|
*/
|
191
331
|
custom(canvas) {
|
192
|
-
this.
|
332
|
+
this._configCanvas(canvas);
|
193
333
|
return Promise.resolve(canvas);
|
194
334
|
}
|
195
335
|
|
196
336
|
/**
|
197
337
|
* @private
|
198
|
-
* @param {
|
338
|
+
* @param {any} config
|
339
|
+
* @return {Canvas}
|
199
340
|
*/
|
200
|
-
|
201
|
-
return Object.assign(
|
341
|
+
_createCanvas(config = {}) {
|
342
|
+
return new headbreaker.Canvas(this.canvasId, Object.assign(config, this.baseConfig));
|
202
343
|
}
|
203
344
|
|
204
345
|
/**
|
205
346
|
* @private
|
206
347
|
* @param {Canvas} canvas
|
207
348
|
*/
|
208
|
-
|
349
|
+
_attachBasicValidator(canvas) {
|
209
350
|
if (!this.expectedRefsAreOnlyDescriptive && this._expectedRefs) {
|
210
|
-
canvas.
|
211
|
-
new headbreaker.PuzzleValidator(
|
212
|
-
headbreaker.PuzzleValidator.relativeRefs(this._expectedRefs)));
|
351
|
+
canvas.attachRelativeRefsValidator(this._expectedRefs);
|
213
352
|
} else {
|
214
353
|
canvas.attachSolvedValidator();
|
215
354
|
}
|
216
355
|
}
|
217
356
|
|
357
|
+
/**
|
358
|
+
* @private
|
359
|
+
* @param {Canvas} canvas
|
360
|
+
*/
|
361
|
+
_attachMatchValidator(canvas) {
|
362
|
+
canvas.attachValidator(new headbreaker.PuzzleValidator(
|
363
|
+
puzzle => puzzle.pieces
|
364
|
+
.filter(it => !it.metadata.odd && it.metadata.left)
|
365
|
+
.every(it => it.rightConnection && it.rightConnection.id === it.metadata.rightTargetId)
|
366
|
+
));
|
367
|
+
}
|
368
|
+
|
218
369
|
/**
|
219
370
|
* @private
|
220
371
|
* @param {string} path
|
@@ -226,23 +377,95 @@ class MuzzleCanvas {
|
|
226
377
|
return new Promise((resolve, reject) => image.onload = () => resolve(image));
|
227
378
|
}
|
228
379
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
380
|
+
/**
|
381
|
+
* @private
|
382
|
+
* @param {string} imagePath
|
383
|
+
* @param {object} options
|
384
|
+
* @returns {Promise<object>}
|
385
|
+
*/
|
386
|
+
_createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false}) {
|
387
|
+
const structure = left ? 'T-N-' : `N-S-`;
|
388
|
+
|
389
|
+
return this._loadImage(imagePath).then((image) => {
|
390
|
+
return {
|
391
|
+
structure,
|
392
|
+
metadata: { id, left, odd, rightTargetId, image, targetPosition }
|
393
|
+
}
|
394
|
+
});
|
234
395
|
}
|
235
396
|
|
236
397
|
/**
|
398
|
+
* @private
|
237
399
|
* @param {Canvas} canvas
|
238
400
|
*/
|
239
|
-
|
401
|
+
_configCanvas(canvas) {
|
240
402
|
this._canvas = canvas;
|
403
|
+
this._canvas.onValid(() => {
|
404
|
+
setTimeout(() => this.onValid(), 0);
|
405
|
+
});
|
406
|
+
this._setUpScaler();
|
241
407
|
this.ready();
|
242
408
|
}
|
243
409
|
|
410
|
+
_setUpScaler() {
|
411
|
+
if (this.manualScale) return;
|
412
|
+
|
413
|
+
['resize', 'load'].forEach((event) => {
|
414
|
+
window.addEventListener(event, () => {
|
415
|
+
var container = document.getElementById(this.canvasId);
|
416
|
+
this.scale(container.offsetWidth, container.scrollHeight);
|
417
|
+
});
|
418
|
+
});
|
419
|
+
}
|
420
|
+
|
421
|
+
scale(width, height) {
|
422
|
+
if (this.fixedDimensions || !this.canvas) return;
|
423
|
+
const factor = this.optimalScaleFactor(width, height);
|
424
|
+
this.canvas.resize(width, height);
|
425
|
+
this.canvas.scale(factor);
|
426
|
+
this.canvas.redraw();
|
427
|
+
this.focus();
|
428
|
+
}
|
429
|
+
|
430
|
+
focus() {
|
431
|
+
const stage = this.canvas['__konvaLayer__'].getStage();
|
432
|
+
|
433
|
+
const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
|
434
|
+
const realDiameter = (() => {
|
435
|
+
const [xs, ys] = this.coordinates;
|
436
|
+
|
437
|
+
const minX = Math.min(...xs);
|
438
|
+
const minY = Math.min(...ys);
|
439
|
+
|
440
|
+
const maxX = Math.max(...xs);
|
441
|
+
const maxY = Math.max(...ys);
|
442
|
+
|
443
|
+
return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
|
444
|
+
})();
|
445
|
+
const diff = headbreaker.Vector.minus(area, realDiameter);
|
446
|
+
const semi = headbreaker.Vector.divide(diff, -2);
|
447
|
+
|
448
|
+
stage.setOffset(semi);
|
449
|
+
stage.draw();
|
450
|
+
}
|
451
|
+
|
452
|
+
get coordinates() {
|
453
|
+
const points = this.canvas.puzzle.points;
|
454
|
+
return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
|
455
|
+
}
|
456
|
+
|
457
|
+
optimalScaleFactor(width, height) {
|
458
|
+
const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
|
459
|
+
return Math.min(factors.x, factors.y) / 1.75;
|
460
|
+
}
|
461
|
+
|
462
|
+
/**
|
463
|
+
* Mark Muzzle as ready, loading previous solution
|
464
|
+
* and drawing the canvas
|
465
|
+
*/
|
244
466
|
ready() {
|
245
467
|
this.loadPreviousSolution();
|
468
|
+
this.resetCoordinates();
|
246
469
|
this.draw();
|
247
470
|
this.onReady();
|
248
471
|
}
|
@@ -262,30 +485,34 @@ class MuzzleCanvas {
|
|
262
485
|
}
|
263
486
|
|
264
487
|
/**
|
488
|
+
* Loads - but does not draw - a solution into the canvas.
|
489
|
+
*
|
265
490
|
* @param {Solution} solution
|
266
491
|
*/
|
267
492
|
loadSolution(solution) {
|
268
493
|
this.canvas.puzzle.relocateTo(solution.positions);
|
494
|
+
this.canvas.puzzle.autoconnect();
|
269
495
|
}
|
270
496
|
|
271
497
|
/**
|
272
|
-
* Loads the current canvas with the
|
498
|
+
* Loads - but does not draw - the current canvas with the previous solution, if available.
|
499
|
+
*
|
273
500
|
*/
|
274
501
|
loadPreviousSolution() {
|
275
|
-
if (this.
|
502
|
+
if (this.previousSolutionContent) {
|
276
503
|
try {
|
277
|
-
this.loadSolution(JSON.parse(this.
|
504
|
+
this.loadSolution(JSON.parse(this.previousSolutionContent));
|
278
505
|
} catch (e) {
|
279
506
|
console.warn("Ignoring unparseabe editor value");
|
280
507
|
}
|
281
|
-
} else {
|
282
|
-
this.canvas.shuffle(0.8);
|
283
508
|
}
|
284
509
|
}
|
285
510
|
|
286
|
-
|
287
|
-
this.
|
288
|
-
|
511
|
+
resetCoordinates() {
|
512
|
+
const [xs, ys] = this.coordinates;
|
513
|
+
const minX = Math.min(...xs);
|
514
|
+
const minY = Math.min(...ys);
|
515
|
+
this.canvas.puzzle.translate(-minX, -minY);
|
289
516
|
}
|
290
517
|
|
291
518
|
// ==========
|
@@ -297,24 +524,56 @@ class MuzzleCanvas {
|
|
297
524
|
* validating it if necessary
|
298
525
|
*/
|
299
526
|
submit() {
|
300
|
-
this.
|
301
|
-
this.onSubmit(this._solutionJson, this.canvas.puzzle.valid);
|
527
|
+
this.onSubmit(this._prepareSubmission());
|
302
528
|
}
|
303
529
|
|
304
530
|
/**
|
305
531
|
* The current solution, expressed as a JSON string
|
306
532
|
*/
|
307
|
-
get
|
533
|
+
get solutionContent() {
|
308
534
|
return JSON.stringify(this.solution);
|
309
535
|
}
|
310
|
-
}
|
311
536
|
|
537
|
+
/**
|
538
|
+
* The solution validation status
|
539
|
+
*
|
540
|
+
* @returns {"passed" | "failed"}
|
541
|
+
*/
|
542
|
+
get clientResultStatus() {
|
543
|
+
return this.canvas.valid ? 'passed' : 'failed';
|
544
|
+
}
|
312
545
|
|
313
|
-
|
546
|
+
_prepareSubmission() {
|
547
|
+
return {
|
548
|
+
solution: {
|
549
|
+
content: this.solutionContent
|
550
|
+
},
|
551
|
+
client_result: {
|
552
|
+
status: this.clientResultStatus
|
553
|
+
}
|
554
|
+
};
|
555
|
+
}
|
314
556
|
|
315
|
-
Muzzle.aux = {};
|
316
|
-
Muzzle.another = (id) => {
|
317
|
-
const muzzle = new MuzzleCanvas(id);
|
318
|
-
Muzzle.aux[id] = muzzle
|
319
|
-
return muzzle;
|
320
557
|
}
|
558
|
+
|
559
|
+
const Muzzle = new class extends MuzzleCanvas {
|
560
|
+
constructor() {
|
561
|
+
super();
|
562
|
+
this.aux = {};
|
563
|
+
}
|
564
|
+
|
565
|
+
/**
|
566
|
+
* Creates a suplementary canvas at the element
|
567
|
+
* of the given id
|
568
|
+
*
|
569
|
+
* @param {string} id
|
570
|
+
* @returns {MuzzleCanvas}
|
571
|
+
*/
|
572
|
+
another(id) {
|
573
|
+
const muzzle = new MuzzleCanvas(id);
|
574
|
+
Muzzle.aux[id] = muzzle
|
575
|
+
return muzzle;
|
576
|
+
}
|
577
|
+
}
|
578
|
+
|
579
|
+
window['Muzzle'] = Muzzle;
|