jigsawpuzzlegame 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.
@@ -0,0 +1,1943 @@
1
+ "use strict";
2
+ /**
3
+ * JigsawPuzzle - A refactored, reusable jigsaw puzzle game class
4
+ *
5
+ * Stripped of all UI code, providing a clean interface for integration.
6
+ *
7
+ * @example
8
+ * const puzzle = new JigsawPuzzle('my-container-id', {
9
+ * image: 'https://example.com/image.jpg',
10
+ * numPieces: 20,
11
+ * shapeType: 0,
12
+ * allowRotation: true
13
+ * });
14
+ *
15
+ * puzzle.start();
16
+ */
17
+
18
+ // ============================================================================
19
+ // Constants
20
+ // ============================================================================
21
+
22
+ const FILE_EXTENSION = ".puz";
23
+ const FILE_SIGNATURE = "pzfilecct";
24
+
25
+ // Math shortcuts
26
+ const mhypot = Math.hypot,
27
+ mrandom = Math.random,
28
+ mmax = Math.max,
29
+ mmin = Math.min,
30
+ mround = Math.round,
31
+ mfloor = Math.floor,
32
+ mceil = Math.ceil,
33
+ msqrt = Math.sqrt,
34
+ mabs = Math.abs;
35
+
36
+ // ============================================================================
37
+ // Utility Functions
38
+ // ============================================================================
39
+
40
+ function alea(min, max) {
41
+ if (typeof max == "undefined") return min * mrandom();
42
+ return min + (max - min) * mrandom();
43
+ }
44
+
45
+ function intAlea(min, max) {
46
+ if (typeof max == "undefined") {
47
+ max = min;
48
+ min = 0;
49
+ }
50
+ return mfloor(min + (max - min) * mrandom());
51
+ }
52
+
53
+ function arrayShuffle(array) {
54
+ let k1, temp;
55
+ for (let k = array.length - 1; k >= 1; --k) {
56
+ k1 = intAlea(0, k + 1);
57
+ temp = array[k];
58
+ array[k] = array[k1];
59
+ array[k1] = temp;
60
+ }
61
+ return array;
62
+ }
63
+
64
+ // ============================================================================
65
+ // Pseudo-Random Number Generator
66
+ // ============================================================================
67
+
68
+ function mMash(seed) {
69
+ let n = 0xefc8249d;
70
+ let intSeed = (seed || Math.random()).toString();
71
+
72
+ function mash(data) {
73
+ if (data) {
74
+ data = data.toString();
75
+ for (var i = 0; i < data.length; i++) {
76
+ n += data.charCodeAt(i);
77
+ var h = 0.02519603282416938 * n;
78
+ n = h >>> 0;
79
+ h -= n;
80
+ h *= n;
81
+ n = h >>> 0;
82
+ h -= n;
83
+ n += h * 0x100000000;
84
+ }
85
+ return (n >>> 0) * 2.3283064365386963e-10;
86
+ }
87
+ n = 0xefc8249d;
88
+ return (n >>> 0) * 2.3283064365386963e-10;
89
+ }
90
+ n = mash(intSeed);
91
+
92
+ var x = function () {
93
+ n += 0x9e3779b9;
94
+ var h = 0x0274ebd7 * n;
95
+ return (h >>> 0) * 2.3283064365386963e-10;
96
+ };
97
+
98
+ x.intAlea = function (min, max) {
99
+ if (typeof max == "undefined") {
100
+ max = min;
101
+ min = 0;
102
+ }
103
+ return mfloor(min + (max - min) * x());
104
+ };
105
+
106
+ x.alea = function (min, max) {
107
+ if (typeof max == "undefined") return min * x();
108
+ return min + (max - min) * x();
109
+ };
110
+
111
+ x.reset = function () {
112
+ n = 0xefc8249d;
113
+ n = mash(intSeed);
114
+ };
115
+
116
+ x.seed = intSeed;
117
+ return x;
118
+ }
119
+
120
+ // ============================================================================
121
+ // Core Geometry Classes
122
+ // ============================================================================
123
+
124
+ class Point {
125
+ constructor(x, y) {
126
+ this.x = Number(x);
127
+ this.y = Number(y);
128
+ }
129
+ copy() {
130
+ return new Point(this.x, this.y);
131
+ }
132
+ distance(otherPoint) {
133
+ return mhypot(this.x - otherPoint.x, this.y - otherPoint.y);
134
+ }
135
+ }
136
+
137
+ class Segment {
138
+ constructor(p1, p2) {
139
+ this.p1 = new Point(p1.x, p1.y);
140
+ this.p2 = new Point(p2.x, p2.y);
141
+ }
142
+ dx() {
143
+ return this.p2.x - this.p1.x;
144
+ }
145
+ dy() {
146
+ return this.p2.y - this.p1.y;
147
+ }
148
+ length() {
149
+ return mhypot(this.dx(), this.dy());
150
+ }
151
+ pointOnRelative(coeff) {
152
+ let dx = this.dx();
153
+ let dy = this.dy();
154
+ return new Point(this.p1.x + coeff * dx, this.p1.y + coeff * dy);
155
+ }
156
+ }
157
+
158
+ class Side {
159
+ constructor() {
160
+ this.type = "";
161
+ this.points = [];
162
+ }
163
+
164
+ reversed() {
165
+ const ns = new Side();
166
+ ns.type = this.type;
167
+ ns.points = this.points.slice().reverse();
168
+ return ns;
169
+ }
170
+
171
+ scale(puzzle) {
172
+ const coefx = puzzle.scalex;
173
+ const coefy = puzzle.scaley;
174
+ this.scaledPoints = this.points.map(
175
+ (p) => new Point(p.x * coefx, p.y * coefy)
176
+ );
177
+ }
178
+
179
+ drawPath(ctx, shiftx, shifty, withoutMoveTo) {
180
+ if (!withoutMoveTo) {
181
+ ctx.moveTo(
182
+ this.scaledPoints[0].x + shiftx,
183
+ this.scaledPoints[0].y + shifty
184
+ );
185
+ }
186
+ if (this.type == "d") {
187
+ ctx.lineTo(
188
+ this.scaledPoints[1].x + shiftx,
189
+ this.scaledPoints[1].y + shifty
190
+ );
191
+ } else {
192
+ for (let k = 1; k < this.scaledPoints.length - 1; k += 3) {
193
+ ctx.bezierCurveTo(
194
+ this.scaledPoints[k].x + shiftx,
195
+ this.scaledPoints[k].y + shifty,
196
+ this.scaledPoints[k + 1].x + shiftx,
197
+ this.scaledPoints[k + 1].y + shifty,
198
+ this.scaledPoints[k + 2].x + shiftx,
199
+ this.scaledPoints[k + 2].y + shifty
200
+ );
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ // ============================================================================
207
+ // Piece Shape Functions (twist functions)
208
+ // ============================================================================
209
+
210
+ function twist0(side, ca, cb, prng) {
211
+ const seg0 = new Segment(side.points[0], side.points[1]);
212
+ const dxh = seg0.dx();
213
+ const dyh = seg0.dy();
214
+
215
+ const seg1 = new Segment(ca, cb);
216
+ const mid0 = seg0.pointOnRelative(0.5);
217
+ const mid1 = seg1.pointOnRelative(0.5);
218
+
219
+ const segMid = new Segment(mid0, mid1);
220
+ const dxv = segMid.dx();
221
+ const dyv = segMid.dy();
222
+
223
+ const scalex = prng.alea(0.8, 1);
224
+ const scaley = prng.alea(0.9, 1);
225
+ const mid = prng.alea(0.45, 0.55);
226
+
227
+ const pa = pointAt(mid - (1 / 12) * scalex, (1 / 12) * scaley);
228
+ const pb = pointAt(mid - (2 / 12) * scalex, (3 / 12) * scaley);
229
+ const pc = pointAt(mid, (4 / 12) * scaley);
230
+ const pd = pointAt(mid + (2 / 12) * scalex, (3 / 12) * scaley);
231
+ const pe = pointAt(mid + (1 / 12) * scalex, (1 / 12) * scaley);
232
+
233
+ side.points = [
234
+ seg0.p1,
235
+ new Point(
236
+ seg0.p1.x + (5 / 12) * dxh * 0.52,
237
+ seg0.p1.y + (5 / 12) * dyh * 0.52
238
+ ),
239
+ new Point(pa.x - (1 / 12) * dxv * 0.72, pa.y - (1 / 12) * dyv * 0.72),
240
+ pa,
241
+ new Point(pa.x + (1 / 12) * dxv * 0.72, pa.y + (1 / 12) * dyv * 0.72),
242
+ new Point(pb.x - (1 / 12) * dxv * 0.92, pb.y - (1 / 12) * dyv * 0.92),
243
+ pb,
244
+ new Point(pb.x + (1 / 12) * dxv * 0.52, pb.y + (1 / 12) * dyv * 0.52),
245
+ new Point(pc.x - (2 / 12) * dxh * 0.4, pc.y - (2 / 12) * dyh * 0.4),
246
+ pc,
247
+ new Point(pc.x + (2 / 12) * dxh * 0.4, pc.y + (2 / 12) * dyh * 0.4),
248
+ new Point(pd.x + (1 / 12) * dxv * 0.52, pd.y + (1 / 12) * dyv * 0.52),
249
+ pd,
250
+ new Point(pd.x - (1 / 12) * dxv * 0.92, pd.y - (1 / 12) * dyv * 0.92),
251
+ new Point(pe.x + (1 / 12) * dxv * 0.72, pe.y + (1 / 12) * dyv * 0.72),
252
+ pe,
253
+ new Point(pe.x - (1 / 12) * dxv * 0.72, pe.y - (1 / 12) * dyv * 0.72),
254
+ new Point(
255
+ seg0.p2.x - (5 / 12) * dxh * 0.52,
256
+ seg0.p2.y - (5 / 12) * dyh * 0.52
257
+ ),
258
+ seg0.p2
259
+ ];
260
+ side.type = "z";
261
+
262
+ function pointAt(coeffh, coeffv) {
263
+ return new Point(
264
+ seg0.p1.x + coeffh * dxh + coeffv * dxv,
265
+ seg0.p1.y + coeffh * dyh + coeffv * dyv
266
+ );
267
+ }
268
+ }
269
+
270
+ function twist1(side, ca, cb, prng) {
271
+ const seg0 = new Segment(side.points[0], side.points[1]);
272
+ const dxh = seg0.dx();
273
+ const dyh = seg0.dy();
274
+
275
+ const seg1 = new Segment(ca, cb);
276
+ const mid0 = seg0.pointOnRelative(0.5);
277
+ const mid1 = seg1.pointOnRelative(0.5);
278
+
279
+ const segMid = new Segment(mid0, mid1);
280
+ const dxv = segMid.dx();
281
+ const dyv = segMid.dy();
282
+
283
+ const pa = pointAt(
284
+ prng.alea(0.3, 0.35),
285
+ prng.alea(-0.05, 0.05)
286
+ );
287
+ const pb = pointAt(prng.alea(0.45, 0.55), prng.alea(0.2, 0.3));
288
+ const pc = pointAt(
289
+ prng.alea(0.65, 0.78),
290
+ prng.alea(-0.05, 0.05)
291
+ );
292
+
293
+ side.points = [
294
+ seg0.p1,
295
+ seg0.p1,
296
+ pa,
297
+ pa,
298
+ pa,
299
+ pb,
300
+ pb,
301
+ pb,
302
+ pc,
303
+ pc,
304
+ pc,
305
+ seg0.p2,
306
+ seg0.p2
307
+ ];
308
+ side.type = "z";
309
+
310
+ function pointAt(coeffh, coeffv) {
311
+ return new Point(
312
+ seg0.p1.x + coeffh * dxh + coeffv * dxv,
313
+ seg0.p1.y + coeffh * dyh + coeffv * dyv
314
+ );
315
+ }
316
+ }
317
+
318
+ function twist2(side, ca, cb, prng) {
319
+ const seg0 = new Segment(side.points[0], side.points[1]);
320
+ const dxh = seg0.dx();
321
+ const dyh = seg0.dy();
322
+
323
+ const seg1 = new Segment(ca, cb);
324
+ const mid0 = seg0.pointOnRelative(0.5);
325
+ const mid1 = seg1.pointOnRelative(0.5);
326
+
327
+ const segMid = new Segment(mid0, mid1);
328
+ const dxv = segMid.dx();
329
+ const dyv = segMid.dy();
330
+
331
+ const hmid = prng.alea(0.45, 0.55);
332
+ const vmid = prng.alea(0.4, 0.5);
333
+ const pc = pointAt(hmid, vmid);
334
+ let sega = new Segment(seg0.p1, pc);
335
+
336
+ const pb = sega.pointOnRelative(2 / 3);
337
+ sega = new Segment(seg0.p2, pc);
338
+ const pd = sega.pointOnRelative(2 / 3);
339
+
340
+ side.points = [seg0.p1, pb, pd, seg0.p2];
341
+ side.type = "z";
342
+
343
+ function pointAt(coeffh, coeffv) {
344
+ return new Point(
345
+ seg0.p1.x + coeffh * dxh + coeffv * dxv,
346
+ seg0.p1.y + coeffh * dyh + coeffv * dyv
347
+ );
348
+ }
349
+ }
350
+
351
+ function twist3(side, ca, cb, prng) {
352
+ side.points = [side.points[0], side.points[1]];
353
+ }
354
+
355
+ // ============================================================================
356
+ // Piece Classes
357
+ // ============================================================================
358
+
359
+ class Piece {
360
+ constructor(kx, ky) {
361
+ this.ts = new Side();
362
+ this.rs = new Side();
363
+ this.bs = new Side();
364
+ this.ls = new Side();
365
+ this.kx = kx;
366
+ this.ky = ky;
367
+ }
368
+
369
+ scale(puzzleInstance) {
370
+ this.ts.scale(puzzleInstance);
371
+ this.rs.scale(puzzleInstance);
372
+ this.bs.scale(puzzleInstance);
373
+ this.ls.scale(puzzleInstance);
374
+ }
375
+ }
376
+
377
+ // ============================================================================
378
+ // PolyPiece Class (needs puzzle instance reference)
379
+ // ============================================================================
380
+
381
+ class PolyPiece {
382
+ constructor(initialPiece, puzzleInstance) {
383
+ this.puzzle = puzzleInstance;
384
+ this.pckxmin = initialPiece.kx;
385
+ this.pckxmax = initialPiece.kx + 1;
386
+ this.pckymin = initialPiece.ky;
387
+ this.pckymax = initialPiece.ky + 1;
388
+ this.pieces = [initialPiece];
389
+ this.selected = false;
390
+ this.listLoops();
391
+
392
+ this.canvas = document.createElement("CANVAS");
393
+ this.puzzle.container.appendChild(this.canvas);
394
+ this.canvas.classList.add("polypiece");
395
+ this.ctx = this.canvas.getContext("2d");
396
+ this.rot = 0;
397
+ }
398
+
399
+ merge(otherPoly) {
400
+ const puzzle = this.puzzle;
401
+ const orgpckxmin = this.pckxmin;
402
+ const orgpckymin = this.pckymin;
403
+ const pbefore = getTransformed(
404
+ 0,
405
+ 0,
406
+ this.nx * puzzle.scalex,
407
+ this.ny * puzzle.scaley,
408
+ this.rot
409
+ );
410
+
411
+ const kOther = puzzle.polyPieces.indexOf(otherPoly);
412
+ puzzle.polyPieces.splice(kOther, 1);
413
+ puzzle.container.removeChild(otherPoly.canvas);
414
+
415
+ for (let k = 0; k < otherPoly.pieces.length; ++k) {
416
+ this.pieces.push(otherPoly.pieces[k]);
417
+ if (otherPoly.pieces[k].kx < this.pckxmin)
418
+ this.pckxmin = otherPoly.pieces[k].kx;
419
+ if (otherPoly.pieces[k].kx + 1 > this.pckxmax)
420
+ this.pckxmax = otherPoly.pieces[k].kx + 1;
421
+ if (otherPoly.pieces[k].ky < this.pckymin)
422
+ this.pckymin = otherPoly.pieces[k].ky;
423
+ if (otherPoly.pieces[k].ky + 1 > this.pckymax)
424
+ this.pckymax = otherPoly.pieces[k].ky + 1;
425
+ }
426
+
427
+ this.pieces.sort(function (p1, p2) {
428
+ if (p1.ky < p2.ky) return -1;
429
+ if (p1.ky > p2.ky) return 1;
430
+ if (p1.kx < p2.kx) return -1;
431
+ if (p1.kx > p2.kx) return 1;
432
+ return 0;
433
+ });
434
+
435
+ this.listLoops();
436
+ this.drawImage();
437
+
438
+ const pafter = getTransformed(
439
+ puzzle.scalex * (orgpckxmin - this.pckxmin),
440
+ puzzle.scaley * (orgpckymin - this.pckymin),
441
+ puzzle.scalex * (this.pckxmax - this.pckxmin + 1),
442
+ puzzle.scaley * (this.pckymax - this.pckymin + 1),
443
+ this.rot
444
+ );
445
+
446
+ this.moveTo(this.x - pafter.x + pbefore.x, this.y - pafter.y + pbefore.y);
447
+ puzzle.evaluateZIndex();
448
+
449
+ function getTransformed(orgx, orgy, width, height, rot) {
450
+ const dx = orgx - width / 2;
451
+ const dy = orgy - height / 2;
452
+ return {
453
+ x: width / 2 + [1, 0, -1, 0][rot] * dx + [0, -1, 0, 1][rot] * dy,
454
+ y: height / 2 + [0, 1, 0, -1][rot] * dx + [1, 0, -1, 0][rot] * dy
455
+ };
456
+ }
457
+ }
458
+
459
+ ifNear(otherPoly) {
460
+ const puzzle = this.puzzle;
461
+ if (this.rot != otherPoly.rot) return false;
462
+
463
+ let p1, p2;
464
+ let org = this.getOrgP();
465
+ let orgOther = otherPoly.getOrgP();
466
+
467
+ if (mhypot(org.x - orgOther.x, org.y - orgOther.y) >= puzzle.dConnect)
468
+ return false;
469
+
470
+ for (let k = this.pieces.length - 1; k >= 0; --k) {
471
+ p1 = this.pieces[k];
472
+ for (let ko = otherPoly.pieces.length - 1; ko >= 0; --ko) {
473
+ p2 = otherPoly.pieces[ko];
474
+ if (p1.kx == p2.kx && mabs(p1.ky - p2.ky) == 1) return true;
475
+ if (p1.ky == p2.ky && mabs(p1.kx - p2.kx) == 1) return true;
476
+ }
477
+ }
478
+
479
+ return false;
480
+ }
481
+
482
+ listLoops() {
483
+ const that = this;
484
+ function edgeIsCommon(kx, ky, edge) {
485
+ let k;
486
+ switch (edge) {
487
+ case 0: ky--; break;
488
+ case 1: kx++; break;
489
+ case 2: ky++; break;
490
+ case 3: kx--; break;
491
+ }
492
+ for (k = 0; k < that.pieces.length; k++) {
493
+ if (kx == that.pieces[k].kx && ky == that.pieces[k].ky) return true;
494
+ }
495
+ return false;
496
+ }
497
+
498
+ function edgeIsInTbEdges(kx, ky, edge) {
499
+ let k;
500
+ for (k = 0; k < tbEdges.length; k++) {
501
+ if (
502
+ kx == tbEdges[k].kx &&
503
+ ky == tbEdges[k].ky &&
504
+ edge == tbEdges[k].edge
505
+ )
506
+ return k;
507
+ }
508
+ return false;
509
+ }
510
+
511
+ let tbLoops = [];
512
+ let tbEdges = [];
513
+ let k;
514
+ let kEdge;
515
+ let lp;
516
+ let currEdge;
517
+ let tries;
518
+ let edgeNumber;
519
+ let potNext;
520
+
521
+ let tbTries = [
522
+ [
523
+ { dkx: 0, dky: 0, edge: 1 },
524
+ { dkx: 1, dky: 0, edge: 0 },
525
+ { dkx: 1, dky: -1, edge: 3 }
526
+ ],
527
+ [
528
+ { dkx: 0, dky: 0, edge: 2 },
529
+ { dkx: 0, dky: 1, edge: 1 },
530
+ { dkx: 1, dky: 1, edge: 0 }
531
+ ],
532
+ [
533
+ { dkx: 0, dky: 0, edge: 3 },
534
+ { dkx: -1, dky: 0, edge: 2 },
535
+ { dkx: -1, dky: 1, edge: 1 }
536
+ ],
537
+ [
538
+ { dkx: 0, dky: 0, edge: 0 },
539
+ { dkx: 0, dky: -1, edge: 3 },
540
+ { dkx: -1, dky: -1, edge: 2 }
541
+ ]
542
+ ];
543
+
544
+ for (k = 0; k < this.pieces.length; k++) {
545
+ for (kEdge = 0; kEdge < 4; kEdge++) {
546
+ if (!edgeIsCommon(this.pieces[k].kx, this.pieces[k].ky, kEdge))
547
+ tbEdges.push({
548
+ kx: this.pieces[k].kx,
549
+ ky: this.pieces[k].ky,
550
+ edge: kEdge,
551
+ kp: k
552
+ });
553
+ }
554
+ }
555
+
556
+ while (tbEdges.length > 0) {
557
+ lp = [];
558
+ currEdge = tbEdges[0];
559
+ lp.push(currEdge);
560
+ tbEdges.splice(0, 1);
561
+ do {
562
+ for (tries = 0; tries < 3; tries++) {
563
+ potNext = tbTries[currEdge.edge][tries];
564
+ edgeNumber = edgeIsInTbEdges(
565
+ currEdge.kx + potNext.dkx,
566
+ currEdge.ky + potNext.dky,
567
+ potNext.edge
568
+ );
569
+ if (edgeNumber === false) continue;
570
+ currEdge = tbEdges[edgeNumber];
571
+ lp.push(currEdge);
572
+ tbEdges.splice(edgeNumber, 1);
573
+ break;
574
+ }
575
+ if (edgeNumber === false) break;
576
+ } while (1);
577
+ tbLoops.push(lp);
578
+ }
579
+
580
+ this.tbLoops = tbLoops.map((loop) =>
581
+ loop.map((edge) => {
582
+ let cell = this.pieces[edge.kp];
583
+ if (edge.edge == 0) return cell.ts;
584
+ if (edge.edge == 1) return cell.rs;
585
+ if (edge.edge == 2) return cell.bs;
586
+ return cell.ls;
587
+ })
588
+ );
589
+ }
590
+
591
+ getRect() {
592
+ const puzzle = this.puzzle;
593
+ let rect0 = puzzle.container.getBoundingClientRect();
594
+ let rect = this.canvas.getBoundingClientRect();
595
+ return {
596
+ x: rect.x - rect0.x,
597
+ y: rect.y - rect0.y,
598
+ right: rect.right - rect0.x,
599
+ bottom: rect.bottom - rect0.y,
600
+ width: rect.width,
601
+ height: rect.height
602
+ };
603
+ }
604
+
605
+ getOrgP() {
606
+ const puzzle = this.puzzle;
607
+ const rect = this.getRect();
608
+ switch (this.rot) {
609
+ case 0:
610
+ return {
611
+ x: rect.x - puzzle.scalex * this.pckxmin,
612
+ y: rect.y - puzzle.scaley * this.pckymin
613
+ };
614
+ case 1:
615
+ return {
616
+ x: rect.right + puzzle.scaley * this.pckymin,
617
+ y: rect.y - puzzle.scalex * this.pckxmin
618
+ };
619
+ case 2:
620
+ return {
621
+ x: rect.right + puzzle.scalex * this.pckxmin,
622
+ y: rect.bottom + puzzle.scaley * this.pckymin
623
+ };
624
+ case 3:
625
+ return {
626
+ x: rect.x - puzzle.scaley * this.pckymin,
627
+ y: rect.bottom + puzzle.scalex * this.pckxmin
628
+ };
629
+ }
630
+ }
631
+
632
+ drawPath(ctx, shiftx, shifty) {
633
+ this.tbLoops.forEach((loop) => {
634
+ let without = false;
635
+ loop.forEach((side) => {
636
+ side.drawPath(ctx, shiftx, shifty, without);
637
+ without = true;
638
+ });
639
+ ctx.closePath();
640
+ });
641
+ }
642
+
643
+ drawImage(special) {
644
+ const puzzle = this.puzzle;
645
+ this.nx = this.pckxmax - this.pckxmin + 1;
646
+ this.ny = this.pckymax - this.pckymin + 1;
647
+ this.canvas.width = this.nx * puzzle.scalex;
648
+ this.canvas.height = this.ny * puzzle.scaley;
649
+
650
+ this.offsx = (this.pckxmin - 0.5) * puzzle.scalex;
651
+ this.offsy = (this.pckymin - 0.5) * puzzle.scaley;
652
+
653
+ this.path = new Path2D();
654
+ this.drawPath(this.path, -this.offsx, -this.offsy);
655
+
656
+ this.ctx.fillStyle = "none";
657
+ this.ctx.shadowColor = this.selected
658
+ ? special
659
+ ? "lime"
660
+ : "gold"
661
+ : "rgba(0, 0, 0, 0.5)";
662
+ this.ctx.shadowBlur = this.selected ? mmin(8, puzzle.scalex / 10) : 4;
663
+ this.ctx.shadowOffsetX = this.selected ? 0 : -4;
664
+ this.ctx.shadowOffsetY = this.selected ? 0 : 4;
665
+ this.ctx.fill(this.path);
666
+ if (this.selected) {
667
+ for (let i = 0; i < 6; i++) this.ctx.fill(this.path);
668
+ }
669
+ this.ctx.shadowColor = "rgba(0, 0, 0, 0)";
670
+
671
+ this.pieces.forEach((pp) => {
672
+ this.ctx.save();
673
+
674
+ const path = new Path2D();
675
+ const shiftx = -this.offsx;
676
+ const shifty = -this.offsy;
677
+ pp.ts.drawPath(path, shiftx, shifty, false);
678
+ pp.rs.drawPath(path, shiftx, shifty, true);
679
+ pp.bs.drawPath(path, shiftx, shifty, true);
680
+ pp.ls.drawPath(path, shiftx, shifty, true);
681
+ path.closePath();
682
+
683
+ this.ctx.clip(path);
684
+ const srcx = pp.kx ? (pp.kx - 0.5) * puzzle.scalex : 0;
685
+ const srcy = pp.ky ? (pp.ky - 0.5) * puzzle.scaley : 0;
686
+
687
+ const destx =
688
+ (pp.kx ? 0 : puzzle.scalex / 2) +
689
+ (pp.kx - this.pckxmin) * puzzle.scalex;
690
+ const desty =
691
+ (pp.ky ? 0 : puzzle.scaley / 2) +
692
+ (pp.ky - this.pckymin) * puzzle.scaley;
693
+
694
+ let w = 2 * puzzle.scalex;
695
+ let h = 2 * puzzle.scaley;
696
+ if (srcx + w > puzzle.gameCanvas.width)
697
+ w = puzzle.gameCanvas.width - srcx;
698
+ if (srcy + h > puzzle.gameCanvas.height)
699
+ h = puzzle.gameCanvas.height - srcy;
700
+
701
+ this.ctx.drawImage(
702
+ puzzle.gameCanvas,
703
+ srcx,
704
+ srcy,
705
+ w,
706
+ h,
707
+ destx,
708
+ desty,
709
+ w,
710
+ h
711
+ );
712
+ this.ctx.lineWidth = puzzle.embossThickness * 1.5;
713
+
714
+ this.ctx.translate(
715
+ puzzle.embossThickness / 2,
716
+ -puzzle.embossThickness / 2
717
+ );
718
+ this.ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
719
+ this.ctx.stroke(path);
720
+
721
+ this.ctx.translate(-puzzle.embossThickness, puzzle.embossThickness);
722
+ this.ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
723
+ this.ctx.stroke(path);
724
+
725
+ this.ctx.restore();
726
+ this.canvas.style.transform = `rotate(${90 * this.rot}deg)`;
727
+ });
728
+ }
729
+
730
+ moveTo(x, y) {
731
+ this.x = x;
732
+ this.y = y;
733
+ this.canvas.style.left = x + "px";
734
+ this.canvas.style.top = y + "px";
735
+ }
736
+
737
+ moveToInitialPlace() {
738
+ const puzzle = this.puzzle;
739
+ this.moveTo(
740
+ puzzle.offsx + (this.pckxmin - 0.5) * puzzle.scalex,
741
+ puzzle.offsy + (this.pckymin - 0.5) * puzzle.scaley
742
+ );
743
+ }
744
+
745
+ rotate(angle) {
746
+ this.rot = angle;
747
+ }
748
+
749
+ isPointInPath(p) {
750
+ let npath = new Path2D();
751
+ this.drawPath(npath, 0, 0);
752
+ let rect = this.getRect();
753
+
754
+ let pRefx = [rect.x, rect.right, rect.right, rect.x][this.rot];
755
+ let pRefy = [rect.y, rect.y, rect.bottom, rect.bottom][this.rot];
756
+
757
+ let mposx =
758
+ [1, 0, -1, 0][this.rot] * (p.x - pRefx) +
759
+ [0, 1, 0, -1][this.rot] * (p.y - pRefy);
760
+ let mposy =
761
+ [0, -1, 0, 1][this.rot] * (p.x - pRefx) +
762
+ [1, 0, -1, 0][this.rot] * (p.y - pRefy);
763
+
764
+ return this.ctx.isPointInPath(this.path, mposx, mposy);
765
+ }
766
+
767
+ coerceToContainer() {
768
+ const puzzle = this.puzzle;
769
+ let dimx = [puzzle.scalex, puzzle.scaley, puzzle.scalex, puzzle.scaley][
770
+ this.rot
771
+ ];
772
+ let dimy = [puzzle.scaley, puzzle.scalex, puzzle.scaley, puzzle.scalex][
773
+ this.rot
774
+ ];
775
+ const rect = this.getRect();
776
+ if (rect.y > -dimy && rect.bottom < puzzle.contHeight + dimy) {
777
+ if (rect.right < dimx) {
778
+ this.moveTo(this.x + dimx - rect.right, this.y);
779
+ return;
780
+ }
781
+ if (rect.x > puzzle.contWidth - dimx) {
782
+ this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
783
+ return;
784
+ }
785
+ return;
786
+ }
787
+ if (rect.x > -dimx && rect.right < puzzle.contHeight + dimy) {
788
+ if (rect.bottom < dimy) {
789
+ this.moveTo(this.x, this.y + dimy - rect.bottom);
790
+ return;
791
+ }
792
+ if (rect.y > puzzle.contHeight - dimy) {
793
+ this.moveTo(this.x, this.y + puzzle.contHeight - dimy - rect.y);
794
+ return;
795
+ }
796
+ return;
797
+ }
798
+ if (rect.y < -dimy) {
799
+ this.moveTo(this.x, this.y - rect.y - dimy);
800
+ this.getRect();
801
+ }
802
+ if (rect.bottom > puzzle.contHeight + dimy) {
803
+ this.moveTo(this.x, this.y + puzzle.contHeight + dimy - rect.bottom);
804
+ this.getRect();
805
+ }
806
+ if (rect.right < dimx) {
807
+ this.moveTo(this.x + dimx - rect.right, this.y);
808
+ return;
809
+ }
810
+ if (rect.x > puzzle.contWidth - dimx) {
811
+ this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
812
+ return;
813
+ }
814
+ }
815
+ }
816
+
817
+ // ============================================================================
818
+ // Helper Functions
819
+ // ============================================================================
820
+
821
+ function fitImage(img, width, height) {
822
+ let wn = img.naturalWidth;
823
+ let hn = img.naturalHeight;
824
+ let w = width;
825
+ let h = (w * hn) / wn;
826
+ if (h > height) {
827
+ h = height;
828
+ w = (h * wn) / hn;
829
+ }
830
+ img.style.position = "absolute";
831
+ img.style.width = w + "px";
832
+ img.style.height = h + "px";
833
+ img.style.top = "50%";
834
+ img.style.left = "50%";
835
+ img.style.transform = "translate(-50%,-50%)";
836
+ }
837
+
838
+ // ============================================================================
839
+ // Internal Puzzle Class (refactored to be UI-independent)
840
+ // ============================================================================
841
+
842
+ class InternalPuzzle {
843
+ constructor(container) {
844
+ this.container = typeof container === "string"
845
+ ? document.getElementById(container)
846
+ : container;
847
+
848
+ this.gameCanvas = document.createElement("CANVAS");
849
+ this.container.appendChild(this.gameCanvas);
850
+
851
+ this.srcImage = new Image();
852
+ this.imageLoaded = false;
853
+
854
+ // State
855
+ this.nbPieces = 20;
856
+ this.rotationAllowed = false;
857
+ this.typeOfShape = 0;
858
+ }
859
+
860
+ getContainerSize() {
861
+ let styl = window.getComputedStyle(this.container);
862
+ this.contWidth = parseFloat(styl.width);
863
+ this.contHeight = parseFloat(styl.height);
864
+ }
865
+
866
+ create(baseData) {
867
+ this.prng = mMash(baseData ? baseData[3] : null);
868
+ this.container.innerHTML = "";
869
+ this.getContainerSize();
870
+
871
+ if (baseData) {
872
+ this.nx = baseData[0];
873
+ this.ny = baseData[1];
874
+ // baseData[2] is total game width (scalex * nx), not scalex itself
875
+ // scalex will be calculated in doScale()
876
+ this.rotationAllowed = !!baseData[4];
877
+ this.typeOfShape = baseData[5];
878
+ } else {
879
+ this.computenxAndny();
880
+ }
881
+
882
+ this.relativeHeight =
883
+ this.srcImage.naturalHeight /
884
+ this.ny /
885
+ (this.srcImage.naturalWidth / this.nx);
886
+
887
+ if (!baseData) {
888
+ this.typeOfShape = this.typeOfShape || 0;
889
+ }
890
+
891
+ this.defineShapes({
892
+ coeffDecentr: 0.12,
893
+ twistf: [twist0, twist1, twist2, twist3][this.typeOfShape]
894
+ });
895
+
896
+ this.polyPieces = [];
897
+ if (!baseData) {
898
+ this.pieces.forEach((row) =>
899
+ row.forEach((piece) => {
900
+ this.polyPieces.push(new PolyPiece(piece, this));
901
+ })
902
+ );
903
+ arrayShuffle(this.polyPieces);
904
+ if (this.rotationAllowed)
905
+ this.polyPieces.forEach((pp) => (pp.rot = intAlea(4)));
906
+ } else {
907
+ const pps = baseData[8];
908
+ const offs = this.rotationAllowed ? 3 : 2;
909
+ pps.forEach((ppData) => {
910
+ let polyp = new PolyPiece(this.pieces[ppData[offs + 1]][ppData[offs]], this);
911
+ polyp.x = ppData[0];
912
+ polyp.y = ppData[1];
913
+ polyp.rot = this.rotationAllowed ? ppData[2] : 0;
914
+ for (let k = offs + 2; k < ppData.length; k += 2) {
915
+ let kx = ppData[k];
916
+ let ky = ppData[k + 1];
917
+ polyp.pieces.push(this.pieces[ky][kx]);
918
+ polyp.pckxmin = mmin(polyp.pckxmin, kx);
919
+ polyp.pckxmax = mmax(polyp.pckxmax, kx + 1);
920
+ polyp.pckymin = mmin(polyp.pckymin, ky);
921
+ polyp.pckymax = mmax(polyp.pckymax, ky + 1);
922
+ }
923
+ polyp.listLoops();
924
+ this.polyPieces.push(polyp);
925
+ });
926
+ }
927
+ this.evaluateZIndex();
928
+ }
929
+
930
+ computenxAndny() {
931
+ let kx, ky,
932
+ width = this.srcImage.naturalWidth,
933
+ height = this.srcImage.naturalHeight,
934
+ npieces = this.nbPieces;
935
+ let err, errmin = 1e9;
936
+ let ncv, nch;
937
+
938
+ let nHPieces = mround(msqrt((npieces * width) / height));
939
+ let nVPieces = mround(npieces / nHPieces);
940
+
941
+ for (ky = 0; ky < 5; ky++) {
942
+ ncv = nVPieces + ky - 2;
943
+ for (kx = 0; kx < 5; kx++) {
944
+ nch = nHPieces + kx - 2;
945
+ err = (nch * height) / ncv / width;
946
+ err = err + 1 / err - 2;
947
+ err += mabs(1 - (nch * ncv) / npieces);
948
+
949
+ if (err < errmin) {
950
+ errmin = err;
951
+ this.nx = nch;
952
+ this.ny = ncv;
953
+ }
954
+ }
955
+ }
956
+ }
957
+
958
+ defineShapes(shapeDesc) {
959
+ let { coeffDecentr, twistf } = shapeDesc;
960
+ const corners = [];
961
+ const nx = this.nx, ny = this.ny;
962
+ let np;
963
+
964
+ for (let ky = 0; ky <= ny; ++ky) {
965
+ corners[ky] = [];
966
+ for (let kx = 0; kx <= nx; ++kx) {
967
+ corners[ky][kx] = new Point(
968
+ kx + this.prng.alea(-coeffDecentr, coeffDecentr),
969
+ ky + this.prng.alea(-coeffDecentr, coeffDecentr)
970
+ );
971
+ if (kx == 0) corners[ky][kx].x = 0;
972
+ if (kx == nx) corners[ky][kx].x = nx;
973
+ if (ky == 0) corners[ky][kx].y = 0;
974
+ if (ky == ny) corners[ky][kx].y = ny;
975
+ }
976
+ }
977
+
978
+ this.pieces = [];
979
+ for (let ky = 0; ky < ny; ++ky) {
980
+ this.pieces[ky] = [];
981
+ for (let kx = 0; kx < nx; ++kx) {
982
+ this.pieces[ky][kx] = np = new Piece(kx, ky);
983
+ if (ky == 0) {
984
+ np.ts.points = [corners[ky][kx], corners[ky][kx + 1]];
985
+ np.ts.type = "d";
986
+ } else {
987
+ np.ts = this.pieces[ky - 1][kx].bs.reversed();
988
+ }
989
+ np.rs.points = [corners[ky][kx + 1], corners[ky + 1][kx + 1]];
990
+ np.rs.type = "d";
991
+ if (kx < nx - 1) {
992
+ if (this.prng.intAlea(2))
993
+ twistf(np.rs, corners[ky][kx], corners[ky + 1][kx], this.prng);
994
+ else twistf(np.rs, corners[ky][kx + 2], corners[ky + 1][kx + 2], this.prng);
995
+ }
996
+ if (kx == 0) {
997
+ np.ls.points = [corners[ky + 1][kx], corners[ky][kx]];
998
+ np.ls.type = "d";
999
+ } else {
1000
+ np.ls = this.pieces[ky][kx - 1].rs.reversed();
1001
+ }
1002
+ np.bs.points = [corners[ky + 1][kx + 1], corners[ky + 1][kx]];
1003
+ np.bs.type = "d";
1004
+ if (ky < ny - 1) {
1005
+ if (this.prng.intAlea(2))
1006
+ twistf(np.bs, corners[ky][kx + 1], corners[ky][kx], this.prng);
1007
+ else twistf(np.bs, corners[ky + 2][kx + 1], corners[ky + 2][kx], this.prng);
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ scale() {
1014
+ const maxWidth = 0.95 * this.contWidth;
1015
+ const maxHeight = 0.95 * this.contHeight;
1016
+ const woh = this.srcImage.naturalWidth / this.srcImage.naturalHeight;
1017
+ let bestWidth = 0;
1018
+ let piecex, piecey;
1019
+
1020
+ let xtra = this.nx * this.ny * 1.2;
1021
+ for (let extrax = 0; extrax <= mceil(this.nx * 0.2); ++extrax) {
1022
+ let availx = extrax == 0 ? maxWidth : this.contWidth;
1023
+ for (let extray = 0; extray <= mceil(this.ny * 0.2); ++extray) {
1024
+ if ((this.nx + extrax) * (this.ny + extray) < xtra) continue;
1025
+ let availy = extray == 0 ? maxHeight : this.contHeight;
1026
+ piecex = availx / (this.nx + extrax);
1027
+ piecey = (piecex * this.nx) / woh / this.ny;
1028
+ if (piecey * (this.ny + extray) > availy) {
1029
+ piecey = availy / (this.ny + extray);
1030
+ piecex = (piecey * this.ny * woh) / this.nx;
1031
+ }
1032
+ if (piecex * this.nx > bestWidth) bestWidth = piecex * this.nx;
1033
+ }
1034
+ }
1035
+
1036
+ this.doScale(bestWidth);
1037
+ }
1038
+
1039
+ doScale(width) {
1040
+ this.gameWidth = width;
1041
+ this.gameHeight = (width * this.srcImage.naturalHeight) / this.srcImage.naturalWidth;
1042
+
1043
+ this.gameCanvas.width = this.gameWidth;
1044
+ this.gameCanvas.height = this.gameHeight;
1045
+ this.gameCtx = this.gameCanvas.getContext("2d");
1046
+ this.gameCtx.drawImage(this.srcImage, 0, 0, this.gameWidth, this.gameHeight);
1047
+
1048
+ this.gameCanvas.classList.add("gameCanvas");
1049
+ this.gameCanvas.style.zIndex = 500;
1050
+
1051
+ this.scalex = this.gameWidth / this.nx;
1052
+ this.scaley = this.gameHeight / this.ny;
1053
+
1054
+ this.pieces.forEach((row) => {
1055
+ row.forEach((piece) => piece.scale(this));
1056
+ });
1057
+
1058
+ this.offsx = (this.contWidth - this.gameWidth) / 2;
1059
+ this.offsy = (this.contHeight - this.gameHeight) / 2;
1060
+ this.dConnect = mmax(10, mmin(this.scalex, this.scaley) / 10);
1061
+ this.embossThickness = mmin(2 + (this.scalex / 200) * (5 - 2), 5);
1062
+ }
1063
+
1064
+ sweepBy(dx, dy) {
1065
+ this.polyPieces.forEach((pp) => {
1066
+ pp.moveTo(pp.x + dx, pp.y + dy);
1067
+ });
1068
+ }
1069
+
1070
+ zoomBy(coef, center) {
1071
+ let futWidth = this.gameWidth * coef;
1072
+ let futHeight = this.gameHeight * coef;
1073
+ if (
1074
+ ((futWidth > 3000 || futHeight > 3000) && coef > 1) ||
1075
+ (futWidth < 200 || futHeight < 200) & (coef < 1)
1076
+ )
1077
+ return;
1078
+ if (coef == 1) return;
1079
+
1080
+ this.doScale(futWidth);
1081
+ this.polyPieces.forEach((pp) => {
1082
+ pp.moveTo(
1083
+ coef * (pp.x - center.x) + center.x,
1084
+ coef * (pp.y - center.y) + center.y
1085
+ );
1086
+ pp.drawImage();
1087
+ });
1088
+ }
1089
+
1090
+ relativeMouseCoordinates(event) {
1091
+ const br = this.container.getBoundingClientRect();
1092
+ return {
1093
+ x: event.clientX - br.x,
1094
+ y: event.clientY - br.y
1095
+ };
1096
+ }
1097
+
1098
+ limitRectangle(rect) {
1099
+ let minscale = mmin(this.scalex, this.scaley);
1100
+ rect.x0 = mmin(mmax(rect.x0, -minscale / 2), this.contWidth - 1.5 * minscale);
1101
+ rect.x1 = mmin(mmax(rect.x1, -minscale / 2), this.contWidth - 1.5 * minscale);
1102
+ rect.y0 = mmin(mmax(rect.y0, -minscale / 2), this.contHeight - 1.5 * minscale);
1103
+ rect.y1 = mmin(mmax(rect.y1, -minscale / 2), this.contHeight - 1.5 * minscale);
1104
+ }
1105
+
1106
+ spreadInRectangle(rect) {
1107
+ this.limitRectangle(rect);
1108
+ this.polyPieces.forEach((pp) =>
1109
+ pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1110
+ );
1111
+ }
1112
+
1113
+ spreadSetInRectangle(set, rect) {
1114
+ this.limitRectangle(rect);
1115
+ set.forEach((pp) =>
1116
+ pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1117
+ );
1118
+ }
1119
+
1120
+ optimInitial() {
1121
+ const minx = -this.scalex / 2;
1122
+ const miny = -this.scaley / 2;
1123
+ const maxx = this.contWidth - 1.5 * this.scalex;
1124
+ const maxy = this.contHeight - 1.5 * this.scaley;
1125
+ let freex = this.contWidth - this.gameWidth;
1126
+ let freey = this.contHeight - this.gameHeight;
1127
+
1128
+ let where = [0, 0, 0, 0];
1129
+ let rects = [];
1130
+ if (freex > 1.5 * this.scalex) {
1131
+ where[1] = 1;
1132
+ rects[1] = {
1133
+ x0: this.gameWidth - 0.5 * this.scalex,
1134
+ x1: maxx,
1135
+ y0: miny,
1136
+ y1: maxy
1137
+ };
1138
+ }
1139
+ if (freex > 3 * this.scalex) {
1140
+ where[3] = 1;
1141
+ rects[3] = {
1142
+ x0: minx,
1143
+ x1: freex / 2 - 1.5 * this.scalex,
1144
+ y0: miny,
1145
+ y1: maxy
1146
+ };
1147
+ rects[1].x0 = this.contWidth - freex / 2 - 0.5 * this.scalex;
1148
+ }
1149
+ if (freey > 1.5 * this.scaley) {
1150
+ where[2] = 1;
1151
+ rects[2] = {
1152
+ x0: minx,
1153
+ x1: maxx,
1154
+ y0: this.gameHeight - 0.5 * this.scaley,
1155
+ y1: this.contHeight - 1.5 * this.scaley
1156
+ };
1157
+ }
1158
+ if (freey > 3 * this.scaley) {
1159
+ where[0] = 1;
1160
+ rects[0] = {
1161
+ x0: minx,
1162
+ x1: maxx,
1163
+ y0: miny,
1164
+ y1: freey / 2 - 1.5 * this.scaley
1165
+ };
1166
+ rects[2].y0 = this.contHeight - freey / 2 - 0.5 * this.scaley;
1167
+ }
1168
+ if (where.reduce((sum, a) => sum + a) < 2) {
1169
+ if (freex - freey > 0.2 * this.scalex || where[1]) {
1170
+ this.spreadInRectangle({
1171
+ x0: this.gameWidth - this.scalex / 2,
1172
+ x1: maxx,
1173
+ y0: miny,
1174
+ y1: maxy
1175
+ });
1176
+ } else if (freey - freex > 0.2 * this.scalex || where[2]) {
1177
+ this.spreadInRectangle({
1178
+ x0: minx,
1179
+ x1: maxx,
1180
+ y0: this.gameHeight - this.scaley / 2,
1181
+ y1: maxy
1182
+ });
1183
+ } else {
1184
+ if (this.gameWidth > this.gameHeight) {
1185
+ this.spreadInRectangle({
1186
+ x0: minx,
1187
+ x1: maxx,
1188
+ y0: this.gameHeight - this.scaley / 2,
1189
+ y1: maxy
1190
+ });
1191
+ } else {
1192
+ this.spreadInRectangle({
1193
+ x0: this.gameWidth - this.scalex / 2,
1194
+ x1: maxx,
1195
+ y0: miny,
1196
+ y1: maxy
1197
+ });
1198
+ }
1199
+ }
1200
+ return;
1201
+ }
1202
+ let nrects = [];
1203
+ rects.forEach((rect) => nrects.push(rect));
1204
+ let k0 = 0;
1205
+ const npTot = this.nx * this.ny;
1206
+ for (let k = 0; k < nrects.length; ++k) {
1207
+ let k1 = mround(((k + 1) / nrects.length) * npTot);
1208
+ this.spreadSetInRectangle(this.polyPieces.slice(k0, k1), nrects[k]);
1209
+ k0 = k1;
1210
+ }
1211
+ arrayShuffle(this.polyPieces);
1212
+ this.evaluateZIndex();
1213
+ }
1214
+
1215
+ evaluateZIndex() {
1216
+ for (let k = this.polyPieces.length - 1; k > 0; --k) {
1217
+ if (
1218
+ this.polyPieces[k].pieces.length > this.polyPieces[k - 1].pieces.length
1219
+ ) {
1220
+ [this.polyPieces[k], this.polyPieces[k - 1]] = [
1221
+ this.polyPieces[k - 1],
1222
+ this.polyPieces[k]
1223
+ ];
1224
+ }
1225
+ }
1226
+ this.polyPieces.forEach((pp, k) => {
1227
+ pp.canvas.style.zIndex = k + 10;
1228
+ });
1229
+ this.zIndexSup = this.polyPieces.length + 10;
1230
+ }
1231
+
1232
+ getStateData() {
1233
+ let ppData;
1234
+ let saved = { signature: FILE_SIGNATURE };
1235
+ if ("origin" in this.srcImage.dataset) {
1236
+ saved.origin = this.srcImage.dataset.origin;
1237
+ }
1238
+ saved.src = this.srcImage.src;
1239
+ let base = [
1240
+ this.nx,
1241
+ this.ny,
1242
+ this.scalex * this.nx,
1243
+ this.prng.seed,
1244
+ this.rotationAllowed ? 1 : 0,
1245
+ this.typeOfShape,
1246
+ this.srcImage.naturalWidth,
1247
+ this.srcImage.naturalHeight
1248
+ ];
1249
+ saved.base = base;
1250
+ let pps = [];
1251
+ base.push(pps);
1252
+ this.polyPieces.forEach((pp) => {
1253
+ ppData = [mround(pp.x), mround(pp.y)];
1254
+ if (this.rotationAllowed) ppData.push(pp.rot);
1255
+ pp.pieces.forEach((p) => ppData.push(p.kx, p.ky));
1256
+ pps.push(ppData);
1257
+ });
1258
+ return saved;
1259
+ }
1260
+ }
1261
+
1262
+ // JigsawPuzzle Wrapper Class
1263
+ // This will be appended to jigsaw-puzzle-class.js
1264
+
1265
+ // ============================================================================
1266
+ // JigsawPuzzle - Main Public API Class
1267
+ // ============================================================================
1268
+
1269
+ export class JigsawPuzzle {
1270
+ /**
1271
+ * Creates a new JigsawPuzzle instance
1272
+ * @param {string|HTMLElement} containerId - ID of container div or element itself
1273
+ * @param {Object} options - Configuration options
1274
+ * @param {string} options.image - Image URL or data URL to use
1275
+ * @param {number} options.numPieces - Number of puzzle pieces (default: 20)
1276
+ * @param {number} options.shapeType - Shape type 0-3 (default: 0)
1277
+ * @param {boolean} options.allowRotation - Allow piece rotation (default: false)
1278
+ * @param {Function} options.onReady - Callback when puzzle is ready (image loaded, state 15)
1279
+ * @param {Function} options.onWin - Callback when puzzle is solved
1280
+ * @param {Function} options.onStart - Callback when game starts
1281
+ * @param {Function} options.onStop - Callback when game stops
1282
+ */
1283
+ constructor(containerId, options = {}) {
1284
+ const container = typeof containerId === "string"
1285
+ ? document.getElementById(containerId)
1286
+ : containerId;
1287
+
1288
+ if (!container) {
1289
+ throw new Error("Container element not found");
1290
+ }
1291
+
1292
+ // Store options
1293
+ this.options = {
1294
+ image: options.image || null,
1295
+ numPieces: options.numPieces || 20,
1296
+ shapeType: options.shapeType || 0,
1297
+ allowRotation: options.allowRotation || false,
1298
+ onReady: options.onReady || null,
1299
+ onWin: options.onWin || null,
1300
+ onStart: options.onStart || null,
1301
+ onStop: options.onStop || null
1302
+ };
1303
+
1304
+ // Create internal puzzle instance
1305
+ this.puzzle = new InternalPuzzle(container);
1306
+ this.puzzle.nbPieces = this.options.numPieces;
1307
+ this.puzzle.rotationAllowed = this.options.allowRotation;
1308
+ this.puzzle.typeOfShape = this.options.shapeType;
1309
+
1310
+ // Animation state
1311
+ this.events = [];
1312
+ this.state = 0;
1313
+ this.moving = {};
1314
+ this.tmpImage = null;
1315
+ this.lastMousePos = { x: 0, y: 0 };
1316
+ this.useMouse = true;
1317
+ this.playing = false;
1318
+ this.animationFrameId = null;
1319
+ this.restoredState = null;
1320
+ this.restoredString = "";
1321
+
1322
+ // Setup event handlers
1323
+ this._setupEventHandlers();
1324
+
1325
+ // Setup image load handler
1326
+ this.puzzle.srcImage.addEventListener("load", () => this._imageLoaded());
1327
+
1328
+ // Setup resize handler
1329
+ window.addEventListener("resize", () => {
1330
+ if (this.events.length && this.events[this.events.length - 1].event === "resize") return;
1331
+ this.events.push({ event: "resize" });
1332
+ });
1333
+
1334
+ // Load initial image if provided
1335
+ if (this.options.image) {
1336
+ this.setImage(this.options.image);
1337
+ }
1338
+
1339
+ // Start animation loop
1340
+ this._animate(0);
1341
+ }
1342
+
1343
+ _setupEventHandlers() {
1344
+ const puzzle = this.puzzle;
1345
+ const container = puzzle.container;
1346
+
1347
+ container.addEventListener("mousedown", (event) => {
1348
+ this.useMouse = true;
1349
+ event.preventDefault();
1350
+ if (event.button !== 0) return;
1351
+ this.events.push({
1352
+ event: "touch",
1353
+ position: puzzle.relativeMouseCoordinates(event)
1354
+ });
1355
+ });
1356
+
1357
+ container.addEventListener("touchstart", (event) => {
1358
+ this.useMouse = false;
1359
+ event.preventDefault();
1360
+ if (event.touches.length === 0) return;
1361
+ const rTouch = [];
1362
+ for (let k = 0; k < event.touches.length; ++k) {
1363
+ rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1364
+ }
1365
+ if (event.touches.length === 1) {
1366
+ this.events.push({ event: "touch", position: rTouch[0] });
1367
+ }
1368
+ if (event.touches.length === 2) {
1369
+ this.events.push({ event: "touches", touches: rTouch });
1370
+ }
1371
+ }, { passive: false });
1372
+
1373
+ const handleLeave = () => {
1374
+ this.events.push({ event: "leave" });
1375
+ };
1376
+
1377
+ container.addEventListener("mouseup", (event) => {
1378
+ this.useMouse = true;
1379
+ event.preventDefault();
1380
+ if (event.button !== 0) return;
1381
+ handleLeave();
1382
+ });
1383
+ container.addEventListener("touchend", handleLeave);
1384
+ container.addEventListener("touchleave", handleLeave);
1385
+ container.addEventListener("touchcancel", handleLeave);
1386
+
1387
+ container.addEventListener("mousemove", (event) => {
1388
+ this.useMouse = true;
1389
+ event.preventDefault();
1390
+ if (this.events.length && this.events[this.events.length - 1].event === "move")
1391
+ this.events.pop();
1392
+ const pos = puzzle.relativeMouseCoordinates(event);
1393
+ this.lastMousePos = pos;
1394
+ this.events.push({
1395
+ event: "move",
1396
+ position: pos,
1397
+ ev: event
1398
+ });
1399
+ });
1400
+
1401
+ container.addEventListener("touchmove", (event) => {
1402
+ this.useMouse = false;
1403
+ event.preventDefault();
1404
+ const rTouch = [];
1405
+ if (event.touches.length === 0) return;
1406
+ for (let k = 0; k < event.touches.length; ++k) {
1407
+ rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1408
+ }
1409
+ if (event.touches.length === 1) {
1410
+ if (this.events.length && this.events[this.events.length - 1].event === "move")
1411
+ this.events.pop();
1412
+ this.events.push({ event: "move", position: rTouch[0] });
1413
+ }
1414
+ if (event.touches.length === 2) {
1415
+ if (this.events.length && this.events[this.events.length - 1].event === "moves")
1416
+ this.events.pop();
1417
+ this.events.push({ event: "moves", touches: rTouch });
1418
+ }
1419
+ }, { passive: false });
1420
+
1421
+ container.addEventListener("wheel", (event) => {
1422
+ this.useMouse = true;
1423
+ event.preventDefault();
1424
+ if (this.events.length && this.events[this.events.length - 1].event === "wheel")
1425
+ this.events.pop();
1426
+ this.events.push({ event: "wheel", wheel: event });
1427
+ });
1428
+ }
1429
+
1430
+ _imageLoaded() {
1431
+ this.puzzle.imageLoaded = true;
1432
+ let event = { event: "srcImageLoaded" };
1433
+ if (this.restoredState) {
1434
+ if (
1435
+ mround(this.puzzle.srcImage.naturalWidth) !== this.restoredState.base[6] ||
1436
+ mround(this.puzzle.srcImage.naturalHeight) !== this.restoredState.base[7]
1437
+ ) {
1438
+ event.event = "wrongImage";
1439
+ this.restoredState = null;
1440
+ }
1441
+ }
1442
+ this.events.push(event);
1443
+ }
1444
+
1445
+ _animate(tStamp) {
1446
+ this.animationFrameId = requestAnimationFrame((ts) => this._animate(ts));
1447
+
1448
+ let event;
1449
+ if (this.events.length) event = this.events.shift();
1450
+ if (event && event.event === "reset") this.state = 0;
1451
+
1452
+ // Resize event
1453
+ if (event && event.event === "resize") {
1454
+ const puzzle = this.puzzle;
1455
+ const prevWidth = puzzle.contWidth;
1456
+ const prevHeight = puzzle.contHeight;
1457
+ puzzle.getContainerSize();
1458
+ if (this.state === 15 || this.state === 60) {
1459
+ fitImage(this.tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
1460
+ } else if (this.state >= 25) {
1461
+ const prevGameWidth = puzzle.gameWidth;
1462
+ const prevGameHeight = puzzle.gameHeight;
1463
+ puzzle.scale();
1464
+ const reScale = puzzle.contWidth / prevWidth;
1465
+ puzzle.polyPieces.forEach((pp) => {
1466
+ let nx = puzzle.contWidth / 2 - (prevWidth / 2 - pp.x) * reScale;
1467
+ let ny = puzzle.contHeight / 2 - (prevHeight / 2 - pp.y) * reScale;
1468
+ nx = mmin(mmax(nx, -puzzle.scalex / 2), puzzle.contWidth - 1.5 * puzzle.scalex);
1469
+ ny = mmin(mmax(ny, -puzzle.scaley / 2), puzzle.contHeight - 1.5 * puzzle.scaley);
1470
+ pp.moveTo(nx, ny);
1471
+ pp.drawImage();
1472
+ });
1473
+ }
1474
+ return;
1475
+ }
1476
+
1477
+ switch (this.state) {
1478
+ case 0:
1479
+ this.state = 10;
1480
+ // fall through
1481
+
1482
+ case 10:
1483
+ this.playing = false;
1484
+ if (!this.puzzle.imageLoaded) return;
1485
+ this.puzzle.container.innerHTML = "";
1486
+ this.tmpImage = document.createElement("img");
1487
+ this.tmpImage.src = this.puzzle.srcImage.src;
1488
+ this.puzzle.getContainerSize();
1489
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1490
+ this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1491
+ this.puzzle.container.appendChild(this.tmpImage);
1492
+ this.state = 15;
1493
+ // Call onReady callback when puzzle is ready (image loaded and displayed)
1494
+ if (this.options.onReady) {
1495
+ this.options.onReady();
1496
+ }
1497
+ break;
1498
+
1499
+ case 15:
1500
+ this.playing = false;
1501
+ if (!event) return;
1502
+ if (event.event === "nbpieces") {
1503
+ this.puzzle.nbPieces = event.nbpieces;
1504
+ this.state = 20;
1505
+ } else if (event.event === "srcImageLoaded") {
1506
+ this.state = 10;
1507
+ return;
1508
+ } else if (event.event === "restore") {
1509
+ this.state = 150;
1510
+ return;
1511
+ } else return;
1512
+
1513
+ case 20:
1514
+ this.playing = true;
1515
+ if (this.options.onStart) this.options.onStart();
1516
+ this.puzzle.rotationAllowed = this.options.allowRotation;
1517
+ if (this.restoredState) {
1518
+ this.puzzle.create(this.restoredState.base);
1519
+ } else {
1520
+ this.puzzle.create();
1521
+ }
1522
+ if (this.restoredState) {
1523
+ this.puzzle.doScale(this.restoredState.base[2]);
1524
+ } else {
1525
+ this.puzzle.scale();
1526
+ }
1527
+ this.puzzle.polyPieces.forEach((pp) => {
1528
+ pp.drawImage();
1529
+ if (this.restoredState) {
1530
+ pp.moveTo(pp.x, pp.y);
1531
+ } else {
1532
+ pp.moveToInitialPlace();
1533
+ }
1534
+ });
1535
+ this.puzzle.gameCanvas.style.top = this.puzzle.offsy + "px";
1536
+ this.puzzle.gameCanvas.style.left = this.puzzle.offsx + "px";
1537
+ this.puzzle.gameCanvas.style.display = "none";
1538
+ this.state = 25;
1539
+ if (this.restoredState) {
1540
+ this.restoredState = null;
1541
+ this.state = 50;
1542
+ }
1543
+ break;
1544
+
1545
+ case 25:
1546
+ this.puzzle.gameCanvas.style.display = "none";
1547
+ this.puzzle.polyPieces.forEach((pp) => {
1548
+ pp.canvas.classList.add("moving");
1549
+ });
1550
+ this.state = 30;
1551
+ break;
1552
+
1553
+ case 30:
1554
+ this.puzzle.optimInitial();
1555
+ setTimeout(() => this.events.push({ event: "finished" }), 1200);
1556
+ this.state = 35;
1557
+ break;
1558
+
1559
+ case 35:
1560
+ if (!event || event.event !== "finished") return;
1561
+ this.puzzle.polyPieces.forEach((pp) => {
1562
+ pp.canvas.classList.remove("moving");
1563
+ });
1564
+ this.state = 50;
1565
+ break;
1566
+
1567
+ case 50:
1568
+ if (!event) return;
1569
+ if (event.event === "stop") {
1570
+ this.state = 10;
1571
+ return;
1572
+ }
1573
+ if (event.event === "nbpieces") {
1574
+ this.puzzle.nbPieces = event.nbpieces;
1575
+ this.state = 20;
1576
+ } else if (event.event === "save") {
1577
+ this.state = 120;
1578
+ } else if (event.event === "touch") {
1579
+ this.moving = {
1580
+ xMouseInit: event.position.x,
1581
+ yMouseInit: event.position.y,
1582
+ tInit: tStamp
1583
+ };
1584
+ for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1585
+ let pp = this.puzzle.polyPieces[k];
1586
+ if (pp.isPointInPath(event.position)) {
1587
+ pp.selected = true;
1588
+ pp.drawImage();
1589
+ this.moving.pp = pp;
1590
+ this.moving.ppXInit = pp.x;
1591
+ this.moving.ppYInit = pp.y;
1592
+ this.puzzle.polyPieces.splice(k, 1);
1593
+ this.puzzle.polyPieces.push(pp);
1594
+ pp.canvas.style.zIndex = this.puzzle.zIndexSup;
1595
+ this.state = 55;
1596
+ return;
1597
+ }
1598
+ }
1599
+ this.state = 100;
1600
+ } else if (event.event === "touches") {
1601
+ this.moving = { touches: event.touches };
1602
+ this.state = 110;
1603
+ } else if (event.event === "wheel") {
1604
+ if (event.wheel.deltaY > 0) this.puzzle.zoomBy(1.3, this.lastMousePos);
1605
+ if (event.wheel.deltaY < 0) this.puzzle.zoomBy(1 / 1.3, this.lastMousePos);
1606
+ }
1607
+ break;
1608
+
1609
+ case 55:
1610
+ if (!event) return;
1611
+ if (event.event === "stop") {
1612
+ this.state = 10;
1613
+ return;
1614
+ }
1615
+ switch (event.event) {
1616
+ case "moves":
1617
+ case "touches":
1618
+ this.moving.pp.selected = false;
1619
+ this.moving.pp.drawImage();
1620
+ this.moving = { touches: event.touches };
1621
+ this.state = 110;
1622
+ break;
1623
+ case "move":
1624
+ if (event?.ev?.buttons === 0) {
1625
+ this.events.push({ event: "leave" });
1626
+ break;
1627
+ }
1628
+ this.moving.pp.moveTo(
1629
+ event.position.x - this.moving.xMouseInit + this.moving.ppXInit,
1630
+ event.position.y - this.moving.yMouseInit + this.moving.ppYInit
1631
+ );
1632
+ break;
1633
+ case "leave":
1634
+ if (this.puzzle.rotationAllowed && tStamp < this.moving.tInit + 250) {
1635
+ this.moving.pp.rotate((this.moving.pp.rot + 1) % 4);
1636
+ this.moving.pp.coerceToContainer();
1637
+ }
1638
+ this.moving.pp.selected = false;
1639
+ this.moving.pp.drawImage();
1640
+ let merged = false;
1641
+ let doneSomething;
1642
+ do {
1643
+ doneSomething = false;
1644
+ for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1645
+ let pp = this.puzzle.polyPieces[k];
1646
+ if (pp === this.moving.pp) continue;
1647
+ if (this.moving.pp.ifNear(pp)) {
1648
+ merged = true;
1649
+ if (pp.pieces.length > this.moving.pp.pieces.length) {
1650
+ pp.merge(this.moving.pp);
1651
+ this.moving.pp = pp;
1652
+ } else {
1653
+ this.moving.pp.merge(pp);
1654
+ }
1655
+ doneSomething = true;
1656
+ break;
1657
+ }
1658
+ }
1659
+ } while (doneSomething);
1660
+ this.puzzle.evaluateZIndex();
1661
+ if (merged) {
1662
+ this.moving.pp.selected = true;
1663
+ this.moving.pp.drawImage(true);
1664
+ this.moving.tInit = tStamp + 500;
1665
+ this.state = 56;
1666
+ break;
1667
+ }
1668
+ this.state = 50;
1669
+ if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0) {
1670
+ this.state = 60;
1671
+ }
1672
+ }
1673
+ break;
1674
+
1675
+ case 56:
1676
+ if (tStamp < this.moving.tInit) return;
1677
+ this.moving.pp.selected = false;
1678
+ this.moving.pp.drawImage();
1679
+ if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0)
1680
+ this.state = 60;
1681
+ else
1682
+ this.state = 50;
1683
+ break;
1684
+
1685
+ case 60:
1686
+ this.playing = false;
1687
+ if (this.options.onWin) this.options.onWin();
1688
+ this.puzzle.container.innerHTML = "";
1689
+ this.puzzle.getContainerSize();
1690
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1691
+ const finalWidth = this.tmpImage.style.width;
1692
+ const finalHeight = this.tmpImage.style.height;
1693
+ this.tmpImage.style.width = `${this.puzzle.nx * this.puzzle.scalex}px`;
1694
+ this.tmpImage.style.height = `${this.puzzle.ny * this.puzzle.scaley}px`;
1695
+ this.tmpImage.style.left = `${((this.puzzle.polyPieces[0].x + this.puzzle.scalex / 2 + this.puzzle.gameWidth / 2) / this.puzzle.contWidth) * 100}%`;
1696
+ this.tmpImage.style.top = `${((this.puzzle.polyPieces[0].y + this.puzzle.scaley / 2 + this.puzzle.gameHeight / 2) / this.puzzle.contHeight) * 100}%`;
1697
+ this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1698
+ this.tmpImage.classList.add("moving");
1699
+ setTimeout(() => {
1700
+ this.tmpImage.style.top = this.tmpImage.style.left = "50%";
1701
+ this.tmpImage.style.width = finalWidth;
1702
+ this.tmpImage.style.height = finalHeight;
1703
+ }, 0);
1704
+ this.puzzle.container.appendChild(this.tmpImage);
1705
+ this.state = 15;
1706
+ break;
1707
+
1708
+ case 100:
1709
+ if (!event) return;
1710
+ if (event.event === "move") {
1711
+ if (event?.ev?.buttons === 0) {
1712
+ this.state = 50;
1713
+ break;
1714
+ }
1715
+ this.puzzle.sweepBy(
1716
+ event.position.x - this.moving.xMouseInit,
1717
+ event.position.y - this.moving.yMouseInit
1718
+ );
1719
+ this.moving.xMouseInit = event.position.x;
1720
+ this.moving.yMouseInit = event.position.y;
1721
+ return;
1722
+ }
1723
+ if (event.event === "leave") {
1724
+ this.state = 50;
1725
+ return;
1726
+ }
1727
+ if (event.event === "touches") {
1728
+ this.moving = { touches: event.touches };
1729
+ this.state = 110;
1730
+ }
1731
+ break;
1732
+
1733
+ case 110:
1734
+ if (!event) return;
1735
+ if (event.event === "leave") {
1736
+ this.state = 50;
1737
+ return;
1738
+ }
1739
+ if (event.event === "moves") {
1740
+ const center = {
1741
+ x: (this.moving.touches[0].x + this.moving.touches[1].x) / 2,
1742
+ y: (this.moving.touches[0].y + this.moving.touches[1].y) / 2
1743
+ };
1744
+ const dInit = mhypot(
1745
+ this.moving.touches[0].x - this.moving.touches[1].x,
1746
+ this.moving.touches[0].y - this.moving.touches[1].y
1747
+ );
1748
+ const d = mhypot(
1749
+ event.touches[0].x - event.touches[1].x,
1750
+ event.touches[0].y - event.touches[1].y
1751
+ );
1752
+ const dRef = msqrt(this.puzzle.contWidth * this.puzzle.contHeight) / 5;
1753
+ this.puzzle.zoomBy(Math.exp((d - dInit) / dRef), center);
1754
+ this.moving.touches = event.touches;
1755
+ return;
1756
+ }
1757
+ break;
1758
+
1759
+ case 120:
1760
+ const savedData = this.puzzle.getStateData();
1761
+ const savedString = JSON.stringify(savedData);
1762
+ if (event && event.callback) {
1763
+ event.callback(savedString);
1764
+ }
1765
+ this.state = 50;
1766
+ break;
1767
+
1768
+ case 150:
1769
+ this.restoredString = "";
1770
+ if (event && event.data) {
1771
+ this.restoredString = event.data;
1772
+ this.state = 155;
1773
+ } else {
1774
+ try {
1775
+ this.restoredString = localStorage.getItem("savepuzzle");
1776
+ if (!this.restoredString) this.restoredString = "";
1777
+ } catch (exception) {
1778
+ this.restoredString = "";
1779
+ }
1780
+ if (this.restoredString.length === 0) {
1781
+ this.state = 15;
1782
+ break;
1783
+ }
1784
+ this.state = 155;
1785
+ }
1786
+ break;
1787
+
1788
+ case 155:
1789
+ try {
1790
+ this.restoredState = JSON.parse(this.restoredString);
1791
+ } catch (error) {
1792
+ this.restoredState = null;
1793
+ this.state = 10;
1794
+ break;
1795
+ }
1796
+ if (
1797
+ !this.restoredState.signature ||
1798
+ this.restoredState.signature !== FILE_SIGNATURE ||
1799
+ !this.restoredState.src
1800
+ ) {
1801
+ this.restoredState = null;
1802
+ this.state = 10;
1803
+ break;
1804
+ }
1805
+ this.puzzle.imageLoaded = false;
1806
+ this.puzzle.srcImage.src = this.restoredState.src;
1807
+ if (this.restoredState.origin)
1808
+ this.puzzle.srcImage.dataset.origin = this.restoredState.origin;
1809
+ else
1810
+ delete this.puzzle.srcImage.dataset.origin;
1811
+ this.state = 158;
1812
+ // fall through
1813
+
1814
+ case 158:
1815
+ if (event && event.event === "srcImageLoaded") {
1816
+ this.state = 160;
1817
+ } else if (event && event.event === "wrongImage") {
1818
+ this.state = 10;
1819
+ }
1820
+ break;
1821
+
1822
+ case 160:
1823
+ this.tmpImage.src = this.puzzle.srcImage.src;
1824
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1825
+ this.state = 20;
1826
+ break;
1827
+ }
1828
+ }
1829
+
1830
+ // ============================================================================
1831
+ // Public API Methods
1832
+ // ============================================================================
1833
+
1834
+ /**
1835
+ * Start a new game with the current settings
1836
+ */
1837
+ start() {
1838
+ this.events.push({ event: "nbpieces", nbpieces: this.options.numPieces });
1839
+ }
1840
+
1841
+ /**
1842
+ * Stop the current game
1843
+ */
1844
+ stop() {
1845
+ this.playing = false;
1846
+ if (this.options.onStop) this.options.onStop();
1847
+ this.events.push({ event: "stop" });
1848
+ }
1849
+
1850
+ /**
1851
+ * Reset the puzzle to initial state
1852
+ * Use this to start a new game with the same instance and container
1853
+ * The puzzle will reload the current image and be ready for a new game
1854
+ */
1855
+ reset() {
1856
+ this.events.push({ event: "reset" });
1857
+ this.state = 0;
1858
+ this.playing = false;
1859
+ this.restoredState = null;
1860
+ this.restoredString = "";
1861
+ }
1862
+
1863
+ /**
1864
+ * Save the current game state
1865
+ * @param {Function} callback - Optional callback to receive saved data as JSON string
1866
+ */
1867
+ save(callback) {
1868
+ if (callback) {
1869
+ this.events.push({ event: "save", callback });
1870
+ } else {
1871
+ // Default: save to localStorage
1872
+ this.events.push({ event: "save", callback: (data) => {
1873
+ try {
1874
+ localStorage.setItem("savepuzzle", data);
1875
+ } catch (exception) {
1876
+ console.error("Failed to save to localStorage:", exception);
1877
+ }
1878
+ }});
1879
+ }
1880
+ }
1881
+
1882
+ /**
1883
+ * Load a saved game state
1884
+ * @param {string} savedData - JSON string of saved data, or null to load from localStorage
1885
+ */
1886
+ load(savedData) {
1887
+ this.events.push({ event: "restore", data: savedData });
1888
+ }
1889
+
1890
+ /**
1891
+ * Set the puzzle image
1892
+ * @param {string} imageUrl - URL or data URL of the image
1893
+ */
1894
+ setImage(imageUrl) {
1895
+ this.options.image = imageUrl;
1896
+ this.puzzle.imageLoaded = false;
1897
+ this.puzzle.srcImage.src = imageUrl;
1898
+ delete this.puzzle.srcImage.dataset.origin;
1899
+ }
1900
+
1901
+ /**
1902
+ * Update puzzle options
1903
+ * @param {Object} newOptions - Options to update
1904
+ */
1905
+ setOptions(newOptions) {
1906
+ Object.assign(this.options, newOptions);
1907
+ if (newOptions.numPieces !== undefined) {
1908
+ this.puzzle.nbPieces = newOptions.numPieces;
1909
+ }
1910
+ if (newOptions.allowRotation !== undefined) {
1911
+ this.puzzle.rotationAllowed = newOptions.allowRotation;
1912
+ }
1913
+ if (newOptions.shapeType !== undefined) {
1914
+ this.puzzle.typeOfShape = newOptions.shapeType;
1915
+ }
1916
+ }
1917
+
1918
+ /**
1919
+ * Destroy the puzzle instance and clean up completely
1920
+ * Use this when you want to remove the puzzle entirely and create a new one
1921
+ * For reusing the same instance with a new game, use reset() instead
1922
+ */
1923
+ destroy() {
1924
+ // Stop animation loop
1925
+ if (this.animationFrameId) {
1926
+ cancelAnimationFrame(this.animationFrameId);
1927
+ this.animationFrameId = null;
1928
+ }
1929
+
1930
+ // Clear events
1931
+ this.events = [];
1932
+ this.playing = false;
1933
+
1934
+ // Clear container (removes all puzzle elements)
1935
+ if (this.puzzle && this.puzzle.container) {
1936
+ this.puzzle.container.innerHTML = "";
1937
+ }
1938
+
1939
+ // Note: Event listeners are not removed because they're bound to the container
1940
+ // If you need to completely remove listeners, you'd need to store references
1941
+ // For most use cases, clearing innerHTML and stopping animation is sufficient
1942
+ }
1943
+ }